diff --git a/.claude/skills/checking-torquescript-conventions/SKILL.md b/.claude/skills/checking-torquescript-conventions/SKILL.md
new file mode 100644
index 000000000..7785a04f1
--- /dev/null
+++ b/.claude/skills/checking-torquescript-conventions/SKILL.md
@@ -0,0 +1,69 @@
+---
+name: checking-torquescript-conventions
+description: Use when reviewing or verifying changed TorqueScript (.cs) files against the project's coding conventions — after editing or adding game/UI scripts, before committing or opening a PR, or whenever asked to check that script code conforms to TORQUE_SCRIPT.md.
+---
+
+# Checking TorqueScript conventions
+
+## Overview
+
+Verify that the `.cs` script files changed on this branch follow the project's TorqueScript
+conventions. The rules live in **`TORQUE_SCRIPT.md`** at the repo root — that file is the source
+of truth; this skill is the review procedure. Read the doc; don't check from memory (it evolves).
+
+## Steps
+
+1. **List the changed `.cs` files.** Default scope = everything this branch changed, committed or
+ not:
+ ```bash
+ git status --porcelain -- '*.cs' # uncommitted: modified (M), staged, untracked (??)
+ git diff --name-only ...HEAD -- '*.cs' # committed on this branch ( = development/master/main)
+ ```
+ Review the union, deduped. **Skip deleted (`D`) files.** The conventions target game/UI module
+ scripts — don't audit vendored engine code or unrelated sample toys unless those are what changed.
+ State which scope you used.
+
+2. **Read `TORQUE_SCRIPT.md`.** Use its rules + checklist as the criteria.
+
+3. **Check each file against the checklist** (below).
+
+4. **Report** per-file: `conforms`, or list each violation with the file, the rule it breaks, and a
+ `file:line`. End with a one-line summary.
+
+## Checklist (condensed — see TORQUE_SCRIPT.md for the full text)
+
+- One class per file; the filename matches the class (a shared prefix/suffix may be dropped). Every
+ `function X::y` in the file shares class `X` (or its base) — no god-namespace.
+- Managers/systems are `ScriptObject` subclasses, not methods piled on the module namespace.
+- The class configures itself in `onAdd`; the spawner sets only `class`/`superclass` + the values IT
+ controls (constructor params, incl. object handles).
+- `onRemove` deletes what the class created and cancels every **self-rescheduling / repeating**
+ `schedule()` it started — a pulse or tick loop (event id stored on `%this`).
+- Owned objects stored on `%this`; cross-boundary deletes are `isObject()`-guarded.
+- `class`/`superclass` inheritance uses the `init()` pattern (only the most-derived `onAdd` fires).
+- New globals are only `ActionMap` bind targets or genuine game-state singletons.
+
+## Legitimate patterns — do NOT flag these
+
+These look like violations but are correct per the doc. False-positives here are the most common
+failure of this review:
+
+- **The module object with many methods** (e.g. `PlanetXGame`). The module's create-function `%this`
+ is the ONE sanctioned top-level singleton — fine as long as it orchestrates (state machine, owns
+ child managers) and delegates real per-object behavior to those managers.
+- **Global `ActionMap` bind-target functions** (bare `function moveUp(%val)`). The engine calls them
+ by bare name, so they MUST be global. Fine when thin and delegating to an object.
+- **A pool manager that does NOT delete its pooled `SceneObject`s in `onRemove`.** Objects `add()`ed
+ to a Scene are owned by the scene (`clearScene` safeDeletes them); the manager only cancels its own
+ schedules. Deleting them too would be the bug.
+- **Fields set in a spawner's `new{}` block beyond class/position.** Correct when the spawner controls
+ that value.
+- **One-shot, fire-and-forget `schedule()`s** (e.g. a 120 ms color/flash reset). Rule 7 targets
+ self-rescheduling loops; one-shots don't need to be tracked or cancelled.
+
+## Common mistakes
+
+- Guessing the scope instead of running the git commands.
+- Re-deriving rules from memory instead of reading `TORQUE_SCRIPT.md`.
+- False-positives on the legitimate patterns above.
+- Checking non-`.cs` or deleted files.
diff --git a/.gitattributes b/.gitattributes
index d7ddf2ca0..91a74440f 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,2 +1,9 @@
-$ cat .gitattributes
-engine/lib/* linguist-vendored
\ No newline at end of file
+# Keep the vendored third-party libraries out of GitHub language statistics.
+engine/lib/* linguist-vendored
+
+# Line endings for the cross-platform generator scripts:
+# shell scripts must stay LF (CRLF breaks the shebang on macOS/Linux),
+# batch files stay CRLF for cmd.exe.
+*.sh text eol=lf
+*.command text eol=lf
+*.bat text eol=crlf
diff --git a/.github/workflows/PR-builds.yml b/.github/workflows/PR-builds.yml
index b919b528b..bef8435d1 100644
--- a/.github/workflows/PR-builds.yml
+++ b/.github/workflows/PR-builds.yml
@@ -1,129 +1,194 @@
name: Build Packages
+
on: [push, pull_request, workflow_dispatch]
+
+# Cancel superseded runs on the same ref to save runner minutes.
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+# NOTE: every job now builds from the CMake source of truth (generate a project
+# file, then build it) instead of the checked-in solution/make/xcode files.
+# Windows is verified; macOS / Linux / iOS are freshly migrated to CMake and may
+# need iteration — these jobs ARE the verification loop for those platforms.
+
jobs:
- Build-Windows-32bit-VS2019:
- name: 32-bit Windows On VS2019
- runs-on: windows-2019
+ # ===========================================================================
+ # Windows — generate the Visual Studio solution from CMake and build it.
+ # VS2019 is retired; we test VS2026 and VS2022, each in 64- and 32-bit.
+ # ===========================================================================
+ windows:
+ name: Windows ${{ matrix.label }}
+ runs-on: ${{ matrix.runner }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - { label: 'VS2026 x64', runner: windows-2025-vs2026, generator: 'Visual Studio 18 2026', arch: x64, artifact: Torque2D_Windows_x64_VS2026 }
+ - { label: 'VS2026 Win32', runner: windows-2025-vs2026, generator: 'Visual Studio 18 2026', arch: Win32, artifact: Torque2D_Windows_x86_VS2026 }
+ - { label: 'VS2022 x64', runner: windows-2022, generator: 'Visual Studio 17 2022', arch: x64, artifact: Torque2D_Windows_x64_VS2022 }
+ - { label: 'VS2022 Win32', runner: windows-2022, generator: 'Visual Studio 17 2022', arch: Win32, artifact: Torque2D_Windows_x86_VS2022 }
steps:
- - uses: actions/checkout@v2
- - uses: microsoft/setup-msbuild@v1.1
- with:
- vs-version: '[16.0, 17.0)'
- - run: msbuild -m "engine/compilers/VisualStudio 2019/Torque 2D.sln" /p:Configuration=Debug /p:Platform=win32
- - run: msbuild -m "engine/compilers/VisualStudio 2019/Torque 2D.sln" /p:Configuration=Release /p:Platform=win32
+ - uses: actions/checkout@v4
+ # Ensure a recent CMake — the "Visual Studio 18 2026" generator needs CMake 4.x.
+ - uses: lukka/get-cmake@latest
+ - name: Configure
+ run: cmake -S . -B build/ci -G "${{ matrix.generator }}" -A ${{ matrix.arch }}
+ - name: Build Debug
+ run: cmake --build build/ci --config Debug --parallel
+ - name: Build Release
+ run: cmake --build build/ci --config Release --parallel
- uses: actions/upload-artifact@v4
with:
- name: Torque2D_Windows_x86_32bit_VS2019
+ name: ${{ matrix.artifact }}
path: |
.
- ! .git/
- ! engine/
- Build-Windows-64bit-VS2019:
- name: 64-bit Windows On VS2019
- runs-on: windows-2019
+ !.git/**
+ !engine/**
+ !build/**
+
+ # ===========================================================================
+ # Linux — generate Unix Makefiles from CMake and build (x86_64 and 32-bit x86).
+ # ===========================================================================
+ linux-x64:
+ name: Linux x86_64
+ # Pinned to 22.04: the engine's X11 back-end needs GENUINE SDL 1.2 (it links
+ # the SDL-1.2 X11 driver symbol X11_KeyToUnicode and calls 1.2-only APIs).
+ # On 24.04, libsdl1.2-dev is the SDL2-based sdl12-compat shim, which does NOT
+ # export those symbols, so the link fails.
+ runs-on: ubuntu-22.04
steps:
- - uses: actions/checkout@v2
- - uses: microsoft/setup-msbuild@v1.1
- with:
- vs-version: '[16.0, 17.0)'
- msbuild-architecture: x64
- - run: msbuild -m "engine/compilers/VisualStudio 2019/Torque 2D.sln" /p:Configuration=Debug /p:Platform=x64
- - run: msbuild -m "engine/compilers/VisualStudio 2019/Torque 2D.sln" /p:Configuration=Release /p:Platform=x64
+ - uses: actions/checkout@v4
+ - uses: lukka/get-cmake@latest
+ - name: Install dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y build-essential \
+ libsdl1.2-dev libx11-dev libxft-dev libfreetype6-dev libopenal-dev libgl1-mesa-dev
+ - name: Build Debug
+ run: |
+ cmake -S . -B build/linux-debug -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Debug
+ # Bound parallelism to the core count: bare `--parallel` passes `make -j`
+ # with NO limit, which fires off every TU at once (100s of g++) and
+ # OOM-kills the runner ("received a shutdown signal"), hit by the
+ # memory-heavier 64-bit Debug build.
+ cmake --build build/linux-debug --parallel "$(nproc)"
+ - name: Build Release
+ run: |
+ cmake -S . -B build/linux-release -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release
+ cmake --build build/linux-release --parallel "$(nproc)"
- uses: actions/upload-artifact@v4
with:
- name: Torque2D_Windows_x86_64bit_VS2019
+ name: Torque2D_Linux_x86_64
path: |
.
- ! .git/
- ! engine/
- Build-Windows-32bit-VS2022:
- name: 32-bit Windows On VS2022
- runs-on: windows-latest
+ !.git/**
+ !engine/**
+ !build/**
+
+ linux-x86:
+ name: Linux x86 (32-bit)
+ # Pinned to 22.04 for genuine SDL 1.2 (see the linux-x64 job). The 32-bit
+ # build additionally needs NASM: platform/platformCPUInfo.asm (detectX86CPUInfo)
+ # is compiled only on 32-bit (it is x86-only and is skipped on 64-bit via
+ # TORQUE_64).
+ runs-on: ubuntu-22.04
steps:
- - uses: actions/checkout@v2
- - uses: microsoft/setup-msbuild@v1.1
- with:
- vs-version: '[17.0, 18.0)'
- - run: msbuild -m "engine/compilers/VisualStudio 2022/Torque 2D.sln" /p:Configuration=Debug /p:Platform=win32
- - run: msbuild -m "engine/compilers/VisualStudio 2022/Torque 2D.sln" /p:Configuration=Release /p:Platform=win32
+ - uses: actions/checkout@v4
+ - uses: lukka/get-cmake@latest
+ # 32-bit (multilib) toolchain + NASM + :i386 dev libraries.
+ - name: Install 32-bit dependencies (multilib)
+ run: |
+ sudo dpkg --add-architecture i386
+ sudo apt-get update
+ sudo apt-get install -y gcc-multilib g++-multilib nasm \
+ libsdl1.2-dev:i386 libx11-dev:i386 libxft-dev:i386 libfreetype6-dev:i386 \
+ libopenal-dev:i386 libgl1-mesa-dev:i386
+ - name: Build Release (-m32)
+ env:
+ # Point pkg-config / find_package at the 32-bit libraries.
+ PKG_CONFIG_PATH: /usr/lib/i386-linux-gnu/pkgconfig
+ run: |
+ cmake -S . -B build/linux32 -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release \
+ -DCMAKE_C_FLAGS=-m32 -DCMAKE_CXX_FLAGS=-m32 -DCMAKE_EXE_LINKER_FLAGS=-m32
+ cmake --build build/linux32 --parallel "$(nproc)"
- uses: actions/upload-artifact@v4
with:
- name: Torque2D_Windows_x86_32bit_VS2022
+ name: Torque2D_Linux_x86_32bit
path: |
.
- ! .git/
- ! engine/
- Build-Windows-64bit-VS2022:
- name: 64-bit Windows On VS2022
- runs-on: windows-latest
+ !.git/**
+ !engine/**
+ !build/**
+
+ # ===========================================================================
+ # macOS — generate an Xcode project from CMake and build it.
+ # ===========================================================================
+ macos:
+ name: macOS
+ runs-on: macos-latest
steps:
- - uses: actions/checkout@v2
- - uses: microsoft/setup-msbuild@v1.1
- with:
- vs-version: '[17.0, 18.0)'
- msbuild-architecture: x64
- - uses: ChristopheLav/windows-sdk-install@v1
- with:
- version-sdk: 22621
- features: 'OptionId.UWPCPP,OptionId.DesktopCPParm64'
- - run: msbuild -m "engine/compilers/VisualStudio 2022/Torque 2D.sln" /p:Configuration=Debug /p:Platform=x64
- - run: msbuild -m "engine/compilers/VisualStudio 2022/Torque 2D.sln" /p:Configuration=Release /p:Platform=x64
+ - uses: actions/checkout@v4
+ - uses: lukka/get-cmake@latest
+ - name: Configure
+ run: cmake -S . -B build/xcode -G Xcode
+ - name: Build Debug
+ run: cmake --build build/xcode --config Debug
+ - name: Build Release
+ run: cmake --build build/xcode --config Release
- uses: actions/upload-artifact@v4
with:
- name: Torque2D_Windows_x86_64bit_VS2022
+ name: Torque2D_macOS
path: |
.
- ! .git/
- ! engine/
- Build-Linux-32Bit:
- name: Build package for 32-bit x86 Linux
- runs-on: ubuntu-latest
+ !.git/**
+ !engine/**
+ !build/**
+
+ # ===========================================================================
+ # iOS — generate an Xcode project targeting iOS; build without code signing.
+ # ===========================================================================
+ ios:
+ name: iOS
+ runs-on: macos-latest
steps:
- - uses: actions/checkout@v2
- - run: cd engine/compilers/Make-32bit/ && make
+ - uses: actions/checkout@v4
+ - uses: lukka/get-cmake@latest
+ - name: Configure (iOS)
+ run: cmake -S . -B build/ios -G Xcode -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_DEPLOYMENT_TARGET=12.0
+ - name: Build Release (no code signing)
+ run: cmake --build build/ios --config Release -- CODE_SIGNING_ALLOWED=NO
- uses: actions/upload-artifact@v4
with:
- name: Torque2D_Linux_x86_32bit
+ name: Torque2D_iOS
path: |
.
- ! .git/
- ! engine/
- Build-Linux-64bit:
- name: Build package for 64-bit x86 Linux
+ !.git/**
+ !engine/**
+ !build/**
+
+ # ===========================================================================
+ # Android — modern Gradle build whose native step runs CMake via the NDK,
+ # producing libtorque2d.so packaged into an APK. arm64-v8a only for now.
+ # ===========================================================================
+ android:
+ name: Android (arm64-v8a)
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
- - run: cd engine/compilers/Make-64bit/ && make
- - uses: actions/upload-artifact@v4
+ - uses: actions/checkout@v4
+ - uses: actions/setup-java@v4
with:
- name: Torque2D_Linux_x86_64bit
- path: |
- .
- ! .git/
- ! engine/
- Build-MacOS:
- name: Build package for MacOS
- runs-on: macOS-latest
- steps:
- - uses: actions/checkout@v2
- - run: cd engine/compilers/Xcode && xcodebuild -project Torque2D.xcodeproj
- - uses: actions/upload-artifact@v4
- with:
- name: Torque2D_MacOS
- path: |
- .
- ! .git/
- ! engine/
- Build-iOS:
- name: Build package for iOS
- runs-on: macOS-latest
- steps:
- - uses: actions/checkout@v2
- - run: cd engine/compilers/Xcode_iOS && xcodebuild CODE_SIGNING_ALLOWED=no -project Torque2D.xcodeproj
+ distribution: temurin
+ java-version: '17'
+ - uses: android-actions/setup-android@v3
+ - name: Install NDK + CMake (match app/build.gradle ndkVersion)
+ run: sdkmanager "ndk;25.2.9519653" "cmake;3.22.1"
+ - name: Build APK (assembleDebug)
+ working-directory: engine/compilers/android-studio
+ run: |
+ chmod +x ./gradlew
+ ./gradlew assembleDebug --no-daemon --stacktrace
- uses: actions/upload-artifact@v4
with:
- name: Torque2D_iOS
- path: |
- .
- ! .git/
- ! engine/
+ name: Torque2D_Android_arm64
+ path: engine/compilers/android-studio/app/build/outputs/apk/**/*.apk
diff --git a/.gitignore b/.gitignore
index a9b8efce1..e68bcf412 100755
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,8 @@
/tmp/*
/preferences.cs
/engine/**/preferences.cs
+/PlanetXPrefs.cs
+/console.log
*.dso
*.edso
*.user
@@ -24,6 +26,9 @@ Torque2D.app
Torque2D_DEBUG.app
Torque2D.exe
Torque2D_DEBUG.exe
+# OpenAL Soft runtime staged next to the exe by CMake POST_BUILD (build artifact).
+# Root-anchored so the per-arch source DLLs under engine/lib/openal/ stay tracked.
+/OpenAL32.dll
Torque2DGame.app
Torque2DGame_Debug.app
Torque2D
@@ -83,10 +88,48 @@ engine/compilers/android-studio/app/src/main/obj/
engine/compilers/android-studio/app/src/main/libs/
engine/compilers/android-studio/app/src/main/game/
engine/compilers/android-studio/app/.externalNativeBuild/
+engine/compilers/android-studio/app/.cxx/
-# Linux build files #
-#####################
-engine/compilers/Make-32bit/Debug/
-engine/compilers/Make-32bit/Release/
-engine/compilers/Make-64bit/Debug/
-engine/compilers/Make-64bit/Release/
+# CMake out-of-source build directory #
+#######################################
+/build/
+
+# Integration test leavings (see tests/README.md) #
+##################################################
+# The one-line boot stub tests/run.ps1 writes at the root, because the engine
+# takes its working directory from the boot script's own folder.
+/_boot.cs
+# Screenshots the tests/shots harnesses write, and the throwaway projects the
+# tests build to have something to edit. A test that adds a project folder wants
+# a line here too; ProjectManager.setProjectFolder names them.
+/shots/
+# One console log per test, written by tests/run.ps1 and run-unit.ps1 so a run
+# neither collides with a hand-started editor nor throws away the log of the
+# suite that failed.
+/tests/logs/
+/assetLibrarySmokeProject/
+/assetPickerSmokeProject/
+/borderPaneSmokeProject/
+/borderSmokeProject/
+/colorPopupShotProject/
+/colorPopupSmokeProject/
+/cursorPaneSmokeProject/
+/cursorSlotsSmokeProject/
+/fontShotProject/
+/fontSmokeProject/
+/inspectorTextSmokeProject/
+/inspectorVariantsSmokeProject/
+/profileFormShotProject/
+/profileFormSmokeProject/
+/smokeThemeProject/
+/unsavedSmokeProject/
+# GuiDefaultProfile.fontDirectory is the unexpanded string "^EditorCore/gui/fonts"
+# (guiProfiles.cs), and anything that bakes a font-cache miss recorded against it
+# writes to a folder of that literal name. Nothing in the suite does, but it has
+# happened once, and a stray caret folder is never content.
+/^EditorCore/
+
+# Vendored prebuilt Android libraries (override the global *.a / *.so ignore) #
+##############################################################################
+!engine/lib/freetype/android/lib/**/*.a
+!engine/lib/openal/Android/**/*.so
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 000000000..d91a24bb4
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,277 @@
+# Changelog
+
+Notable changes to Torque2D 4.0 "Rocket Edition", from Early Access 1 onward.
+
+The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Entries
+describe what changed for someone building a game on the engine: what you can do,
+what your scripts have to say, and what ends up in your files.
+
+## [4.0-ea4] - 2026-08-18
+
+Early Access 4 builds out the GUI Editor that Early Access 3 introduced, gives the
+Asset Manager a dirty/save model and a purpose-built inspector for every asset kind,
+and makes CMake the only build system.
+
+### Breaking
+
+- Editing an asset no longer writes its file. `refreshAsset()` now only marks the asset as having unsaved changes and tells everything watching it that it changed; `saveAsset()` is the only thing that writes. Any script that relied on a setter persisting its change must now call `saveAsset()` (or `AssetDatabase.saveAllDirtyAssets()`) itself, and a running game no longer rewrites its own content as a side effect of a setter.
+- `AnimationAsset.setNamedCellsMode()` was removed. Named-cell mode is now read live from the animation's image asset, so an animation uses names exactly when its image is in explicit-cell mode. Old files still load: an unrecognized `NamedCellsMode` attribute is read as a dynamic field and dropped on the next save, and an image that does not state its mode is inferred from the presence of its `Cells` node.
+- AppCore no longer ships GUI profiles. Its roughly seventy hand-written `Gui*Profile` objects were replaced by themes, so a project that named one must either apply a theme (the Gui Editor's Set Theme button does it in one click) or define the profile itself. `GuiDefaultProfile` and its border moved into C++ and always exist.
+- The hand-maintained Visual Studio solutions, Xcode projects, Linux makefiles and Emscripten recipe under `engine/compilers/` were deleted. Generate a project from the root `CMakeLists.txt` instead, either directly or through the root scripts (`generate-vs2022.bat`, `generate-vs2026.bat`, `generate-xcode.command`, `generate-xcode-ios.command`, `build-linux.sh`, `generate-emscripten.sh`). Only `engine/compilers/android-studio` remains, and its Gradle build drives the same CMake.
+- The horizontal and vertical sizing flags were named for the edge they hold: `anchorLeft`, `anchorRight`, `anchorTop`, `anchorBottom`, and `scale` for the old `relative`. The old spellings still load, but a Gui saved by this build cannot be opened by an older one, which has no `anchorLeft` in its table and silently falls back to the first entry.
+- `hidden` and `locked` are no longer written to a file. They are editor working state, not properties of the document; they still read and set normally.
+- `GuiImageButtonCtrl` was removed. A profile's `imageAsset` is already indexed by control state, so a plain `GuiButtonCtrl` wearing a four-frame strip draws the same Normal, Hover, Down and Inactive faces.
+- `GuiEditCtrl.saveSelection()` and `loadSelection()` were removed. They were an unused prefab mechanism writing the old console-object format; the Gui Editor's clipboard replaces them.
+- `ParticleAsset.getFieldValue(time)` and `ParticleAssetEmitter.getFieldValue(time)` are now `getFieldValueAtTime(time)`. They were silently shadowing `SimObject.getFieldValue(fieldName)`, so asking a particle asset for any field's value returned a curve sample instead.
+- `GuiTreeViewCtrl` drag-to-reorder now needs the new `AllowReorder` field, which is off by default. It used to be implied by `BindToGuiEditor`, and a tree holding anything other than `GuiControl`s crashed when a row was dragged.
+- `GuiButtonCtrl` no longer seeds its caption with "Button". An empty caption is written as an absent attribute, so a default one could never survive its own round trip. `GuiCheckBoxCtrl` and `GuiRadioCtrl` inherit the change; `GuiDropDownCtrl` keeps its "none", which is an empty state rather than a caption.
+- Toy modules and projects that carried their own `AppCore` copy should take the library's, which now loads themes and cursors at boot. The toybox's older copy is deliberately left alone and its module says so.
+
+### Added
+
+- The Gui Editor gained a control palette: thirty illustrated tiles in four collapsible groups, in a picture grid or a labeled row list, dropped onto the canvas by dragging or placed for you by clicking.
+- The Gui Editor gained undo and redo across placing, moving, resizing, nudging, deleting, reparenting, retheming and field edits, plus cut, copy, paste and duplicate, which work within a Gui and between them.
+- A properties pane replaced the generic inspector in the Gui Editor. It shows only the fields the selected control's class actually reads, with purpose-built editors for the common ones: an anchor picker for sizing, color swatches, an image-asset picker you choose by looking at it, a text block, and editors for things that were previously unreachable, such as a list box's rows, a tab book's pages and a menu bar's items.
+- An Explorer panel shows the whole control tree with a 16-pixel picture of each control's class, columns for hiding and locking, and drag-to-reparent.
+- Controls take their appearance from a `GuiProfileTheme`: a set of profiles, borders and cursors derived from three fonts, six colors and a border size. A theme is authored in the Gui Profile Editor against a live preview, a control dropped on the canvas arrives already wearing the right profile, Set Theme re-skins a whole Gui, and AppCore loads a project's `themes/` folder at runtime so a themed Gui looks the same outside the editor. A stock theme ships in `library/themes` and a new project gets a copy.
+- Cursors became the theme's third member family: per-theme art under `/themes/cursors/`, a hot-spot editor that draws the art a pixel at a time, and `installThemeCursors` so a game can swap cursor sets at runtime.
+- The Gui Editor asks before throwing away a Gui you have changed, on New, Open, Close Project and Exit, and File gained Revert. The document's name sits in the Gui Tools title with an asterisk when it is dirty.
+- List boxes and drop downs carry the rows they were authored with. Items are saved as a TAML custom node, drawn on the canvas as they are typed, and reachable from script through `getItemList()` / `setItemList()`. Saving as `.gui` script now warns which state that format cannot keep.
+- A tab book dropped into a Gui arrives holding a page and draws a "+" for adding more; a menu bar arrives holding a menu and drops an editable box under it for its commands.
+- The Asset Manager gained a dirty/save model: Save, Revert, Duplicate and Undo/Redo on the inspector title bar, a badge on unsaved library tiles, and a Save All / Discard All / Cancel prompt in front of Close Project and Exit. `isAssetDirty`, `saveAsset`, `saveAllDirtyAssets`, `revertAsset`, `duplicateAsset`, `getDirtyAssetCount` and `findAssetDirty` are on `AssetManager`; `isAssetDirty`, `saveAsset`, `revertAsset`, `createStateSnapshot` and `restoreStateSnapshot` are on `AssetBase`.
+- The asset library gained tile and row views, a search box matching name, description and category, and sorting by name or category. The choice is remembered between runs in a new editor preferences file.
+- Image, animation, font, audio and particle assets each got an inspector pane of their own in place of the stock inspector: only the fields worth showing, reflowing into one to four columns, and a readout saying what actually loaded — an image's real size and frame count, an animation's duration and frame rate, a font's native size and glyph count, a sound's length and format, and warnings for the ways each fails silently.
+- An animation editor replaced the preview for animation assets: the art playing on the left, every frame the image offers on the right, and the frames the animation plays along the bottom, with drag-and-drop, scrubbing, a transport bar, a loop toggle, a keep-frame-rate option and a "frames 28 to 32" range builder including ping-pong and hold.
+- An animation can name its frames. An image in explicit-cell mode gives every cell a name (auto-generated as `Frame` where one is missing), and an animation listing names survives the sheet being re-cut or re-ordered. A name whose cell is gone draws as an outlined empty cell rather than being silently dropped. New bindings: `getFrameCount`, `getAnimationFrameCount`, `getMissingFrames`.
+- The particle emitter graph gained a combined Color Channel view: red, green and blue on one plot with a strip underneath showing the color they actually mix to across a particle's life.
+- Each editor now owns its own menus and lends them to the shared bar while it is in front, so the Asset Manager has its own File and Edit with its own Save, Revert, Undo, Redo, Duplicate and Delete.
+- Audio descriptions gained a `Priority` flag, settable in an `AudioAsset`'s TAML, so a looping non-positional sound such as background music is not voice-stolen and restarted when the mixer runs out of voices. `alxPlayPreview` auditions a sound at full volume on a reserved channel regardless of the game's mix, and `OpenALIsInitialized` answers whether the driver is already up (`OpenALInit` begins by shutting down, so asking first matters).
+- New script API on `GuiControl`: `rendersChildren()`, `applySizing()`, a `childrenReordered` callback, and the `canBeChildOf` / `isGeometryEditable` virtuals the editor consults. `GuiFrameSetCtrl` gained `getFrameLayout()` / `setFrameLayout()`. `GuiTreeViewCtrl` gained `IconImage`, `IconSize`, `IndentSize` and an `onGetItemIcon` callback. `GuiSliderCtrl` gained a `thumbProfile`.
+- `SimObject.deepClone()` copies fields, dynamic fields and the whole child tree without running any script lifecycle, which is what makes the editor's clipboard faithful.
+- `getInstalledFonts()` exposes the platform font enumerator to script (implemented on Windows, macOS and, newly, Linux via fontconfig), with `getUncachedFonts()`, `clearUncachedFonts()` and `writeOneFontCache()` for baking only the face-and-size pairs that were actually rendered. `setLogFileName()` names the console log. `isEditorMode()` reports whether names are being shadowed.
+- `GuiColorPopupCtrl` gained two optional rows: a wrapping grid of script-supplied swatches (`addSwatchI`, `addSwatchF`, `clearSwatches`, `selectSwatch`) and one numeric box per channel in 0-255 or 0.0-1.0. Both are off by default.
+- The web target builds and runs. `generate-emscripten.sh` produces a WebAssembly build that boots in a browser, with FreeType compiled to wasm so any face and size renders rather than only the pre-baked `.uft` caches.
+- Android builds through the root CMake and renders the editor on-device; iOS builds and runs on both the simulator and a real device.
+- `PlanetX`, a complete twin-stick demo game, ships as a reference project: a title screen, a noise-generated level, co-op, weapon upgrades chosen between levels, an options screen with rebindable controls, and a pause menu. It is also the reference implementation for `TORQUE_SCRIPT.md`, the new prescriptive style guide for TorqueScript.
+- `TruckToy` was reskinned as an alien-world space rover with a camera-driven parallax background and object-pooled effects.
+- The test suites got runners: `tests\run.ps1` (and `tests/run.sh` for macOS and Linux) for the TorqueScript integration suites, and `tests\run-unit.ps1` for the GoogleTest unit suite.
+- The New Project dialog asks for a **Module Name**, an **Author** and a **Description**, and offers a **Game Core** template to copy from. Module Name defaults to the title stripped to identifier characters with `Game` appended, and follows the title until you edit it.
+- `BlankGame` ships the folders it declares. It had declared eight asset paths and shipped three, so every project made from it warned four times the first time it was opened. Each new folder carries a readme saying what belongs in it and how a file there becomes an asset.
+
+### Changed
+
+- The engine boots the desktop-class editor on every platform, so every platform now gets the 3 MB frame allocator (iOS, Android and Web were on 512 KB and overran it).
+- A `GuiScrollCtrl` leaves room for its own scroll bar. An axis whose bar is `alwaysOff` has a real size and hands it to children; an axis that can scroll does not, and `fill` and `center` are stripped there rather than on both axes.
+- `GuiWindowCtrl`'s default title height went from 20 to 28, which is what a themed title bar with a real font and a chrome border needs.
+- The Explorer tree indents by twelve pixels rather than a full row height, since two gutter columns, a triangle and a class icon already sit in front of the name.
+- Windows ships OpenAL Soft 1.24.3 per architecture, staged next to the executable by CMake. The 32-bit `OpenAL32.dll` committed at the repo root since 2.0 could not be loaded by an x64 build, so audio failed to initialize on every modern Windows build.
+- An emitter's `BlendMode`, `SrcBlendFactor` and `DstBlendFactor` are now actually read. They round-tripped through TAML and were ignored; one setting on the `ParticlePlayer` covered every emitter. Existing particle assets may render differently.
+- `QuantityVariation` on an emitter now applies. It was initialized from the quantity base field, so every emitter got a spurious half-base jitter instead of the variation you set — the stock bonfire emitted 5 to 15 per interval and now emits ten.
+- `setEmitterAngle` takes and returns degrees, matching what the persist field and the renderer already meant. It used to store radians and convert back on read, so it agreed with itself and with nothing else.
+- Zoom works on the 0-to-1 particle graphs (all four color channels and alpha), which had exactly one zoom level and two dead buttons.
+- A `GuiControlProfile`'s `bitmap` and a `GuiCursor`'s `bitmapName` are written relative to the game root when they point inside it. Both are `TypeFilename`, which expands to an absolute path the moment it is set, so what got saved named a folder on one developer's machine. TAML no longer collapses a path that is already relative back into an absolute one.
+- Font caches are no longer baked while you edit a theme; a save bakes each face and size the theme actually rendered at. Changing a font size went from several seconds of frozen engine to nothing.
+- A new project's game module is named after the project rather than being called `BlankGame`. The Author and Description you type now reach that module too, instead of only AppCore. Existing projects are unaffected -- their module keeps the name it was made with.
+- Renaming a module rewrites the module's own source, not just its `module.taml`. A ModuleId is also the namespace the engine calls `::create()` on and the front half of every asset id, so changing only the definition file left a module that loaded, reported its new name everywhere the editor looked, and silently never ran. All three rename paths go through one place now.
+
+### Fixed
+
+- Numeric fields were silently dropped from saved TAML on glibc, so every saved scene, Gui and profile quietly lost its extents, positions, paddings and unnamed colors on Linux. `getPrefixedDataField` formatted a value into the same console return buffer it read it from, which is undefined and comes back empty on glibc.
+- A word too long to wrap left an empty line under it, which threw off vertical alignment, `textExtend` sizing and the decision about whether text fits at all.
+- A newline drew the font's missing-glyph box and took up width, because `isValidChar` answered true for every character on Windows. An empty multi-line text box had no caret, because building paragraphs with `getline` could not tell empty text from no text. Pressing Return left two carets blinking.
+- Deleting a border profile left every `GuiControlProfile` that referenced it pointing at freed memory.
+- A control that changed parent kept the size it had. A scaled control applied the old parent's proportion to the new parent's extent, so a 200-wide button dropped into a 200-wide container arrived 50 wide.
+- A control hidden in the editor is now out of the way rather than merely out of sight: it stops taking clicks aimed at what is behind it, loses its sizing handles, and is skipped by rubber-band selection.
+- A drop was accepted anywhere on the screen, so dragging a control back onto the palette to change your mind added it behind the palette. A click-placed control is now centered in the visible part of its container rather than in a middle that may be off-screen.
+- Fixed on macOS and iOS: a frozen simulation clock, frozen color fade-outs, duplicate windows, a crash enumerating display modes with the display asleep, a crash sorting list-box items under the hardened `std::sort` in newer toolchains, a font enumerator that returned nothing and one that over-released, an assert on a character a font legitimately lacks, `createPath` on a bare file name, and a file's size not counting bytes still in the stdio buffer.
+- Fixed on Linux: `getDirectoryList` returned only the path it was given, so the project selector found no projects; the window was not resizable; `Platform::pathCopy` and `Platform::fileRename` were stubs returning false, so creating a project produced an empty folder; and `dStrcatl`, `dStrrev` and `dItoa` were each wrong.
+- Fixed on Windows: `pathCopy` could not create the folder its destination sat in and would copy a directory into itself; the folder picker crashed on x64 because a 64-bit window procedure pointer was truncated.
+- A `.gui.taml` naming a cursor that is not registered took the editor down with a fatal assert; the type now warns and leaves the field unset.
+- An animation kept the frame numbers from its image's previous cut, and went on playing out of the wrong frames with nothing said, because re-cutting an image did not revalidate the animations depending on it.
+- `AssetBase::copyTo` was wrong in four ways, all reachable through `clone()` and `acquireAsset(id, true)`: an image copied its cell count into its cell offset, never copied image layers and dropped explicit cells; and an animation and a particle emitter both chose between numbered and named frames by reading the target's mode rather than the source's.
+- Selecting a particle asset in the Asset Manager crashed the editor: `PixelArea`'s region name was never initialized, so every frame of every ordinary cell-mode image carried an indeterminate pointer.
+- A tooltip on a control with no tooltip profile asserted on a zero reference count when the control slept.
+- The editor's modules were never unloaded at exit, so no editor teardown ever ran and the profiles a live preview was wearing could be freed out from under it.
+- Accelerators from a grayed-out menu still fired, so Ctrl+N ran the Gui Editor's New Gui from inside the Asset Manager.
+- `findAssetPrivate`'s five-argument form called `findAssetInternal`.
+- `alxGetAudioLength` acquired the asset and released it on none of its return paths.
+- A bitmap font was never initialized and never cleared, so an asset pointed at a missing `.fnt` kept the glyphs of the font it used to have and pointing one at a second `.fnt` left the union of both.
+- `GuiMenuItemCtrl`'s `Radio` field was declared as an integer over a one-byte member, so a plain command could read back as a radio item.
+- A menu bar's `findHitControl` hid rather than overrode its base, so clicking a menu on an authored bar handed the editor the wrong control.
+- Declared asset paths kept the case they were written in. `Path` and `Extension` were plain strings, which the string table interns case-insensitively and hands back whichever spelling reached it first -- so a module copied from a template could come out declaring `Path="Sprites"` where the template said `sprites`. On Windows nobody noticed; on Linux that directory does not exist, so images dropped into `sprites/` were never scanned and never became assets, silently, with a `module.taml` that looked correct.
+- Directory names survive a scan with the case they have on disk. `readdir` results were interned case-insensitively, and two of the most ordinary asset folder names -- `sprites` and `fonts` -- are interned during static initialisation by unrelated engine code, so those two could never come back correctly. A folder could be reported by a scan and then fail to open.
+- The engine reported its version as 1.0. `TORQUE_GAME_ENGINE` had never been raised past `1000`, so `getVersionNumber()` answered `1000` where 4.0 is `4000`, and the server-query compatibility check compared every build as 1.0. `getVersionString()` returned `"Open Source"`, which is not a version at all; it now returns `"4.0 Early Access 4"`, which is also what Linux prints at start-up.
+
+### Removed
+
+- The `OpenAL32.dll` tracked at the repository root; it is now a build artifact staged by CMake.
+- `GuiEditorColorWindow`, a scratch window for comparing color picker modes that was still exec'd on every editor start.
+
+## [4.0-ea3] - 2026-04-18
+
+The release that introduced the GUI Editor as a real editor tab.
+
+### Breaking
+
+- `GuiTreeViewCtrl` was rewritten and its script API replaced wholesale. Thirty-seven methods went — among them `clear`, `open`, `insertItem`, `removeItem`, `selectItem`, `getSelectedItem`, `getItemText`, `findItemByName`, `moveItemUp`, `scrollVisible` and `buildVisibleTree` — in favor of seven: `inspect`, `uninspect`, `refresh`, `refreshItemText`, `getItemOpen`, `setItemOpen` and `getItemParent`. A tree is now driven by pointing it at an object rather than by filling it item by item. The fields `tabSize`, `itemHeight`, `fullRowSelect`, `destroyTreeOnSleep`, `MouseDragging`, `MultipleSelections`, `DeleteObjectAllowed` and `DragToItemAllowed` went with them.
+- `GuiFrameSetCtrl` was rewritten and its script API replaced. `addColumn`, `addRow`, `removeColumn`, `removeRow`, `getColumnCount`, `getRowCount`, `getColumnOffset`, `setColumnOffset`, `getRowOffset`, `setRowOffset`, `frameBorder`, `frameMinExtent` and `frameMovable` were replaced by `createHorizontalSplit`, `createVerticalSplit`, `setFrameSize` and `anchorFrame`, and the fields `columns`, `rows`, `borderWidth`, `borderEnable`, `borderMovable`, `autoBalance` and `fudgeFactor` are gone.
+- `GuiControlProfile` lost `mouseOverSelected`, `profileForChildren`, `soundButtonDown` and `soundButtonOver`, with no successors. A profile still setting button sounds silently stops making them; use the button's callbacks instead.
+- `GuiControl` lost the deprecated `Modal` field and `SetFirstResponder`, and `GuiColorPickerCtrl.getSelectorPos2()` was removed.
+
+### Added
+
+- The Gui Editor: a control list you drag onto the canvas, an inspector, a control tree with reordering, arrow-key nudging (Ctrl for extent, Shift to move faster), a grid with a size dialog, menus, and New / Open / Save for both the `.gui` script format and TAML.
+- `GuiFrameSetCtrl`: frames that position their children and resize when a divider is dragged, windows that can be dragged out of the frame set to float and docked back in, and windows that stack into generated tab books and can be pulled back out. Its layout saves in both formats.
+- `GuiColorPopupCtrl`, a ready-to-use color popup, alongside a reworked `GuiColorPickerCtrl` with repaired display modes, a profile-driven selector, a checkered backdrop behind the alpha slider and text support. The inspector uses the popup for color fields, with text boxes for the four channel values.
+- Complex Colors: a separate blend color per corner of a sprite, usable on the individual sprites of a `CompositeSprite`, which makes lighting-like blends possible.
+- `SceneWindow` event pass-through: an event a scene window does not consume can be passed to controls behind it, so a sprite-built UI window can sit over a game world.
+- Mounted cameras can zoom.
+- A `fill` resize mode that keeps a control at 0,0 matching its parent's content area.
+- `GuiProgressCtrl` gained instant setting plus `onDisplayChange` and `onProgressComplete` callbacks.
+- The inspector remembers which panels were open between objects, and gained a `GuiCursor` field type.
+- The Asset Admin was rebuilt on the frame set.
+- A first pass at a CMake build, and the Screen Fade library module gained dialog swapping.
+
+### Changed
+
+- GoogleTest went from 1.6.0 to 1.17.0, which moved the engine from C++14 to C++17.
+- Tabbing through inspector controls works: check boxes and drop downs show first responder, hidden controls cannot be tabbed to, a text box loses focus on a click outside it, and a text box tabbed into selects its contents.
+- The frame set clips a child that cannot fit inside its frame rather than modifying the child's minimum extent.
+
+### Fixed
+
+- `GuiControlProfile` reference counts were wrong, usually high, which held resources far too long and occasionally tripped a fatal assert. Fixing it exposed a second bug: the code that reinstated a profile's image assets when it came back into use had not worked for a long time.
+- Buttons and other controls inside a tab page could not be dragged, because the page always stole the selection.
+- `mRound(-0.49)` returned negative zero.
+- A non-ASCII character in the executable's path broke startup on Win32.
+- Sizable bordered textures could exceed their bounds at sizes smaller than their corner pieces.
+- Tabs did not resize when their text was updated.
+- Deprecated fields no longer trip assert failures on load.
+
+### Removed
+
+- The Gui Editor toy, replaced by the full Gui Editor.
+- `GuiInspectorTypeColor`, split into `GuiInspectorTypeFluidColorI` and `GuiInspectorTypeGuiCursor`.
+
+## [4.0-ea2.1] - 2023-05-02
+
+A maintenance release with two new asset features.
+
+### Added
+
+- Layered image assets: several textures composed onto an existing image asset and used as one, with an editor for building them.
+- A `ScreenFade` library module for switching canvas contents by fading out and in, also usable as a backdrop for popup dialogs.
+- `RandomNumberGenerator`, a PCG-based random number object.
+- `GuiSpriteCtrl` gained `ClampImage` (with `getClampImage()` / `setClampImage()`), which decides whether an oversized image is pinned to the top-left or centered and cropped evenly.
+
+### Changed
+
+- Visual Studio 2022 replaced Visual Studio 2017 as a supported toolchain.
+- The Linux build moved to `-std=c++17`.
+- A drop down always renders its down arrow.
+
+### Fixed
+
+- The modulus operator gave different results on different hardware for negative numbers, because a signed float was converted straight to an unsigned integer.
+- Relative positioning did not work correctly with a minimum extent and scroll bars.
+- The expand control now re-centers as it expands, fires `onResized` each iteration, and handles a child being resized while expanded.
+- White artifacts around the edge of images using bilinear blending and layered images.
+- Explicit cell mode could not be turned off through the editor, because it refused to remove the last cell.
+- A `GuiSpriteCtrl` rendered its children incorrectly, and its image can now overflow the control's content area.
+- A batch of null dereferences, unfreed memory, unclosed handles and out-of-bounds indices across the engine.
+
+## [4.0-ea2] - 2022-06-27
+
+The release that added the Project Manager and the module library.
+
+### Breaking
+
+- `GuiMouseEventCtrl` was removed; its callbacks are on the base `GuiControl`. `onMouseWheelUp` and `onMouseWheelDown` now return void, since nothing used the return value. A script callback on `GuiControl` can return true to consume an event and stop it bubbling.
+- `GuiScriptNotifyCtrl`, `GuiFilterCtrl` and `GuiRolloutCtrl` were removed. The first is covered by optional callbacks on `GuiControl`; the second was an ancient way to plot a graph.
+- `GuiMLTextCtrl` and `GuiMLTextEditCtrl` were removed, and the `StripMLControlChars()` global went with them. A `GuiControl` carries wrapped text and a `GuiTextEditCtrl` with text wrap on edits it.
+- The `modal` field became `UseInput` and moved onto `GuiControl`, with input events now bubbling up through the control tree rather than being handled at one level. `UseInput` should generally not be needed.
+- Mouse enter and leave no longer pair up: entering a child no longer leaves its parent, so you may get a dozen enter events walking down to a child and the corresponding leaves only as the pointer leaves the stack.
+- `GuiTextEditCtrl` was rewritten around `std::string` and supports multi-line editing by turning on text wrap. UTF16 strings are no longer supported in a text box, double-clicking a word selects that word rather than everything, and a text box changes on hover using its highlight state. Two profile colors were added for selected text.
+- Each asset kind now has its own file extension, and a module's `` glob must match it.
+
+### Added
+
+- The Project Manager: a startup screen for picking or creating a project, a module list with install and update, dependency syncing, a new-module dialog with library templates, editing of a module's declared-asset folders and basic data, and a launch-on-startup flag.
+- Library modules a project can import: an `AppCore` with a starter profile file, an `Audio` module handling music on channel 0 and effects on channel 1, an Art Pack template, and a `BlankGame` template.
+- `NoiseGenerator`, a Perlin noise object, and a Noise Toy demonstrating it.
+- `FontSizeAdjust` on any `GuiControl`: a multiplier on the profile's font size, so one profile serves every size.
+- A font color override (`FontColor` plus `OverrideFontColor`) and an alignment override, both on the control, so a minor adjustment no longer needs its own profile.
+- `GuiControl.textExtend`, which sizes a control to its text.
+- Explicit cell support in the Asset Manager: cells can be added, removed, edited and reordered in the UI, and image assets can be grouped.
+- The particle editor shows variation in the base graph.
+- `ModuleManager` gained functions for getting a module definition's path and clearing a module database, and its copy function takes a source module.
+- `GuiTextEditCtrl` gained a `ReturnCommand`, and `GuiControl` gained keyboard-event callbacks.
+- `SimSet.callOnChildrenNoRecurse()` was restored.
+- 64-bit Linux builds.
+
+### Fixed
+
+- `ImageAsset.removeExplicitCell()` never worked; `ImageAsset.getExplicitCellIndex()` now returns -1 when there is no such cell.
+- Only visible controls have `render` and `preRender` called.
+- Controls drifted out of position when relative positioning was on and the window was resized a few times.
+- The console no longer runs its contents when the console loses focus, which typically happens as it closes.
+- Windows and tab pages tried to pass events back down, which became an infinite loop once events bubbled up.
+
+## [4.0-ea1] - 2021-08-10
+
+The first Early Access release: a reworked GUI system, the first editor, and the
+content layout 4.0 uses.
+
+### Breaking
+
+- The content layout changed. The `modules/` folder was replaced by `toybox/` for the example toys, `library/` for reusable importable modules, and `editor/` for the in-engine tools.
+- Borders were reworked around a new `GuiBorderProfile` object that can be applied to one or all of a control's sides, with margin and padding so profiles follow the CSS box model. `borderDefault`, `borderTop`, `borderBottom`, `borderLeft` and `borderRight` on a `GuiControlProfile` now name border profiles, and the old flat border settings — `borderThickness`, `borderColor`, `borderColorHL`, `borderColorNA`, `bevelColorHL` and `bevelColorLL` — are gone.
+- `GuiControlProfile` lost several more fields with no direct successor: `opaque`, `modal`, `numbersOnly`, `returnTab`, `autoSizeWidth`, `autoSizeHeight` and `fontColorSEL`. `justify` was replaced by the separate `align` and `vAlign`, and `fillColorSL`, `fontColorSL`, `fontDirectory`, `category` and `useInput` were added.
+- Buttons went from ten classes to four, and many GUI callbacks were renamed from `onMouse...` to `onTouch...`.
+- Thirty-two console classes were removed, effectively the whole legacy control set. `GuiTextCtrl` went because every `GuiControl` now carries text; `GuiBitmapCtrl` and `GuiFadeinBitmapCtrl` were replaced by an upgraded `GuiSpriteCtrl`; `GuiPopUpMenuCtrl` and `GuiPopUpMenuCtrlEx` by the new `GuiDropDownCtrl`; `GuiMenuBar` and `GuiFormCtrl` by the new `GuiMenuBarCtrl` and `GuiMenuItemCtrl`; `GuiPaneControl` by `GuiExpandCtrl` and `GuiPanelCtrl`; `GuiStackControl` by `GuiChainCtrl`; and `GuiControlArrayControl`, `GuiDynamicCtrlArrayControl` and `GuiGridControl` by the rewritten `GuiGridCtrl`. Also gone: `GuiArrayCtrl` (now an abstract base requiring `renderCell`, with its unused row and column headers dropped), `GuiTickCtrl` (every `GuiControl` can animate and processes ticks only if it asks to), `GuiBitmapButtonCtrl`, `GuiBitmapButtonTextCtrl`, `GuiBitmapBorderCtrl`, `GuiBorderButtonCtrl`, `GuiButtonBaseCtrl`, `GuiIconButtonCtrl`, `GuiToolboxButtonCtrl`, `GuiAutoScrollCtrl`, `GuiBackgroundCtrl`, `GuiBubbleTextCtrl`, `GuiConsoleTextCtrl`, `GuiTextListCtrl`, `GuiControlListPopUp`, `GuiSeparatorCtrl` and `GuiImageList`.
+- `GuiDragAndDropControl` was renamed `GuiDragAndDropCtrl`.
+- `SceneWindow.getWindowExtents()` is now `SceneWindow.getWindowArea()`, the one break outside the GUI.
+- `GuiListBoxCtrl.setMultipleSelection()` is now `setMultiSelection()`, with a matching `getMultiSelection()`. `GuiScrollCtrl.getUseScrollEvents()` and `setUseScrollEvents()` were removed with no replacement.
+- Spine support was removed: `SkeletonObject` and `SkeletonAsset` are gone, along with every method on them. A branch off master keeps the Spine code.
+- Physics particles were removed from the emitter, pending a dedicated emitter.
+- `GuiTextEditCtrl`'s `validate` command was removed, along with a handful of long-obsolete features.
+
+### Added
+
+- The Asset Manager: view, create and delete image, animation, font, particle and audio assets, with changes reflected live in the preview above, buttons to create, reorder and delete particle emitters, and support for images whose frames are explicitly defined.
+- A graph control for editing particle effect fields, which is what makes particle effects authorable at all.
+- Any control can render itself from an `ImageAsset`, a bitmap, or the default drawing, chosen by its profile — with either one frame per state or nine frames per state, for both bitmaps and image assets.
+- Themes, including switching themes dynamically, and a Torque 4.0 theme and icon.
+- `GuiChainCtrl`, which orders its children in a single line while letting each keep its own size.
+- `GuiDropDownCtrl`, combining the button and list box with all their features, plus item IDs, per-item active state, sorting by text or id, and arrow, enter and delete key handling shared with `GuiListBoxCtrl`.
+- `GuiExpandCtrl` and `GuiPanelCtrl` replaced the old pane control, and button color changes gained easing.
+- `GuiMenuBarCtrl`, a new menu bar.
+- Text wrapping on any GUI control, with `getTextWrap` and `setTextWrap`.
+- `SceneWindow` scroll bars, so the camera can be moved with the mouse wheel and bars — used by the Asset Admin for zooming and panning around an asset.
+- `mEase()`, which eases a value from a starting point to an ending point.
+- `Array`, Daniel Neilsen's array object, with push, pop, move, sort and insert.
+- `AssetDatabase.PreloadAsset()`, which loads an asset into memory before it is used.
+- The cursor hides itself when a touch screen is used and comes back when the mouse is, controlled by `$pref::Gui::hideCursorWhenTouchEventDetected`.
+- `GuiTabBookCtrl.getSelectedPage()`, and stock color names on `GuiSpriteCtrl`.
+- `Hidden` and `Locked` flags on `SimObject` for the editors, written only when true, and a `TypeName` field.
+- `GuiWindowCtrl` was brought up to date with callbacks, multiple profiles and GUI cursors for resizing; `GuiListBoxCtrl` items apply borders and background; `GuiProgressCtrl` animates smoothly.
+- `GuiTextEditCtrl` gained an `InputMode` that restricts input to plain text, a number or a decimal value.
+- The inspector gained tooltips, adjustable profiles, a grid layout, and a hidden-field list so an editor can keep a field out of reach.
+- 64-bit Windows builds, and Android changes required by the Play Store along with a working OpenAL implementation.
+
+### Fixed
+
+- Field validators did not fire in most cases, and `setField` was broken.
+- A range of GUI clipping problems, and an extent that shrank below `minExtent` was not restored correctly when there was room again.
+- Tabs were not correctly removed from a tab book, and `GuiRadioCtrl` could crash.
+- `ImageAsset`'s filter mode.
+- The toybox, which several rounds of GUI changes had broken to the point of being unusable.
+
+### Removed
+
+- The Spine and Leap toys.
+
+---
+
+Releases before 4.0 (3.1 through 3.5) are not covered here; see the git log for
+that history.
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 000000000..306c11128
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,118 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Overview
+
+Torque2D 4.0 ("Rocket Edition", Early Access) is a cross-platform 2D game engine. The C++ engine lives in `engine/source`; games are written in **TorqueScript** (`.cs` files) and structured as **modules**. The same engine binary runs the in-engine editors (Project Manager, Asset Manager, GUI Editor) and any game built on top of it.
+
+**When writing or refactoring TorqueScript, follow the conventions in [`TORQUE_SCRIPT.md`](TORQUE_SCRIPT.md)** — the prescriptive style guide for script code (one class per file, `onAdd`/`onRemove` lifecycle, ownership/teardown chains, `class`/`superclass` inheritance).
+
+Target platforms: Windows, macOS, Linux, iOS, Android, and Web (Emscripten).
+
+## Building
+
+**CMake is the single source of truth.** The engine is built from the root
+`CMakeLists.txt`; you generate a project for your platform/toolchain and build it.
+The executable is dropped at the **repository root** (`Torque2D.exe` /
+`Torque2D_DEBUG.exe` on Windows).
+
+- **Configure + build:** e.g. `cmake -S . -B build -G "Visual Studio 17 2022" -A x64` then `cmake --build build --config Debug` (also `Release`/`Shipping`). Single-config generators (Make/Ninja) use `-DCMAKE_BUILD_TYPE=` instead of `--config`. Convenience generator scripts live at the repo root (`generate-vs2022.bat`, `generate-vs2026.bat`, `generate-xcode.command`, `generate-make.sh`, `build-linux.sh`).
+- **Per-platform recipes & status** (configure flags for macOS/iOS/Linux 32-bit/Android, runtime-verification state) are documented in `cmake/BUILD-PLATFORM-NOTES.md`.
+- Engine sources are listed **explicitly** in `cmake/EngineSources.cmake` (cross-platform) and `cmake/PlatformSources.cmake` (per-platform back-ends: Windows, macOS, Linux, iOS, Android wired; Emscripten stubbed) — these are the authoritative file lists, **not** globs. (All six back-ends — Windows, macOS, Linux, iOS, Android, and Emscripten — are wired and runtime-verified.)
+- Third-party libs (libogg, libvorbis, lpng, ljpeg, zlib) are built as static targets from `engine/lib/CMakeLists.txt`; GoogleTest is built via `add_subdirectory` and linked for the in-engine unit tests (desktop only).
+- **Windows specifics that are load-bearing:** static non-debug runtime `/MT` for all configs (avoids `_DEBUG`, which would make tinyXML `#define DEBUG` and break Box2D), `/Zc:wchar_t-` (so `wchar_t` == the engine's `UTF16`), C++17, and `_HAS_STD_BYTE=0`.
+
+The only remaining item under `engine/compilers/` is **not** a standalone build
+system: `android-studio` is the Android app shell whose Gradle native step *invokes*
+the root CMake via the NDK. The legacy hand-maintained projects — the VS solutions,
+the macOS and iOS Xcode projects, the Linux Makefiles, and the Emscripten reference
+recipe — have all been **retired** (the Web target is now CMake-runtime-verified);
+CMake replaces them.
+
+The built executable must run from the repo root because it loads `main.cs` and the script/asset trees (`editor/`, `library/`, `toybox/`, `tools/`) relative to the working directory.
+
+## Running
+
+The engine's entry point is the **`main.cs`** script next to the executable. On launch it calls `setCompanyAndProduct(...)` then `exec("./editor/main.cs")`, which starts the Project Manager UI. To boot directly into a game instead, scan and load a module (see the commented `ModuleDatabase.scanModules` / `ModuleDatabase.LoadExplicit` lines in `main.cs`).
+
+The in-engine **console** (and the editor tabs: Asset Manager, Project Manager, GUI Editor) is opened with **Ctrl + Tilde (~)**.
+
+## Tests
+
+There are two suites, and they test different things.
+
+### C++ unit tests (GoogleTest)
+
+Vendored at `engine/source/testing/googleTest`. **Every test lives in `engine/source/testing/tests/`** (e.g. `guiTreeRowLayoutTests.cc`, `platformStringTests.cc`); each file is listed explicitly in `cmake/EngineSources.cmake`, so a new one needs a CMake edit and a re-configure to be compiled at all.
+
+```
+tests\run-unit.ps1 all of them
+tests\run-unit.ps1 GuiTreeRowLayoutTests.* one suite (a GoogleTest filter)
+```
+
+- Under the hood that launches the engine with the alternate boot script `main.runAllUnitTests.cs`, which calls `runAllUnitTests()` and quits. You can also invoke `runAllUnitTests()` from the in-engine console.
+- **`runAllUnitTests()` takes no arguments.** It hands `InitGoogleTest` an empty argv, so a subset is selected with the `GTEST_FILTER` environment variable — which is what `run-unit.ps1`'s parameter sets.
+- **What a unit test can reach.** The engine boots far enough to give it `Con`, `Sim`, the string table, the resource manager and `GuiDefaultProfile`, so it can `new` and `registerObject()` a control, read and write fields, run script via `Con::evaluate`, and round-trip TAML. It has **no canvas and no GL context**, so it must never wake a control or measure text: a font registers a texture and `TextureManager::refresh` asserts — which in a debug build is a modal box, so the failure arrives as a *hang*. Note this rules out adding rows to a list box or tree, since that calls `updateSize()` → `getFont()`.
+- The established move when GUI logic is worth testing is to extract the arithmetic into a `static` that takes everything it uses, then test the static — see `GuiScrollCtrl::subtractScrollBars`, `GuiControl::splitParagraphs`, `GuiTreeViewCtrl::resolveIndent`.
+- Tests are compiled out of shipping builds (`TORQUE_SHIPPING` guards `unitTesting.h`).
+
+### TorqueScript integration tests
+
+`tests/` drives the real engine — a real canvas, the real editor, real posted mouse and keyboard input — and checks that it behaves. This is what covers the editors, which the unit tests do not reach. It is also much slower: one process per suite with a 90 second timeout each, against seconds for the whole unit run. **Prefer a unit test where the thing under test can be reached without a canvas**, and keep these for what genuinely needs one.
+
+```
+tests\run.ps1 every pass/fail suite (exits non-zero on a change)
+tests\run.ps1 colorPopup one of them
+tests\run.ps1 -Shots the screenshot harnesses instead
+```
+
+**Read `tests/README.md` before writing one.** In particular: a relative path is expanded against the calling *script*, not the working directory, so tests use the `testRoot()` / `testExec()` helpers from `tests/lib/prelude.cs` for every path they name. Known-failing suites are recorded in `run.ps1` so a real regression stands out.
+
+## Architecture
+
+### Engine ↔ Script boundary (the most important pattern)
+Almost every C++ class is exposed to TorqueScript. The conventions:
+
+- A C++ class derives (transitively) from `SimObject` and uses `DECLARE_CONOBJECT(ClassName)` in its header and `IMPLEMENT_CONOBJECT(ClassName)` in its `.cc`. This registers it with the **console type system** so script can instantiate it by name (e.g. `new Sprite()`).
+- Script-visible **fields** are registered in a static `initPersistFields()` override (these are also what TAML serializes).
+- Script-callable **methods/functions** are defined in companion **`*_ScriptBinding.h`** files (≈140 of them) using the `ConsoleMethod` / `ConsoleFunction` / `...WithDocs` macros. These headers are `#include`d into the matching `.cc`. The doc comments in them generate the scripting reference. **When adding a script API, edit the class's `_ScriptBinding.h`, not the `.cc` directly.**
+
+`engine/source/console/` is the TorqueScript implementation itself: lexer/parser (`CMDscan.l`, `CMDgram.y` → generated `CMDscan.cc`, `cmdgram.cc`), AST (`astNodes.cc`), compiler (`compiler.cc`, `codeBlock.cc`), and the bytecode VM (`compiledEval.cc`). Scripts compile to **`.dso`** files (gitignored); `$Scripts::ignoreDSOs` in `main.cs` controls whether compiled scripts are reused.
+
+### Object & lifecycle core
+`engine/source/sim/` is the runtime object model: `SimObject` (base), `SimSet`/`SimGroup` (containers), `SimManager` (registry, id/name lookup, event scheduling), `SimDatablock`, and script-defined objects (`ScriptObject`, `ScriptGroup`). The global `Sim` namespace owns object IDs and the event queue.
+
+### Modules & Assets (TAML)
+Games are composed of **modules**, each defined by a `module.taml` (`engine/source/module/`, `ModuleManager`/`ModuleDefinition`). A module declares a script file, create/destroy functions, dependencies, and `` globs. The **Asset system** (`engine/source/assets/`, `AssetManager`/`AssetDatabase`) loads `*.asset.taml` files (images, animations, fonts, sounds, particles) referenced by AssetId.
+
+**TAML** (`engine/source/persistence/taml/`) is the object-serialization layer underpinning all of this — any `SimObject`'s persistent fields can be written/read in **XML, JSON, or binary** form. This is how editors save scenes/assets and how `.taml` files are loaded at runtime.
+
+### 2D game framework (`engine/source/2d/`)
+- `2d/scene/Scene.cc` — the world container; wraps a **Box2D** physics world (`engine/source/Box2D/`), manages SceneObjects, contacts, and the render pipeline (`SceneRenderQueue`, `SceneRenderState`).
+- `2d/sceneobject/` — `SceneObject` (base renderable/physical body) and concrete types: `Sprite`, `CompositeSprite`, `ParticlePlayer`, `LightObject`, `Trigger`, skeleton/spine objects, etc.
+- `2d/core/` — rendering and math support: `BatchRender` (batched OpenGL sprite rendering), `SpriteBatch`, `ParticleSystem`, `Vector2`, `ImageFrameProvider`.
+- `2d/controllers/` — scene controllers (forces, e.g. point/uniform/buoyancy).
+- `2d/assets/` — 2D-specific assets (ImageAsset, AnimationAsset, ParticleAsset, etc.).
+
+### Other subsystems
+- `graphics/` — `dgl` (OpenGL wrapper), texture management (`TextureManager`/`TextureHandle`), bitmap codecs (png/jpeg/bmp/pvr), fonts (`gFont`).
+- `audio/` — OpenAL-based audio, Vorbis/WAV streaming, `AudioAsset`.
+- `gui/` — the GUI control hierarchy (`GuiControl` and subclasses in `buttons/`, `containers/`, `editor/`). The 4.0 GUI Editor is implemented in script under `editor/GuiEditor/`.
+- `platform/` + `platformWin32/`, `platformOSX/`, `platformX86UNIX/`, `platformiOS/`, `platformAndroid/`, `platformEmscripten/` — `platform/platform.h` declares the cross-platform abstraction (windowing, input, threads, file IO, networking); each `platformXXX/` provides the concrete implementation. CMake selects the right one per target.
+
+### Script & content layout (repo root)
+- `editor/` — in-engine tools (Project Manager, Asset Admin, GUI Editor, Editor Console) written in TorqueScript.
+- `library/` — reusable importable modules (`AppCore`, `Audio`, `ArtPack`, …); `AppCore` provides per-project bootstrap.
+- `toybox/` — 30+ example "toy" modules demonstrating engine features (good reference for script-side APIs).
+- `tools/` — non-engine tooling (TexturePacker, Zwoptex, doxygen config, CMake modules, VS debugger visualizers).
+
+## Conventions
+
+- **TorqueScript game/UI code follows [`TORQUE_SCRIPT.md`](TORQUE_SCRIPT.md)** (repo root): one class per file named for the class, self-configuring objects via `onAdd`/`onRemove`, each object owning and deleting what it creates, and `class`/`superclass` inheritance with the `init()` pattern. Read it before touching any `.cs` game code. To verify changed scripts against these rules, use the **`checking-torquescript-conventions`** skill (`.claude/skills/`).
+- Header include guards use the `_NAME_H_` convention and are wrapped in `#ifndef` checks at every include site (see `platform.h`) — follow this when adding headers.
+- New engine source files must be added explicitly to `cmake/EngineSources.cmake` (cross-platform) or `cmake/PlatformSources.cmake` (platform-specific back-ends) to be compiled. These CMake lists are the single source of truth — regenerate your project from them; there are no hand-maintained `.vcxproj`/Xcode/Makefile lists to keep in sync anymore.
+- All pull requests target the **`development`** branch, not `master` (master is the stable release branch).
+- **A user-visible change needs a `CHANGELOG.md` entry in the same commit or PR** — new behavior, a changed or removed API, a fixed bug. Entries go under the unreleased section, written for someone building a game on the engine rather than for someone working on it. Written afterwards, they do not get written: the previous changelog lived on the wiki, was reconstructed from `git log` after each merge, and stopped in 2013.
+- **`CONTRIBUTING.md`** (how to submit a change, and the C++/script standards) and **`CODE_OF_CONDUCT.md`** are at the repo root. There is no contributor agreement to sign; a contribution must simply be legally yours to give and MIT-compatible. Governance follows Torque3D's model — see `CONTRIBUTING.md` rather than looking for a steering committee charter, which was retired.
+- **Documentation lives in the sibling `Torque2D.wiki` repo**, which is separate and has its own conventions — every root `.md` there is a published page. Engine changes that alter a script API or an editor workflow usually need a wiki change too.
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 10c06779c..408507c15 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,9 +1,632 @@
-cmake_minimum_required (VERSION 3.6.0)
+# -----------------------------------------------------------------------------
+# Torque2D — root CMake build
+#
+# This is the authoritative, modern (target-based) build for the engine. Engine
+# sources are listed EXPLICITLY (see cmake/EngineSources.cmake and
+# cmake/PlatformSources.cmake) rather than globbed, so that CMake — not the
+# filesystem or a hand-maintained .sln — is the single source of truth. Add a
+# source file there once, then regenerate the per-platform project files.
+#
+# Windows (VS), macOS (arm64), Linux (32/64-bit), iOS (arm64 simulator), Android,
+# and Emscripten (Web/WASM) are all wired. See cmake/PlatformSources.cmake and
+# cmake/BUILD-PLATFORM-NOTES.md for per-platform status and recipes.
+# -----------------------------------------------------------------------------
+cmake_minimum_required(VERSION 3.21)
-set(TORQUE_APP_NAME "Torque2D")
+# CMP0091: select the MSVC runtime via CMAKE_MSVC_RUNTIME_LIBRARY (abstraction)
+# rather than editing CMAKE_CXX_FLAGS by hand.
+cmake_policy(SET CMP0091 NEW)
-project(${TORQUE_APP_NAME}
-VERSION 4.0.0.0
+project(Torque2D VERSION 4.0.0 LANGUAGES CXX C)
+
+# ----------------------------------------------------------------------------
+# Build configurations — mirror the historical VS solution: Debug/Release/Shipping
+# ----------------------------------------------------------------------------
+get_property(_isMultiConfig GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
+if(_isMultiConfig)
+ set(CMAKE_CONFIGURATION_TYPES "Debug;Release;Shipping" CACHE STRING "" FORCE)
+else()
+ if(NOT CMAKE_BUILD_TYPE)
+ set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "" FORCE)
+ endif()
+ set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug;Release;Shipping")
+endif()
+
+# Shipping is an optimized build: inherit Release's compiler/linker flags.
+foreach(_flagVar
+ CMAKE_C_FLAGS CMAKE_CXX_FLAGS
+ CMAKE_EXE_LINKER_FLAGS CMAKE_SHARED_LINKER_FLAGS CMAKE_STATIC_LINKER_FLAGS)
+ set(${_flagVar}_SHIPPING "${${_flagVar}_RELEASE}" CACHE STRING "" FORCE)
+endforeach()
+
+# Static, NON-debug CRT (/MT) for every config — including Debug — matching the
+# maintained VS solution (all 6 configs use MultiThreaded). This is load-bearing:
+# the debug CRT would define _DEBUG, which makes tinyXML (persistence/tinyXML/
+# tinyxml.h) do `#define DEBUG` (empty), which in turn breaks Box2D's
+# `#if DEBUG && ...` (b2Settings.h) with C1017. Must be set before any target
+# (libs, gtest, engine) is created.
+set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded")
+
+# Treat wchar_t as a non-built-in type (== unsigned short), matching the VS
+# solution (TreatWChar_tAsBuiltInType=false). The engine's UTF16 type is
+# `unsigned short`, and the Win32 W-APIs take wchar_t*; this makes them
+# compatible. Applied globally (before any target) so wchar_t has a consistent
+# ABI across the engine, the third-party libs, and gtest.
+if(MSVC)
+ add_compile_options(/Zc:wchar_t-)
+endif()
+
+# ----------------------------------------------------------------------------
+# Paths / output — the executable lands at the repo root next to main.cs so it
+# can find the script + asset trees (editor/, library/, toybox/, tools/).
+# ----------------------------------------------------------------------------
+set(TORQUE_SRC "${CMAKE_SOURCE_DIR}/engine/source")
+set(TORQUE_LIB "${CMAKE_SOURCE_DIR}/engine/lib")
+
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}")
+foreach(_cfg Debug Release Shipping)
+ string(TOUPPER "${_cfg}" _CFG)
+ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_${_CFG} "${CMAKE_SOURCE_DIR}")
+endforeach()
+
+# Emscripten: define EMSCRIPTEN=1 GLOBALLY (before any target). emcc only
+# predefines __EMSCRIPTEN__, but the engine (platform/types.gcc.h) AND several
+# vendored libs (e.g. ljpeg/jconfig.h's platform dispatcher) key off bare
+# EMSCRIPTEN — without it ljpeg fails with "No jconfig.h was included". (The
+# retired hand-maintained Emscripten recipe set this the same way, globally.)
+if(EMSCRIPTEN)
+ add_compile_definitions(EMSCRIPTEN=1)
+endif()
+
+# ----------------------------------------------------------------------------
+# Third-party static libraries (libogg, libvorbis, lpng, ljpeg, zlib)
+# ----------------------------------------------------------------------------
+add_subdirectory(engine/lib)
+
+# ----------------------------------------------------------------------------
+# GoogleTest — build the vendored distribution and link gtest into the engine so
+# the in-engine unit tests (runAllUnitTests) actually have a framework to run.
+# Not built on Android or Emscripten (no on-device/in-browser test framework; the
+# testing/* sources are also excluded from those builds below).
+# ----------------------------------------------------------------------------
+if(NOT ANDROID AND NOT EMSCRIPTEN)
+ set(gtest_force_shared_crt OFF CACHE BOOL "" FORCE) # use the static CRT we selected
+ set(BUILD_GMOCK OFF CACHE BOOL "" FORCE)
+ set(INSTALL_GTEST OFF CACHE BOOL "" FORCE)
+ # This is the inner googletest library dir (no monorepo root above it), so the
+ # version normally provided by the distribution's parent must be supplied here.
+ set(GOOGLETEST_VERSION 1.17.0)
+ add_subdirectory(engine/source/testing/googleTest)
+endif()
+
+# ----------------------------------------------------------------------------
+# Engine target
+# ----------------------------------------------------------------------------
+include(cmake/EngineSources.cmake) # -> TORQUE_ENGINE_SOURCES
+include(cmake/PlatformSources.cmake) # -> TORQUE_PLATFORM_SOURCES_{WINDOWS,MACOS,LINUX}
+
+# iOS must be detected explicitly: APPLE is also true on iOS, so it has to be
+# distinguished from the macOS desktop build (set via -DCMAKE_SYSTEM_NAME=iOS).
+if(CMAKE_SYSTEM_NAME STREQUAL "iOS")
+ set(TORQUE_IOS TRUE)
+endif()
+
+# Select the active platform's OS-specific back-end sources. Note ANDROID and
+# EMSCRIPTEN are also UNIX, so they must be matched before the UNIX (Linux) branch.
+if(WIN32)
+ set(TORQUE_PLATFORM_SOURCES ${TORQUE_PLATFORM_SOURCES_WINDOWS})
+elseif(ANDROID)
+ set(TORQUE_PLATFORM_SOURCES ${TORQUE_PLATFORM_SOURCES_ANDROID})
+elseif(EMSCRIPTEN)
+ set(TORQUE_PLATFORM_SOURCES ${TORQUE_PLATFORM_SOURCES_EMSCRIPTEN})
+elseif(TORQUE_IOS)
+ set(TORQUE_PLATFORM_SOURCES ${TORQUE_PLATFORM_SOURCES_IOS})
+elseif(APPLE)
+ set(TORQUE_PLATFORM_SOURCES ${TORQUE_PLATFORM_SOURCES_MACOS})
+elseif(UNIX)
+ set(TORQUE_PLATFORM_SOURCES ${TORQUE_PLATFORM_SOURCES_LINUX})
+else()
+ message(FATAL_ERROR "Unsupported platform: no PlatformSources list for this OS.")
+endif()
+
+# The in-engine unit tests need gtest, which we don't build on Android/Emscripten.
+if(ANDROID OR EMSCRIPTEN)
+ list(FILTER TORQUE_ENGINE_SOURCES EXCLUDE REGEX "/testing/")
+endif()
+
+# Emscripten uses a dedicated networking back-end (platformNet_Emscripten.cpp, all
+# stubs — browsers can't open raw sockets) INSTEAD of the desktop BSD-socket
+# implementation. Swap it into the engine source list (the platform back-end list
+# in PlatformSources.cmake deliberately omits it). platformNet_ScriptBinding.cc
+# (the script API) is shared and stays.
+if(EMSCRIPTEN)
+ list(FILTER TORQUE_ENGINE_SOURCES EXCLUDE REGEX "platformNet(Async)?\\.cpp$")
+ list(APPEND TORQUE_ENGINE_SOURCES ${TORQUE_SRC}/platform/platformNet_Emscripten.cpp)
+endif()
+
+# Android builds a shared library (libtorque2d.so) loaded by the NativeActivity;
+# every other platform builds an executable. (WIN32 only affects Windows.)
+if(ANDROID)
+ add_library(Torque2D SHARED)
+else()
+ add_executable(Torque2D WIN32)
+endif()
+target_sources(Torque2D PRIVATE ${TORQUE_ENGINE_SOURCES} ${TORQUE_PLATFORM_SOURCES})
+
+# C++17 across the engine (replaces the old -std=c++11 / unset MSVC default).
+target_compile_features(Torque2D PRIVATE cxx_std_17)
+
+target_include_directories(Torque2D PRIVATE
+ ${TORQUE_SRC}
+ ${TORQUE_LIB}
+ ${TORQUE_SRC}/persistence/rapidjson
+ ${TORQUE_SRC}/persistence/rapidjson/include
+)
+if(WIN32)
+ # OpenAL headers (OpenAL itself is loaded dynamically at runtime — not linked).
+ target_include_directories(Torque2D PRIVATE ${TORQUE_LIB}/openal/win32)
+
+ # Stage the architecture-matching OpenAL Soft runtime next to the executable.
+ # winOpenAL.cc loads OpenAL via LoadLibrary("OpenAL32.dll") from the app
+ # directory, so the DLL must sit beside Torque2D.exe. A 64-bit process cannot
+ # load a 32-bit DLL (and vice versa), so pick by the target's pointer size.
+ # The staged root-level OpenAL32.dll is a build artifact (git-ignored), same
+ # as Torque2D.exe. Sources live in engine/lib/openal/{win32,win64}.
+ if(CMAKE_SIZEOF_VOID_P EQUAL 8)
+ set(TORQUE_OPENAL_DLL "${TORQUE_LIB}/openal/win64/OpenAL32.dll")
+ else()
+ set(TORQUE_OPENAL_DLL "${TORQUE_LIB}/openal/win32/OpenAL32.dll")
+ endif()
+ add_custom_command(TARGET Torque2D POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy_if_different
+ "${TORQUE_OPENAL_DLL}" "$/OpenAL32.dll"
+ VERBATIM
+ COMMENT "Staging OpenAL Soft runtime (OpenAL32.dll) next to Torque2D")
+endif()
+if(ANDROID)
+ # platformAndroid vendors android_native_app_glue.h (included as <...> by
+ # T2DActivity.h); matches the old Android.mk LOCAL_C_INCLUDES.
+ target_include_directories(Torque2D PRIVATE ${TORQUE_SRC}/platformAndroid)
+endif()
+
+# Preprocessor definitions.
+# * The spurious unconditional DEBUG=1 from the old CMake is intentionally dropped
+# (the engine keys off TORQUE_DEBUG; MSVC defines _DEBUG for debug runtimes).
+target_compile_definitions(Torque2D PRIVATE
+ $<$:TORQUE_DEBUG>
+ $<$:TORQUE_SHIPPING>
)
+if(ANDROID)
+ # From the ndk-build recipe. __ANDROID__ is defined by the NDK toolchain.
+ # HAVE_NEON applies to arm64-v8a (the only ABI we target for now).
+ target_compile_definitions(Torque2D PRIVATE
+ TORQUE_OS_ANDROID GL_GLEXT_PROTOTYPES ENABLE_CONSOLE_MSGS HAVE_NEON=1)
+ target_compile_options(Torque2D PRIVATE -fsigned-char)
+else()
+ target_compile_definitions(Torque2D PRIVATE
+ UNICODE _UNICODE TORQUE_UNICODE TORQUE_DEBUG_GUARD TORQUE_NET_STATS)
+endif()
+if(MSVC)
+ # _HAS_STD_BYTE=0 is REQUIRED under C++17/MSVC (std::byte vs the engine's byte).
+ target_compile_definitions(Torque2D PRIVATE
+ _CRT_SECURE_NO_WARNINGS _CRT_SECURE_NO_DEPRECATE _HAS_STD_BYTE=0)
+endif()
+if(UNIX AND NOT APPLE AND NOT ANDROID AND NOT EMSCRIPTEN)
+ target_compile_definitions(Torque2D PRIVATE LINUX)
+endif()
+
+if(MSVC)
+ target_compile_options(Torque2D PRIVATE
+ /MP # parallel compilation
+ /EHsc # standard C++ exception model
+ /bigobj # large TUs (compiledEval.cc etc.)
+ /wd4800 /wd4100 /wd4127 /wd4512 # noisy warnings (matches old build)
+ )
+else()
+ # The engine uses the (C++17-removed) `register` keyword in ~35 files. GCC
+ # tolerates it; Clang (Android NDK, and macOS/iOS) makes -Wregister an error.
+ target_compile_options(Torque2D PRIVATE -Wno-register)
+endif()
+
+# Link: third-party libs built from source (transitively bring their includes).
+target_link_libraries(Torque2D PRIVATE libogg libvorbis lpng ljpeg)
+if(NOT ANDROID)
+ # zlib is built from source everywhere except Android (which uses the NDK's
+ # system libz). Emscripten compiles the vendored zlib to wasm just fine.
+ target_link_libraries(Torque2D PRIVATE zlib)
+endif()
+if(NOT ANDROID AND NOT EMSCRIPTEN)
+ # gtest provides the in-engine unit-test framework (desktop only).
+ target_link_libraries(Torque2D PRIVATE gtest)
+endif()
+
+if(WIN32)
+ # Windows system libraries (matches the VS solution's AdditionalDependencies).
+ # OpenGL is linked via #pragma comment in the platform sources, as before.
+ target_link_libraries(Torque2D PRIVATE
+ comctl32 comdlg32 user32 advapi32 gdi32 rpcrt4 winmm
+ ws2_32 vfw32 imm32 shell32 shlwapi ole32
+ )
+endif()
+
+if(APPLE AND NOT TORQUE_IOS)
+ # Apple Silicon (arm64) build. The legacy Xcode project hard-coded x86_64;
+ # we target arm64 natively. Set explicitly so the architecture is reproducible
+ # regardless of the host (and overridable via -DCMAKE_OSX_ARCHITECTURES for a
+ # universal/Intel build).
+ if(NOT CMAKE_OSX_ARCHITECTURES)
+ set(CMAKE_OSX_ARCHITECTURES "arm64" CACHE STRING "macOS build architecture" FORCE)
+ endif()
+
+ # Minimum macOS. Without this the binary inherits the build host's SDK default
+ # (e.g. 14.6), needlessly excluding older Macs. 11.0 (Big Sur) is the floor for
+ # arm64 and is the lowest that still lets users on older systems run the game.
+ # It does NOT constrain a future Metal renderer: Metal/MetalKit have shipped
+ # since 10.11, so 11.0 fully supports Metal (only the Metal 3 feature set would
+ # need 13.0+, which a 2D engine won't require). Overridable on the command line.
+ if(NOT CMAKE_OSX_DEPLOYMENT_TARGET)
+ set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0" CACHE STRING "Minimum macOS version" FORCE)
+ endif()
+
+ # Build a proper .app bundle, NOT a bare command-line tool. Without this CMake
+ # emits a `com.apple.product-type.tool` target; when Xcode runs a tool that then
+ # turns itself into a GUI app (NSApplication / setActivationPolicy:Regular),
+ # macOS relaunches it to grant a GUI session — and relaunching a non-bundle
+ # executable goes through LaunchServices -> Terminal, which spawned EXTRA copies
+ # of the app (the mysterious "3 windows" only under Xcode). A real .app has a
+ # stable LaunchServices identity and is launched exactly once.
+ #
+ # The bundle lands at the repo root (CMAKE_RUNTIME_OUTPUT_DIRECTORY) next to
+ # main.cs. The engine's getExecutablePath() (osxFileIO.mm) searches inside the
+ # bundle (Contents/Resources) and then the bundle's PARENT dir for main.cs, so
+ # the script/asset trees resolve with no extra packaging — same as the legacy
+ # .app build.
+ set_target_properties(Torque2D PROPERTIES
+ MACOSX_BUNDLE TRUE
+ MACOSX_BUNDLE_BUNDLE_NAME "Torque2D"
+ MACOSX_BUNDLE_GUI_IDENTIFIER "org.torque2d.Torque2D"
+ MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_VERSION}"
+ MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}")
+
+ # Ad-hoc code signing ("Sign to Run Locally"). On Apple Silicon a .app must be
+ # signed to run, but we don't want to require an Apple developer team/identity
+ # for a local build. "-" is the ad-hoc identity; Manual style stops Xcode's
+ # automatic signing from trying (and failing) to use a real team. Override these
+ # for a distributable, notarized build.
+ set_target_properties(Torque2D PROPERTIES
+ XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "-"
+ XCODE_ATTRIBUTE_CODE_SIGN_STYLE "Manual"
+ XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "NO")
+
+ # The platformOSX .mm back-end uses AppKit/Foundation types at file scope and
+ # relied on the legacy Xcode prefix header to import Cocoa. Reproduce that by
+ # force-including a CMake-owned prefix header (no-op for C/C++ TUs via __OBJC__).
+ target_compile_options(Torque2D PRIVATE
+ -include "${CMAKE_SOURCE_DIR}/tools/CMake/macOS-Prefix.h")
+
+ # macOS desktop frameworks (from the historical build recipe). The on-platform
+ # (Mac) session should confirm this links and the app starts; OpenAL is a
+ # framework here (not dynamically loaded as on Windows).
+ target_link_libraries(Torque2D PRIVATE
+ "-framework Cocoa" "-framework OpenGL" "-framework CoreData"
+ "-framework CoreFoundation" "-framework Foundation" "-framework AppKit"
+ "-framework AVFoundation" "-framework OpenAL")
+endif()
+
+if(TORQUE_IOS)
+ # iOS frameworks, derived from the maintained engine/compilers/Xcode_iOS
+ # project. Configure with the Xcode generator and -DCMAKE_SYSTEM_NAME=iOS.
+ # iOS uses OpenGL ES (not desktop OpenGL).
+
+ # Simulator vs. device. The simulator (-DCMAKE_OSX_SYSROOT=iphonesimulator)
+ # needs NO code-signing — the easiest first target. A physical device needs a
+ # real signing identity + provisioning profile, which means automatic signing
+ # and an Apple Development Team. Detect from the SDK: anything not a *simulator*
+ # SDK (incl. the default empty -> iphoneos) is a device build.
+ if(CMAKE_OSX_SYSROOT MATCHES "[Ss]imulator")
+ set(TORQUE_IOS_SIMULATOR TRUE)
+ else()
+ set(TORQUE_IOS_SIMULATOR FALSE)
+ endif()
+
+ # Overridable identity. Bundle id should be unique to YOUR Apple account for
+ # device provisioning (default uses the project's reverse-domain; override with
+ # e.g. -DTORQUE_IOS_BUNDLE_ID=com.yourcompany.torque2d). TORQUE_IOS_TEAM is your
+ # 10-char Apple Development Team ID — leave empty to pick the team in Xcode's
+ # Signing & Capabilities tab instead.
+ set(TORQUE_IOS_BUNDLE_ID "org.torque2d.Torque2D" CACHE STRING "iOS app bundle identifier")
+ set(TORQUE_IOS_TEAM "" CACHE STRING "Apple Development Team ID for iOS device code-signing")
+
+ set_target_properties(Torque2D PROPERTIES
+ MACOSX_BUNDLE TRUE
+ XCODE_ATTRIBUTE_IPHONEOS_DEPLOYMENT_TARGET "12.0")
+
+ if(TORQUE_IOS_SIMULATOR)
+ # Simulator: skip signing entirely.
+ set_target_properties(Torque2D PROPERTIES XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "NO")
+ else()
+ # Device: automatic signing. Xcode creates/uses a development provisioning
+ # profile for TORQUE_IOS_BUNDLE_ID under the selected team. If TORQUE_IOS_TEAM
+ # is empty, Xcode shows "Signing requires a development team" until you pick
+ # one in the target's Signing & Capabilities tab (a free Apple ID works).
+ set_target_properties(Torque2D PROPERTIES XCODE_ATTRIBUTE_CODE_SIGN_STYLE "Automatic")
+ if(TORQUE_IOS_TEAM)
+ set_target_properties(Torque2D PROPERTIES XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "${TORQUE_IOS_TEAM}")
+ endif()
+ endif()
+
+ # The iOS app's window is NOT created programmatically — T2DAppDelegate relies
+ # on a Main storyboard (UIMainStoryboardFile) to instantiate T2DViewController
+ # (a GLKViewController) hosting T2DView (the GLKView the engine renders into).
+ # CMake's auto-generated Info.plist has none of the iOS keys (no bundle id, no
+ # storyboard, no orientations), so the bare bundle launches the delegate but
+ # never gets a window -> black screen. Supply a real iOS Info.plist template
+ # and fill it from these MACOSX_BUNDLE_* properties (CFBundleExecutable is
+ # filled per-config, so Debug -> Torque2D_DEBUG / Release -> Torque2D).
+ set_target_properties(Torque2D PROPERTIES
+ MACOSX_BUNDLE_INFO_PLIST "${CMAKE_SOURCE_DIR}/tools/CMake/iOS-Info.plist.in"
+ MACOSX_BUNDLE_BUNDLE_NAME "Torque2D"
+ MACOSX_BUNDLE_GUI_IDENTIFIER "${TORQUE_IOS_BUNDLE_ID}"
+ MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_VERSION}"
+ MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}"
+ # Xcode prefers these build settings over the matching Info.plist keys and
+ # warns on a mismatch. Set them so the bundle id used at install time is
+ # ours and the device family (1=iPhone, 2=iPad) is authoritative. (The
+ # plist's CFBundleIdentifier is kept as a literal for non-Xcode tooling;
+ # UIDeviceFamily is intentionally omitted from the plist in favor of this.)
+ XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${TORQUE_IOS_BUNDLE_ID}"
+ XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2")
+
+ # Bundle the storyboards as compiled resources (Xcode runs ibtool on them).
+ # iPhone/iPad: the GLKit UI referenced by UIMainStoryboardFile. LaunchScreen:
+ # a plain static screen referenced by UILaunchStoryboardName (modern iOS needs
+ # one for a full-screen drawable; it may not contain custom classes, so it's
+ # separate from the GL storyboards). The two GL storyboards were copied from
+ # engine/compilers/Xcode_iOS into tools/CMake so the CMake build is self-
+ # contained (independent of the legacy project tree).
+ set(TORQUE_IOS_STORYBOARDS
+ ${CMAKE_SOURCE_DIR}/tools/CMake/iPhoneStoryboard.storyboard
+ ${CMAKE_SOURCE_DIR}/tools/CMake/iPadStoryboard.storyboard
+ ${CMAKE_SOURCE_DIR}/tools/CMake/LaunchScreen.storyboard)
+ target_sources(Torque2D PRIVATE ${TORQUE_IOS_STORYBOARDS})
+ set_source_files_properties(${TORQUE_IOS_STORYBOARDS} PROPERTIES
+ MACOSX_PACKAGE_LOCATION Resources)
+
+ # The engine's OS detection (platform/types.gcc.h) gates the iOS branch on
+ # TORQUE_OS_IOS but only DEFINES it inside that branch — so the build system
+ # must predefine it (the legacy Xcode_iOS project did). Without it, __APPLE__
+ # makes the engine select the macOS/desktop-GL back-end and fail.
+ target_compile_definitions(Torque2D PRIVATE TORQUE_OS_IOS)
+
+ # The debug-only "outline GL" feature (iOSOutlineGL.h) does
+ # `#define glDrawArrays glDrawArraysProcPtr` to route draws through a swappable
+ # function pointer. On a modern iOS SDK the prefix header drags GLES gl.h in a
+ # second time (UIKit -> CoreImage), and the macro rewrites the SDK's
+ # glDrawArrays *function* declaration into glDrawArraysProcPtr, colliding with
+ # the engine's same-named *variable*. NO_REDEFINE_GL_FUNCS disables the macro
+ # (the engine's own escape hatch; each platform's *OutlineGL source already
+ # defines it locally) — outline/wireframe debug draw becomes a no-op on iOS.
+ target_compile_definitions(Torque2D PRIVATE NO_REDEFINE_GL_FUNCS)
+
+ # graphics/bitmapPvr.cc (PVR textures) is required on iOS but excluded from
+ # desktop builds in EngineSources.cmake — add it back here.
+ target_sources(Torque2D PRIVATE ${TORQUE_SRC}/graphics/bitmapPvr.cc)
+
+ # The platformiOS .mm back-end uses UIKit/Foundation types at file scope and
+ # relied on the legacy Xcode prefix header. Force-include the CMake-owned one
+ # (no-op for C/C++ TUs via __OBJC__).
+ target_compile_options(Torque2D PRIVATE
+ -include "${CMAKE_SOURCE_DIR}/tools/CMake/iOS-Prefix.h")
+
+ target_link_libraries(Torque2D PRIVATE
+ "-framework UIKit" "-framework OpenGLES" "-framework GLKit"
+ "-framework QuartzCore" "-framework CoreGraphics" "-framework CoreText"
+ "-framework CoreMotion" "-framework CoreMedia" "-framework CoreVideo"
+ "-framework CoreAudio" "-framework CoreImage" "-framework AudioToolbox"
+ "-framework AVFoundation" "-framework MediaPlayer" "-framework Foundation"
+ "-framework CoreFoundation" "-framework OpenAL"
+ "-framework GameKit") # platformiOS/GameCenter.mm
+
+ # Bundle the script/asset trees INTO the .app. Unlike the desktop build (which
+ # runs from the repo root and finds main.cs + the trees via the cwd), an
+ # installed iOS app is sandboxed: getExecutablePath() (iOSFileio.mm) resolves
+ # to the bundle, so the content must live inside it. The engine boots the
+ # editor via main.cs (exec "./editor/main.cs"), which needs the library/ and
+ # toybox/ trees too. iOS bundles are FLAT, so everything goes to the bundle
+ # ROOT ($), matching the legacy Xcode_iOS folder references.
+ # (POST_BUILD so it lands in whatever build dir Xcode produces, before install;
+ # copy_directory re-copies each build — acceptable for a dev/smoke-test bundle.)
+ set(TORQUE_IOS_CONTENT_DIRS editor library toybox tools)
+ add_custom_command(TARGET Torque2D POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy
+ "${CMAKE_SOURCE_DIR}/main.cs" "$/main.cs"
+ COMMENT "Bundling Torque2D script/asset trees into the iOS .app")
+ foreach(_dir ${TORQUE_IOS_CONTENT_DIRS})
+ add_custom_command(TARGET Torque2D POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy_directory
+ "${CMAKE_SOURCE_DIR}/${_dir}" "$/${_dir}")
+ endforeach()
+endif()
+
+if(UNIX AND NOT APPLE AND NOT ANDROID AND NOT EMSCRIPTEN)
+ # Linux / X11 desktop build (platformX86UNIX). NOTES for the on-platform
+ # (WSL/Linux) session:
+ # * SDL 1.2 is REQUIRED, not optional: the platform back-end calls 1.2-only
+ # APIs (SDL_GetVideoSurface, SDL_WM_*, SDL_*GammaRamp, SDL_GL_SwapBuffers)
+ # and pulls X11_KeyToUnicode out of libSDL. This is NOT SDL2.
+ # * `rt` is a no-op on modern glibc but harmless.
+ # * OpenGL/FreeType are resolved via find_package (more robust than the old
+ # hard-coded /usr/include/freetype2 paths). Install dev packages, e.g. on
+ # Debian/Ubuntu: libsdl1.2-dev libx11-dev libxft-dev libfontconfig1-dev
+ # libfreetype6-dev libopenal-dev libgl1-mesa-dev (and nasm for the 32-bit
+ # build).
+ # * fontconfig is linked explicitly, not left to Xft: x86UNIXFont.cc calls
+ # Fc* directly for PlatformFont::enumeratePlatformFonts.
+ find_package(Threads REQUIRED)
+ find_package(OpenGL REQUIRED)
+ find_package(Freetype REQUIRED)
+
+ # SDL 1.2 (legacy). Resolve it directly rather than via FindSDL: the sources
+ # use `#include `, which needs the PARENT of the SDL header dir on
+ # the include path (find_path on SDL/SDL.h returns exactly that directory).
+ find_library(SDL12_LIBRARY NAMES SDL SDL-1.2)
+ find_path(SDL12_INCLUDE_DIR NAMES SDL/SDL.h)
+ if(NOT SDL12_LIBRARY OR NOT SDL12_INCLUDE_DIR)
+ message(FATAL_ERROR
+ "SDL 1.2 not found. Install it (Debian/Ubuntu: 'sudo apt install libsdl1.2-dev').")
+ endif()
+ target_include_directories(Torque2D PRIVATE ${SDL12_INCLUDE_DIR})
+
+ target_link_libraries(Torque2D PRIVATE
+ Threads::Threads
+ OpenGL::GL
+ Freetype::Freetype
+ m dl rt
+ X11 Xft fontconfig
+ ${SDL12_LIBRARY}
+ openal)
+
+ # CPU / bitness. detectX86CPUInfo() (platform/platformCPUInfo.asm — 32-bit
+ # NASM, it does NOT assemble for elf64) is referenced only when TORQUE_64 is
+ # undefined (engine/source/platformX86UNIX/x86UNIXCPUInfo.cc).
+ # * 64-bit: define TORQUE_64 so the asm is never referenced (and never built).
+ # * 32-bit (-m32): assemble the asm with NASM (elf32) and define `i386` —
+ # bare `i386` isn't predefined under standard C++, and the engine's CPU
+ # detection (types.gcc.h) keys off it. LINUX is already a target define,
+ # so NASM picks it up too (its export macro is `%ifdef LINUX`).
+ if(CMAKE_SIZEOF_VOID_P EQUAL 8)
+ target_compile_definitions(Torque2D PRIVATE TORQUE_64)
+ else()
+ set(CMAKE_ASM_NASM_OBJECT_FORMAT elf32)
+ enable_language(ASM_NASM)
+ target_sources(Torque2D PRIVATE ${TORQUE_SRC}/platform/platformCPUInfo.asm)
+ target_compile_definitions(Torque2D PRIVATE i386)
+ endif()
+endif()
+
+if(ANDROID)
+ # Prebuilt third-party libs (arm64-v8a only — the other ABIs have no prebuilts).
+ # FreeType is an ancient (2.4.12) static .a; OpenAL is a shared .so that must
+ # also be packaged into the APK (see the Gradle jniLibs config).
+ add_library(android_freetype STATIC IMPORTED)
+ set_target_properties(android_freetype PROPERTIES
+ IMPORTED_LOCATION "${TORQUE_LIB}/freetype/android/lib/${ANDROID_ABI}/libfreetype.a"
+ INTERFACE_INCLUDE_DIRECTORIES "${TORQUE_LIB}/freetype/android/include;${TORQUE_LIB}/freetype/android/include/freetype2")
+
+ add_library(android_openal SHARED IMPORTED)
+ set_target_properties(android_openal PROPERTIES
+ IMPORTED_LOCATION "${TORQUE_LIB}/openal/Android/${ANDROID_ABI}/libopenal.so"
+ INTERFACE_INCLUDE_DIRECTORIES "${TORQUE_LIB}/openal/Android/openal-soft-master/jni/OpenAL/include")
+
+ # Android system libraries (z = NDK libz; GLESv1_CM matches the engine GL path).
+ target_link_libraries(Torque2D PRIVATE
+ android_freetype android_openal
+ log android EGL GLESv1_CM OpenSLES z)
+endif()
+
+if(EMSCRIPTEN)
+ # WebAssembly build (configure via `emcmake cmake`, which points CMake at the
+ # Emscripten toolchain and sets EMSCRIPTEN=1). See cmake/BUILD-PLATFORM-NOTES.md.
+
+ # (EMSCRIPTEN=1 is defined GLOBALLY above, before the third-party libs — the
+ # engine's types.gcc.h and some vendored libs both key off bare EMSCRIPTEN.
+ # That's what makes the engine select TORQUE_OS_EMSCRIPTEN / platformEmscripten.)
+
+ # SDL 1.2 (input/video back-end, like the desktop X11 build) is provided by
+ # emscripten's bundled SDL1 port; USE_SDL=1 is the default but set it explicitly
+ # for both compile (headers: #include ) and link.
+ target_compile_options(Torque2D PRIVATE "-sUSE_SDL=1")
+
+ # FreeType (built from source in engine/lib for the web build only) — the
+ # rasterizer EmscriptenFont uses to synthesize glyphs from a bundled .ttf when a
+ # font isn't in the .uft cache. Its PUBLIC include dir propagates here, so
+ # EmscriptenFont.cpp sees / FT_FREETYPE_H.
+ target_link_libraries(Torque2D PRIVATE freetype)
+
+ # Keep the web bundle (.html/.js/.wasm/.data) in the build tree rather than the
+ # repo root — unlike a desktop exe it isn't run from the cwd (assets come from
+ # the MEMFS preload below), so there's no reason to drop four files at the root.
+ set_target_properties(Torque2D PROPERTIES
+ RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
+ RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}"
+ RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}"
+ RUNTIME_OUTPUT_DIRECTORY_SHIPPING "${CMAKE_BINARY_DIR}")
+
+ # Script/asset trees packaged into the MEMFS virtual filesystem by the emcc file
+ # packager (--preload-file SRC@DST -> a .data sidecar). The engine runs from cwd
+ # "/" in the browser, so main.cs lands at /main.cs and the trees under /editor,
+ # /library, /toybox. Everything a web build can touch must be preloaded (there's
+ # no host FS to lazy-load from), so the runtime content trees are all bundled.
+ # NOTE: tools/ is deliberately EXCLUDED — unlike the iOS bundle it is not shipped,
+ # because it is build-time-only tooling (TexturePacker, and ~90 MB of generated
+ # doxygen HTML under tools/doxygen/output) that the engine never loads at runtime.
+ # (SHELL: keeps each option+path pair together and unde-duplicated by CMake.)
+ set(_emPreload
+ "SHELL:--preload-file ${CMAKE_SOURCE_DIR}/main.cs@/main.cs"
+ "SHELL:--preload-file ${CMAKE_SOURCE_DIR}/editor@/editor"
+ "SHELL:--preload-file ${CMAKE_SOURCE_DIR}/library@/library"
+ "SHELL:--preload-file ${CMAKE_SOURCE_DIR}/toybox@/toybox"
+ "SHELL:--preload-file ${CMAKE_SOURCE_DIR}/PlanetX@/PlanetX")
+
+ target_link_options(Torque2D PRIVATE
+ "-sUSE_SDL=1"
+ "-sLEGACY_GL_EMULATION=1" # fixed-function GL over WebGL (at-risk flag; see notes)
+ # Fixed 512 MB heap: with ALLOW_MEMORY_GROWTH the WASM heap ArrayBuffer is
+ # resizable, and Chrome's texImage2D rejects typed-array views into a
+ # resizable buffer (seen with emscripten 6.x + LEGACY_GL_EMULATION).
+ "-sINITIAL_MEMORY=536870912"
+ "-sALLOW_MEMORY_GROWTH=0"
+ "-sEXIT_RUNTIME=0"
+ "-sFORCE_FILESYSTEM=1"
+ "$<$:-sASSERTIONS=1>"
+ "SHELL:--js-library ${TORQUE_SRC}/platformEmscripten/platform.js"
+ ${_emPreload})
+
+ # emcc picks the output format from the target suffix: .html emits the HTML
+ # shell + .js + .wasm (+ .data from the preload). -> Torque2D_DEBUG.html etc.
+ set_target_properties(Torque2D PROPERTIES SUFFIX ".html")
+endif()
+
+# Output name. On Android the NativeActivity loads "torque2d" -> libtorque2d.so.
+# On desktop, match the VS solution (Debug -> Torque2D_DEBUG.exe).
+if(ANDROID)
+ set_target_properties(Torque2D PROPERTIES OUTPUT_NAME "torque2d")
+else()
+ set_target_properties(Torque2D PROPERTIES
+ OUTPUT_NAME "Torque2D"
+ OUTPUT_NAME_DEBUG "Torque2D_DEBUG"
+ )
+endif()
+
+# ----------------------------------------------------------------------------
+# Windows application resource (icon + version info) from the .rc template.
+# Assembled in the build tree so resource.h and the .ico resolve next to the rc.
+# ----------------------------------------------------------------------------
+if(WIN32)
+ set(_resDir "${CMAKE_BINARY_DIR}/resources")
+ configure_file("${CMAKE_SOURCE_DIR}/tools/CMake/Torque 2D.rc.in" "${_resDir}/Torque2D.rc" COPYONLY)
+ configure_file("${CMAKE_SOURCE_DIR}/tools/CMake/resource.h" "${_resDir}/resource.h" COPYONLY)
+ configure_file("${CMAKE_SOURCE_DIR}/tools/CMake/Torque 2D.ico" "${_resDir}/Torque 2D.ico" COPYONLY)
+ target_sources(Torque2D PRIVATE "${_resDir}/Torque2D.rc")
+endif()
+
+# ----------------------------------------------------------------------------
+# IDE niceties (Visual Studio + Xcode).
+# ----------------------------------------------------------------------------
+source_group(TREE "${TORQUE_SRC}" PREFIX "engine"
+ FILES ${TORQUE_ENGINE_SOURCES} ${TORQUE_PLATFORM_SOURCES})
+
+set_property(DIRECTORY "${CMAKE_SOURCE_DIR}" PROPERTY VS_STARTUP_PROJECT Torque2D)
+set_target_properties(Torque2D PROPERTIES
+ VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
-add_subdirectory(tools/CMake)
+# On the macOS desktop build, make Xcode's Run/Debug scheme launch the app with
+# the repo root as its working directory — the exe loads main.cs and the script/
+# asset trees relative to the cwd, so without this "Run" from Xcode starts in the
+# wrong directory and the engine can't find main.cs. (iOS runs from a bundle in
+# the simulator/device, so this doesn't apply there.)
+if(APPLE AND NOT TORQUE_IOS)
+ set_target_properties(Torque2D PROPERTIES
+ XCODE_GENERATE_SCHEME TRUE
+ XCODE_SCHEME_WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
+endif()
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 000000000..1aa1658bd
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,14 @@
+Torque Game Engines Community Code of Conduct
+=============================================
+
+* We, as a community, are committed to providing a friendly, safe and welcoming environment for all, regardless of experience, background, history or characteristic.
+* Please avoid using any overly sexual, offensive, or insulting alias or nicknames for yourself or others that would otherwise detract from a friendly, safe and welcoming environment.
+* Please be kind and courteous. There's no need to be rude.
+* Respect that people have differences of opinion, and that every design, implementation, or choice carries considerations, trade-offs, and costs. There is rarely ever a perfect answer.
+* Please keep critiques constructive. If you are going to critique someone's work, ideas, or decisions, do it with topical and specific feedback so that they can improve. Unconstructive critiques don't help anyone better their work.
+* Insulting, demeaning or harassing anyone is not welcome behavior, whether in public or private conversations. If you feel you have been subjected to any of this behavior, please contact any moderator or admin immediately. Whether you're a regular or a newcomer, we care about making this community a safe place for you. If you have any lack of clarity about what might fall under those concepts, feel free to ask for clarification from the moderation team.
+* Likewise, any spamming, trolling, flaming, baiting or other attention-stealing/seeking behavior is not welcome.
+* Do not insult or deride other users. 'You're an idiot' is not a useful comment. Do not do this.
+* If feedback is being provided, it needs to be constructive. Just simply calling something a bad idea is not helpful in correcting the core issues of that idea. Take the time to explain what is wrong with the idea and how it could be improved upon.
+* In discussions, under normal circumstances if the OP of a thread requests a post to be removed because they feel it is inflammatory, or off topic or the like, the mods will review it and in most cases, remove it per OP's request. It's their thread, so they get nearly final say in how the thread's topic should flow. MODGRU of course gets the actual final say, but will listen to the thread OPs reasoning, and if it makes sense, will generally abide the request.
+* If any user has a problem with something on any community site, such as content posted, or another user's behavior, they need to contact someone in MODGRU. Moderation cannot be expected to happen in a concise, timely manner if they are not informed about it. Rather than engaging in the negative behavior, users are heavily encouraged to report it and move on. Engaging in bad behavior in response only sets both parties up to be in trouble with moderation.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..7d37236c0
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,122 @@
+# Contributing to Torque2D
+
+Torque2D is MIT licensed and maintained by the Torque Game Engines team with
+contributions from the community. Bug reports, fixes, features and documentation are all
+welcome.
+
+There is no agreement to sign. Two things are required of every contribution:
+
+- **It must be legally yours to give.** Your contribution cannot contain code that is not
+ legally compatible with Torque2D's MIT license (see `LICENSE.md`) — no copied code from a
+ GPL, proprietary or otherwise incompatible source, and nothing you do not have the right
+ to relicense. Submitting a pull request means your contribution is offered under that
+ license.
+- **It must follow the standards below.**
+
+## Where pull requests go
+
+**All pull requests target the `development` branch.** A pull request opened against
+`master` will be asked to retarget.
+
+| Branch | Purpose |
+|---|---|
+| `master` | Current stable release. Production-usable. |
+| `development` | Active development. Merged to `master` at release. Treat as unstable. |
+| `gh-pages` | Generated Doxygen output. Not edited by hand. |
+
+## Submitting a change
+
+1. Fork the repository and clone your fork.
+2. `git checkout development`
+3. Add the upstream remote: `git remote add upstream https://github.com/TorqueGameEngines/Torque2D.git`
+4. Branch from `development` for your work: `git checkout -b my-change`
+5. Commit, push to your fork, and open a pull request against `TorqueGameEngines/Torque2D` `development`.
+
+Keep your branch current with `git pull upstream development`.
+
+### Pull request scope
+
+**One change per pull request.** A pull request that adds shader support should not also
+refactor the math helpers. Unrelated changes bundled together are slower to review and
+harder to revert.
+
+Include in the description: what the change does, why, and how you tested it. If it fixes
+an issue, reference it.
+
+## Before you submit
+
+**Build it.** CMake is the single source of truth; see the
+[Building from Source](https://github.com/TorqueGameEngines/Torque2D/wiki/Building) guide.
+Generating a project by hand is not required — the root scripts (`generate-vs2022.bat`,
+`generate-vs2026.bat`, `generate-xcode.command`, `build-linux.sh`, `generate-emscripten.sh`)
+do it for you.
+
+**Run the tests.** CI builds your change but does not run tests, so this is on you:
+
+```
+tests\run-unit.ps1 C++ unit tests (GoogleTest, fast)
+tests\run.ps1 TorqueScript integration tests (slow; drives a real engine)
+```
+
+On Linux and macOS use `tests/run.sh`. `tests/README.md` covers writing a new test —
+read it before adding one, particularly the note about relative paths.
+
+**Add a changelog entry.** If your change is user-visible — new behavior, a changed or
+removed API, a fixed bug — add a line to `CHANGELOG.md` under the unreleased section, in
+the same pull request. Entries written after the fact do not get written.
+
+**What CI checks.** Every push and pull request builds Windows (VS2022 and VS2026, 64- and
+32-bit), Linux (64- and 32-bit), macOS, iOS and Android. A red build will not be merged.
+
+## Adding files
+
+New engine source files must be listed explicitly in `cmake/EngineSources.cmake`
+(cross-platform) or `cmake/PlatformSources.cmake` (platform back-ends). These lists are the
+source of truth — there are no globs, and no `.vcxproj`, Xcode or Makefile lists to update.
+A file not listed is not compiled.
+
+## Coding standards
+
+### C++
+
+- Header guards use the `_NAME_H_` convention, and every include site wraps the `#include`
+ in an `#ifndef` check. See `platform.h`.
+- A script-visible class uses `DECLARE_CONOBJECT(ClassName)` in its header and
+ `IMPLEMENT_CONOBJECT(ClassName)` in its `.cc`.
+- Script-visible fields are registered in `initPersistFields()`. These are also what TAML
+ serializes.
+- **Script-callable methods belong in the class's `*_ScriptBinding.h` file, not the `.cc`.**
+ The doc comments there generate the scripting reference, so write them.
+- Match the surrounding file's formatting. Do not reformat code you are not changing.
+- Fix compiler warnings your change introduces.
+
+Fuller detail, including examples, is in the
+[Pull Requests and Coding Standards](https://github.com/TorqueGameEngines/Torque2D/wiki/Pull-Requests-Coding-Standards)
+guide.
+
+### TorqueScript
+
+Script conventions are in `TORQUE_SCRIPT.md` at the repository root: one class per file,
+`onAdd`/`onRemove` lifecycle, each object freeing what it created, and `class`/`superclass`
+inheritance. These are recommendations for keeping a growing codebase navigable, not engine
+requirements — but the shipped modules follow them, and new script in this repository
+should too.
+
+### Portability
+
+The engine targets Windows, macOS, Linux, iOS, Android and the web. Do not assume a
+platform, a word size, or an endianness. Use the engine's platform layer
+(`platform/platform.h`) rather than OS APIs directly, and its types (`U32`, `S32`, `F32`)
+rather than raw C types where the size matters.
+
+## Reporting bugs
+
+Open an issue with the engine version or commit, your platform and build configuration,
+what you expected, what happened, and the smallest steps that reproduce it. A failing test
+or a small module that demonstrates the problem is the most useful thing you can attach.
+
+## Documentation
+
+Engine and API documentation lives in the
+[wiki](https://github.com/TorqueGameEngines/Torque2D/wiki), which is a separate repository.
+Corrections there do not go through this repository.
diff --git a/OpenAL32.dll b/OpenAL32.dll
deleted file mode 100644
index 5ce625849..000000000
Binary files a/OpenAL32.dll and /dev/null differ
diff --git a/PlanetX/AppCore/1/appCore.cs b/PlanetX/AppCore/1/appCore.cs
new file mode 100644
index 000000000..856d192ed
--- /dev/null
+++ b/PlanetX/AppCore/1/appCore.cs
@@ -0,0 +1,49 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2013 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+
+function AppCore::create( %this )
+{
+ // Load system scripts
+ exec("./scripts/constants.cs");
+ exec("./scripts/defaultPreferences.cs");
+ exec("./gui/guiCursors.cs");
+ exec("./scripts/themes.cs");
+ %this.loadThemes();
+ // After the themes, not before: the cursors a project uses are its theme's,
+ // and this installs them under the names the engine looks up.
+ %this.installThemeCursors(%this.cursorTheme());
+ exec("./scripts/canvas.cs");
+
+ // Initialize the canvas
+ %module = ModuleDatabase.findModule("AppCore", 1);
+ %this.initializeCanvas(%module.Project);
+
+ // Load other modules
+ ModuleDatabase.loadGroup("launch");
+}
+
+//-----------------------------------------------------------------------------
+
+function AppCore::destroy( %this )
+{
+
+}
diff --git a/PlanetX/AppCore/1/fonts/Roboto-OFL.txt b/PlanetX/AppCore/1/fonts/Roboto-OFL.txt
new file mode 100644
index 000000000..9c48e05a2
--- /dev/null
+++ b/PlanetX/AppCore/1/fonts/Roboto-OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2011 The Roboto Project Authors (https://github.com/googlefonts/roboto-classic)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+https://openfontlicense.org
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/PlanetX/AppCore/1/fonts/Roboto-Regular.ttf b/PlanetX/AppCore/1/fonts/Roboto-Regular.ttf
new file mode 100644
index 000000000..3db0d1fb0
Binary files /dev/null and b/PlanetX/AppCore/1/fonts/Roboto-Regular.ttf differ
diff --git a/PlanetX/AppCore/1/fonts/share tech mono 12 (ansi).uft b/PlanetX/AppCore/1/fonts/share tech mono 12 (ansi).uft
new file mode 100644
index 000000000..c4b04d8ad
Binary files /dev/null and b/PlanetX/AppCore/1/fonts/share tech mono 12 (ansi).uft differ
diff --git a/PlanetX/AppCore/1/fonts/share tech mono 14 (ansi).uft b/PlanetX/AppCore/1/fonts/share tech mono 14 (ansi).uft
new file mode 100644
index 000000000..d06745b4c
Binary files /dev/null and b/PlanetX/AppCore/1/fonts/share tech mono 14 (ansi).uft differ
diff --git a/PlanetX/AppCore/1/fonts/share tech mono 16 (ansi).uft b/PlanetX/AppCore/1/fonts/share tech mono 16 (ansi).uft
new file mode 100644
index 000000000..57a055e5e
Binary files /dev/null and b/PlanetX/AppCore/1/fonts/share tech mono 16 (ansi).uft differ
diff --git a/PlanetX/AppCore/1/fonts/share tech mono 18 (ansi).uft b/PlanetX/AppCore/1/fonts/share tech mono 18 (ansi).uft
new file mode 100644
index 000000000..bb647a9e4
Binary files /dev/null and b/PlanetX/AppCore/1/fonts/share tech mono 18 (ansi).uft differ
diff --git a/PlanetX/AppCore/1/fonts/share tech mono 24 (ansi).uft b/PlanetX/AppCore/1/fonts/share tech mono 24 (ansi).uft
new file mode 100644
index 000000000..b94cdc102
Binary files /dev/null and b/PlanetX/AppCore/1/fonts/share tech mono 24 (ansi).uft differ
diff --git a/PlanetX/AppCore/1/gui/guiCursors.cs b/PlanetX/AppCore/1/gui/guiCursors.cs
new file mode 100644
index 000000000..7eb14ab73
--- /dev/null
+++ b/PlanetX/AppCore/1/gui/guiCursors.cs
@@ -0,0 +1,169 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2013 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+
+/// The mouse cursors a GUI names by convention: a text field asks for
+/// EditCursor, a window's edges for LeftRightCursor and friends, and a control
+/// with none of its own gets DefaultCursor. Those names are hard-coded in the
+/// engine (guiTextEditCtrl.cc, guiWindowCtrl.cc, guiFrameSetCtrl.cc,
+/// guiEditCtrl.cc), so something has to answer to them.
+///
+/// This file used to answer by building seven cursors out of literals, the last
+/// of the hand-written GUI furniture after the ~70 profiles became a
+/// GuiProfileTheme. Now the theme owns cursors too - each one its own art,
+/// tinted from the theme's palette - and this installs a chosen theme's set
+/// under the canonical names. A control that names a cursor outright still wins;
+/// this is only what everything else falls back to, including the canvas arrow.
+///
+/// It is also callable at any time, which is how a game swaps between themes
+/// that look nothing alike:
+///
+/// AppCore.installThemeCursors(Combat);
+/// Canvas.setCursor(DefaultCursor);
+
+/// Registers %object under %name, or - if something already holds the name -
+/// copies the new object's fields onto the existing one and throws the new one
+/// away. That is how a project tunes an object the engine or another module
+/// already made, GuiDefaultProfile being the standing example.
+function AppCore::SafeCreateNamedObject(%this, %name, %object)
+{
+ if(isObject(%name))
+ {
+ %originalObject = nameToID(%name);
+ if(%originalObject.getClassName() !$= %object.getClassName())
+ {
+ warn("Attempted to change the class of the named object " @ %name @ "!");
+ warn("Original Class: " @ %originalObject.getClassName());
+ warn("New Class: " @ %object.getClassName());
+ return;
+ }
+ %originalObject.assignFieldsFrom(%object);
+ %object.delete();
+ }
+ else
+ {
+ %object.setName(%name);
+ }
+}
+
+/// Every theme the project loaded, as a space-separated list of ids. They live
+/// in the Gui data group, which is where GuiProfileTheme::onAdd puts them.
+function AppCore::getThemes(%this)
+{
+ %themes = "";
+ if(!isObject(GuiDataGroup))
+ {
+ return %themes;
+ }
+
+ for(%i = 0; %i < GuiDataGroup.getCount(); %i++)
+ {
+ %object = GuiDataGroup.getObject(%i);
+ if(%object.getClassName() $= "GuiProfileTheme")
+ {
+ %themes = (%themes $= "") ? %object.getId() : (%themes SPC %object.getId());
+ }
+ }
+
+ return %themes;
+}
+
+/// Which theme's cursors become the canonical ones. A project with one theme
+/// never has to think about this; a project with several says so by setting
+/// $pref::AppCore::cursorTheme, and gets told when it hasn't.
+function AppCore::cursorTheme(%this)
+{
+ %themes = %this.getThemes();
+ %count = getWordCount(%themes);
+ if(%count == 0)
+ {
+ return 0;
+ }
+
+ if($pref::AppCore::cursorTheme !$= "")
+ {
+ for(%i = 0; %i < %count; %i++)
+ {
+ %theme = getWord(%themes, %i);
+ if(%theme.getName() $= $pref::AppCore::cursorTheme)
+ {
+ return %theme;
+ }
+ }
+ warn("AppCore::cursorTheme: $pref::AppCore::cursorTheme names '" @ $pref::AppCore::cursorTheme @ "', which is not a loaded theme.");
+ }
+
+ if(%count == 1)
+ {
+ return getWord(%themes, 0);
+ }
+
+ for(%i = 0; %i < %count; %i++)
+ {
+ %theme = getWord(%themes, %i);
+ if(%theme.getName() $= "Base")
+ {
+ return %theme;
+ }
+ }
+
+ %first = getWord(%themes, 0);
+ warn("AppCore::cursorTheme: " @ %count @ " themes are loaded and none is named 'Base', so the cursors come from '" @
+ %first.getName() @ "'. Set $pref::AppCore::cursorTheme to choose.");
+ return %first;
+}
+
+/// Point the canonical cursor names at %theme's cursors. The names are copies
+/// rather than the members themselves: a name can only belong to one object,
+/// and a theme's members have to keep their own names for the Guis that
+/// reference them.
+function AppCore::installThemeCursors(%this, %theme)
+{
+ if(!isObject(%theme))
+ {
+ warn("AppCore::installThemeCursors: no theme to install cursors from.");
+ return false;
+ }
+
+ %categories = %theme.getCursorCategoryNames();
+ %count = getWordCount(%categories);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %category = getWord(%categories, %i);
+ %cursor = %theme.getCursor(%category);
+ if(!isObject(%cursor))
+ {
+ continue;
+ }
+
+ // The category's canonical name comes from the engine's own table, so
+ // this never has to take a theme name apart to find it.
+ %this.SafeCreateNamedObject(%theme.getCursorCanonicalName(%category), new GuiCursor()
+ {
+ bitmapName = %cursor.bitmapName;
+ hotSpot = %cursor.hotSpot;
+ renderOffset = %cursor.renderOffset;
+ color = %cursor.color;
+ });
+ }
+
+ return true;
+}
diff --git a/PlanetX/AppCore/1/gui/images/cursors/NESW.png b/PlanetX/AppCore/1/gui/images/cursors/NESW.png
new file mode 100644
index 000000000..2f73696c8
Binary files /dev/null and b/PlanetX/AppCore/1/gui/images/cursors/NESW.png differ
diff --git a/PlanetX/AppCore/1/gui/images/cursors/NWSE.png b/PlanetX/AppCore/1/gui/images/cursors/NWSE.png
new file mode 100644
index 000000000..c952a3e45
Binary files /dev/null and b/PlanetX/AppCore/1/gui/images/cursors/NWSE.png differ
diff --git a/PlanetX/AppCore/1/gui/images/cursors/defaultCursor.png b/PlanetX/AppCore/1/gui/images/cursors/defaultCursor.png
new file mode 100644
index 000000000..a0d1ab9de
Binary files /dev/null and b/PlanetX/AppCore/1/gui/images/cursors/defaultCursor.png differ
diff --git a/PlanetX/AppCore/1/gui/images/cursors/ibeam.png b/PlanetX/AppCore/1/gui/images/cursors/ibeam.png
new file mode 100644
index 000000000..56079504c
Binary files /dev/null and b/PlanetX/AppCore/1/gui/images/cursors/ibeam.png differ
diff --git a/PlanetX/AppCore/1/gui/images/cursors/leftRight.png b/PlanetX/AppCore/1/gui/images/cursors/leftRight.png
new file mode 100644
index 000000000..c29b7a9f8
Binary files /dev/null and b/PlanetX/AppCore/1/gui/images/cursors/leftRight.png differ
diff --git a/PlanetX/AppCore/1/gui/images/cursors/move.png b/PlanetX/AppCore/1/gui/images/cursors/move.png
new file mode 100644
index 000000000..70f9cd540
Binary files /dev/null and b/PlanetX/AppCore/1/gui/images/cursors/move.png differ
diff --git a/PlanetX/AppCore/1/gui/images/cursors/upDown.png b/PlanetX/AppCore/1/gui/images/cursors/upDown.png
new file mode 100644
index 000000000..377217897
Binary files /dev/null and b/PlanetX/AppCore/1/gui/images/cursors/upDown.png differ
diff --git a/PlanetX/AppCore/1/gui/images/treeView.png b/PlanetX/AppCore/1/gui/images/treeView.png
new file mode 100644
index 000000000..b9bd571c9
Binary files /dev/null and b/PlanetX/AppCore/1/gui/images/treeView.png differ
diff --git a/PlanetX/AppCore/1/module.taml b/PlanetX/AppCore/1/module.taml
new file mode 100644
index 000000000..7e4a81a7f
--- /dev/null
+++ b/PlanetX/AppCore/1/module.taml
@@ -0,0 +1,21 @@
+
+
+
+
diff --git a/PlanetX/AppCore/1/projectIcon.png b/PlanetX/AppCore/1/projectIcon.png
new file mode 100644
index 000000000..6ef4d33fa
Binary files /dev/null and b/PlanetX/AppCore/1/projectIcon.png differ
diff --git a/PlanetX/AppCore/1/scripts/canvas.cs b/PlanetX/AppCore/1/scripts/canvas.cs
new file mode 100644
index 000000000..acf2ed1a7
--- /dev/null
+++ b/PlanetX/AppCore/1/scripts/canvas.cs
@@ -0,0 +1,126 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2013 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+
+//------------------------------------------------------------------------------
+// initializeCanvas
+// Constructs and initializes the default canvas window.
+//------------------------------------------------------------------------------
+function AppCore::initializeCanvas(%this, %windowName)
+{
+ // Don't duplicate the canvas.
+ if(!isObject(Canvas))
+ {
+ videoSetGammaCorrection($pref::OpenGL::gammaCorrection);
+
+ if ( !createCanvas(%windowName) )
+ {
+ error("Canvas creation failed. Shutting down.");
+ quit();
+ }
+
+ if ($platform $= "iOS")
+ {
+ %resolution = $pref::iOS::Width SPC $pref::iOS::Height SPC $pref::iOS::ScreenDepth;
+ }
+ else if ($platform $= "Android")
+ {
+ %resolution = GetAndroidResolution();
+ }
+ else
+ {
+ if ( $pref::Video::windowedRes !$= "" )
+ %resolution = $pref::Video::windowedRes;
+ else
+ %resolution = $pref::Video::defaultResolution;
+ }
+
+ if ($platform $= "windows" || $platform $= "macos")
+ {
+ setScreenMode( %resolution._0, %resolution._1, %resolution._2, $pref::Video::fullScreen );
+ }
+ else
+ {
+ setScreenMode( %resolution._0, %resolution._1, %resolution._2, false );
+ }
+ }
+ else
+ {
+ setCanvasTitle(%windowName);
+ Canvas.repaint();
+ }
+ Canvas.UseBackgroundColor = true;
+ Canvas.BackgroundColor = "Black";
+}
+
+//------------------------------------------------------------------------------
+// iOSResolutionFromSetting
+// Helper function that grabs resolution strings based on device type
+//------------------------------------------------------------------------------
+function AppCore::iOSResolutionFromSetting( %this, %deviceType, %deviceScreenOrientation )
+{
+ // A helper function to get a string based resolution from the settings given.
+ %x = 0;
+ %y = 0;
+
+ %scaleFactor = $pref::iOS::RetinaEnabled ? 2 : 1;
+
+ switch(%deviceType)
+ {
+ case $iOS::constant::iPhone:
+ if(%deviceScreenOrientation == $iOS::constant::Landscape)
+ {
+ %x = $iOS::constant::iPhoneWidth * %scaleFactor;
+ %y = $iOS::constant::iPhoneHeight * %scaleFactor;
+ }
+ else
+ {
+ %x = $iOS::constant::iPhoneHeight * %scaleFactor;
+ %y = $iOS::constant::iPhoneWidth * %scaleFactor;
+ }
+
+ case $iOS::constant::iPad:
+ if(%deviceScreenOrientation == $iOS::constant::Landscape)
+ {
+ %x = $iOS::constant::iPadWidth * %scaleFactor;
+ %y = $iOS::constant::iPadHeight * %scaleFactor;
+ }
+ else
+ {
+ %x = $iOS::constant::iPadHeight * %scaleFactor;
+ %y = $iOS::constant::iPadWidth * %scaleFactor;
+ }
+
+ case $iOS::constant::iPhone5:
+ if(%deviceScreenOrientation == $iOS::constant::Landscape)
+ {
+ %x = $iOS::constant::iPhone5Width;
+ %y = $iOS::constant::iPhone5Height;
+ }
+ else
+ {
+ %x = $iOS::constant::iPhone5Height;
+ %y = $iOS::constant::iPhone5Width;
+ }
+ }
+
+ return %x @ " " @ %y;
+}
diff --git a/PlanetX/AppCore/1/scripts/constants.cs b/PlanetX/AppCore/1/scripts/constants.cs
new file mode 100644
index 000000000..d1b23bc4d
--- /dev/null
+++ b/PlanetX/AppCore/1/scripts/constants.cs
@@ -0,0 +1,51 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2013 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+
+$iOS::constant::iPhone = 0;
+$iOS::constant::iPad = 1;
+$iOS::constant::iPhone5 = 2;
+
+$iOS::constant::Landscape = 0;
+$iOS::constant::Portrait = 1;
+$iOS::constant::ResolutionFull = 0;
+$iOS::constant::ResolutionSmall = 1;
+
+$iOS::constant::iPhoneWidth = 480;
+$iOS::constant::iPhoneHeight = 320;
+
+$iOS::constant::iPhone4Width = 960;
+$iOS::constant::iPhone4Height = 640;
+
+$iOS::constant::iPadWidth = 1024;
+$iOS::constant::iPadHeight = 768;
+
+$iOS::constant::NewiPadWidth = 2048;
+$iOS::constant::NewiPadHeight = 1536;
+
+$iOS::constant::iPhone5Width = 1136;
+$iOS::constant::iPhone5Height = 640;
+
+$iOS::constant::OrientationUnknown = 0;
+$iOS::constant::OrientationLandscapeLeft = 1;
+$iOS::constant::OrientationLandscapeRight = 2;
+$iOS::constant::OrientationPortrait = 3;
+$iOS::constant::OrientationPortraitUpsideDown = 4;
\ No newline at end of file
diff --git a/PlanetX/AppCore/1/scripts/defaultPreferences.cs b/PlanetX/AppCore/1/scripts/defaultPreferences.cs
new file mode 100644
index 000000000..c4a015b5e
--- /dev/null
+++ b/PlanetX/AppCore/1/scripts/defaultPreferences.cs
@@ -0,0 +1,88 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2013 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+
+/// Game
+$Game::CompanyName = "Torque Game Engines";
+$Game::ProductName = "Torque2D";
+
+/// iOS
+$pref::iOS::ScreenOrientation = $iOS::constant::Landscape;
+$pref::iOS::ScreenDepth = 32;
+$pref::iOS::UseGameKit = 0;
+$pref::iOS::UseMusic = 0;
+$pref::iOS::UseMoviePlayer = 0;
+$pref::iOS::UseAutoRotate = 1;
+$pref::iOS::EnableOrientationRotation = 1;
+$pref::iOS::EnableOtherOrientationRotation = 1;
+$pref::iOS::StatusBarType = 0;
+
+/// AppCore. Which theme's cursors are installed under the names the engine
+/// looks up when a control names none of its own - DefaultCursor, EditCursor
+/// and the rest (see gui/guiCursors.cs). Empty is the usual answer: a project
+/// with one theme uses it, and one with several is asked to say which.
+$pref::AppCore::cursorTheme = "";
+
+/// T2D
+$pref::T2D::ParticlePlayerEmissionRateScale = 1.0;
+$pref::T2D::ParticlePlayerSizeScale = 1.0;
+$pref::T2D::ParticlePlayerForceScale = 1.0;
+$pref::T2D::ParticlePlayerTimeScale = 1.0;
+$pref::T2D::warnFileDeprecated = 1;
+$pref::T2D::warnSceneOccupancy = 1;
+$pref::T2D::imageAssetGlobalFilterMode = Bilinear;
+$pref::T2D::TAMLSchema="";
+$pref::T2D::JSONStrict = 1;
+
+/// Video
+$pref::Video::appliedPref = 0;
+$pref::Video::displayDevice = "OpenGL";
+$pref::Video::preferOpenGL = 1;
+$pref::Video::fullScreen = 0;
+$pref::Video::defaultResolution = "1024 768";
+$pref::Video::windowedRes = "1024 768 32";
+$pref::OpenGL::gammaCorrection = 0.5;
+
+/// Fonts. The project's one font-cache folder, shared by every theme it holds --
+/// a cache is keyed by face and size alone, so a second location could only hold
+/// a duplicate of what the first already has. This is where the GUI Profile
+/// Editor bakes the caches for the fonts a theme uses, and what a profile naming
+/// no directory of its own falls back to. It sits beside the themes rather than
+/// inside this module, because the themes are the project's while AppCore is
+/// boilerplate a project starts from. (AppCore's own legacy profiles in
+/// gui/guiProfiles.cs name ^AppCore/fonts explicitly -- that is where their
+/// bundled caches ship, and they are unaffected by this.)
+///
+/// Derived from where this module sits (/AppCore/), the same
+/// way the editor finds the themes folder, so renaming or moving the project
+/// keeps working. Expanded, not left as a ^AppCore expando: the resource manager
+/// does not resolve expandos for font cache lookups.
+%appCoreModule = ModuleDatabase.findModule( "AppCore", 1 );
+$Gui::fontCacheDirectory = isObject( %appCoreModule ) ?
+ pathConcat( filePath( filePath( makeFullPath( %appCoreModule.getModulePath(), getMainDotCsDir() ) ) ), "themes/fonts" ) :
+ expandPath( "^AppCore/fonts" );
+
+/// Generic fallback font (a .ttf rasterized by FreeType) used by platforms that
+/// have no system fonts to synthesize a missing face/size from -- currently the
+/// web (Emscripten) build. Kept inside AppCore so a shipped game is self-contained
+/// (nothing here depends on the removable editor/). The editor registers its OWN
+/// copy of this var when it loads (editor/EditorCore/scripts/defaultPreferences.cs).
+$pref::Web::fallbackFont = expandPath( "^AppCore/fonts/Roboto-Regular.ttf" );
diff --git a/PlanetX/AppCore/1/scripts/themes.cs b/PlanetX/AppCore/1/scripts/themes.cs
new file mode 100644
index 000000000..1a61994d0
--- /dev/null
+++ b/PlanetX/AppCore/1/scripts/themes.cs
@@ -0,0 +1,262 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2013 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+
+/// The project's GUI skin. A GuiProfileTheme derives a complete set of control
+/// profiles from six colors, three fonts and a border size, and the GUI Profile
+/// Editor writes one .taml per theme into /themes. This is where a
+/// running game reads them back, so what the editor stamped into a .gui is what
+/// renders.
+///
+/// The folder sits beside AppCore rather than inside it because the themes are
+/// the project's, while AppCore is boilerplate the project started from and
+/// replaces wholesale when it updates.
+
+/// The face a generated theme starts on. A theme normally names its own face and
+/// ships baked glyph caches beside it, which is what makes it render the same
+/// everywhere; a theme built here has no caches of its own, so it asks for
+/// something the platform can actually supply.
+function AppCore::SetProfileFont(%this)
+{
+ if ($platform $= "windows")
+ %this.platformFontType = "share tech mono";
+ else if ($platform $= "Android")
+ // "Droid" is gone from modern Android (Roboto since ~2014); request "Roboto",
+ // which is the system face AND the bundled assets/fonts/Roboto-Regular.ttf.
+ %this.platformFontType = "Roboto";
+ else if ($platformUnixType $= "emscripten")
+ // Web build: the browser has no system fonts and there's no font backend,
+ // so use a face that ships a pre-baked .uft glyph cache ("share tech mono"
+ // at sizes 12/14/16/18/24 under ^AppCore/fonts). $platform is "x86UNIX" on
+ // the web build (same as desktop Linux), so key off $platformUnixType.
+ %this.platformFontType = "share tech mono";
+ else
+ %this.platformFontType = "monaco";
+}
+
+function AppCore::getThemesPath(%this)
+{
+ %module = ModuleDatabase.findModule("AppCore", 1);
+ if(!isObject(%module))
+ {
+ return "";
+ }
+
+ // /AppCore/ -> /themes. The same derivation
+ // $Gui::fontCacheDirectory uses (scripts/defaultPreferences.cs).
+ return pathConcat(filePath(filePath(makeFullPath(%module.getModulePath(), getMainDotCsDir()))), "themes");
+}
+
+/// Where one theme keeps its cursor art. A folder per theme, because two themes
+/// in the same project may want cursors that look nothing alike - a menu
+/// pointer and a combat reticle - and sharing one folder would mean one
+/// overwriting the other.
+function AppCore::getThemeCursorsPath(%this, %theme)
+{
+ %path = %this.getThemesPath();
+ if(%path $= "" || !isObject(%theme) || %theme.getName() $= "")
+ {
+ return "";
+ }
+ return pathConcat(%path, "cursors", %theme.getName());
+}
+
+/// The stock cursor art every theme starts from, inside AppCore itself. It is
+/// grayscale on purpose so a theme's tint colors it.
+function AppCore::getStockCursorsPath(%this)
+{
+ %module = ModuleDatabase.findModule("AppCore", 1);
+ if(!isObject(%module))
+ {
+ return "";
+ }
+ return pathConcat(makeFullPath(%module.getModulePath(), getMainDotCsDir()), "gui/images/cursors");
+}
+
+/// Give %theme its own copy of the stock cursor art and point it at the folder.
+/// Idempotent: pathCopy is asked not to overwrite, so a theme whose art is
+/// already there (or has been edited) is left exactly as it is.
+///
+/// Only the folder being absent triggers the copy, which keeps boot down to one
+/// directory test per theme - and means Android, where pathCopy is unsupported,
+/// never reaches it in a project that shipped its art.
+function AppCore::seedThemeCursors(%this, %theme)
+{
+ %target = %this.getThemeCursorsPath(%theme);
+ if(%target $= "")
+ {
+ return false;
+ }
+
+ // isDirectory rather than isFile: isFile answers out of the resource
+ // manager, which knows nothing about files written after the last scan.
+ if(!isDirectory(%target))
+ {
+ %source = %this.getStockCursorsPath();
+ if(%source $= "" || !isDirectory(%source))
+ {
+ warn("AppCore::seedThemeCursors: no stock cursor art at " @ %source @ ".");
+ return false;
+ }
+
+ createPath(%target @ "/");
+
+ %categories = %theme.getCursorCategoryNames();
+ for(%i = 0; %i < getWordCount(%categories); %i++)
+ {
+ %file = %theme.getCursorStockFile(getWord(%categories, %i));
+ if(%file $= "")
+ {
+ continue;
+ }
+ pathCopy(pathConcat(%source, %file), pathConcat(%target, %file));
+ }
+ }
+
+ // Assigning the folder restamps the theme, which fills in the bitmap of any
+ // cursor that has none yet. A cursor already pointing at art keeps it.
+ %directory = makeRelativePath(%target, getMainDotCsDir());
+ if(%theme.cursorDirectory !$= %directory)
+ {
+ %theme.cursorDirectory = %directory;
+ }
+
+ return true;
+}
+
+function AppCore::loadThemes(%this)
+{
+ %path = %this.getThemesPath();
+ if(%path $= "")
+ {
+ warn("AppCore::loadThemes: could not locate the AppCore module, so the project's themes were not loaded.");
+ return;
+ }
+ createPath(%path @ "/");
+
+ %found = false;
+ %pattern = %path @ "/*.taml";
+ for(%file = findFirstFile(%pattern); %file !$= ""; %file = findNextFile(%pattern))
+ {
+ if(%this.loadTheme(%file))
+ {
+ %found = true;
+ }
+ }
+
+ // A project that has never had a theme - one made before themes existed, or
+ // a folder someone emptied. Give it the stock one rather than leaving every
+ // control on the engine's bare fallback profile.
+ if(!%found)
+ {
+ %this.createStockTheme(%path);
+ }
+}
+
+/// Reads one file from the themes folder. Alongside themes it may hold stand-alone
+/// profiles, which the Profile Editor writes as a one-profile bundle - a SimSet,
+/// or a SimGroup from older versions (and, older still, a bare profile). Loading
+/// is all these need: a profile registers under its own name, which is how a Gui
+/// finds it.
+function AppCore::loadTheme(%this, %file)
+{
+ %object = TAMLRead(%file);
+ if(!isObject(%object))
+ {
+ warn("AppCore::loadThemes: could not read " @ %file);
+ return false;
+ }
+
+ %class = %object.getClassName();
+ if(%class $= "GuiProfileTheme")
+ {
+ if(%object.getName() $= "")
+ {
+ warn("AppCore::loadThemes: skipping " @ %file @ " - the theme has no name (possibly a name collision).");
+ %object.delete();
+ return false;
+ }
+
+ %this.repairFontDirectory(%object);
+ %this.seedThemeCursors(%object);
+ return true;
+ }
+
+ // A bundle is a SimSet; SimGroup, which the older ones are, derives from it.
+ if(%object.isMemberOfClass("SimSet") || %class $= "GuiControlProfile")
+ {
+ return false;
+ }
+
+ warn("AppCore::loadThemes: skipping " @ %file @ " - not a theme or profile.");
+ %object.delete();
+ return false;
+}
+
+/// A project keeps every font cache in one folder beside its themes, so a theme
+/// naming any other folder is a theme that came from somewhere else - shipped
+/// with the library, or copied in from another project. Point it at this
+/// project's folder, in memory only: nothing is rewritten on disk, and the theme
+/// restamps its members so their fontDirectory follows.
+function AppCore::repairFontDirectory(%this, %theme)
+{
+ %directory = makeRelativePath(pathConcat(%this.getThemesPath(), "fonts"), getMainDotCsDir());
+ if(%theme.fontDirectory !$= %directory)
+ {
+ %theme.fontDirectory = %directory;
+ }
+}
+
+/// Builds the stock theme in code and writes it out, so the project ends up with
+/// the same editable file a new project is shipped. The palette is the one
+/// AppCore's hand-written profiles used for years; the face comes from the
+/// platform, since a generated theme has no baked font caches of its own to name.
+function AppCore::createStockTheme(%this, %path)
+{
+ %this.SetProfileFont();
+
+ %theme = new GuiProfileTheme("Base")
+ {
+ colorBackground = "43 43 43 255";
+ colorSurface = "81 92 102 255";
+ colorForeground = "224 224 224 255";
+ colorAccent = "54 135 196 255";
+ colorHighlight = "245 210 50 255";
+ colorWarning = "196 54 71 255";
+
+ fontBody = %this.platformFontType;
+ fontTitle = %this.platformFontType;
+ fontCode = %this.platformFontType;
+ fontDirectory = makeRelativePath(pathConcat(%path, "fonts"), getMainDotCsDir());
+ fontSize = 16;
+
+ borderSize = 1;
+ };
+
+ // Before the write, so the file records where the art went.
+ %this.seedThemeCursors(%theme);
+
+ %file = pathConcat(%path, "Base.taml");
+ TAMLWrite(%theme, %file);
+
+ echo("AppCore: no theme found, so the stock theme was written to " @ %file @ ".");
+ return %theme;
+}
diff --git a/PlanetX/Audio/1/audio.cs b/PlanetX/Audio/1/audio.cs
new file mode 100644
index 000000000..9c17f2831
--- /dev/null
+++ b/PlanetX/Audio/1/audio.cs
@@ -0,0 +1,119 @@
+function Audio::create(%this)
+{
+ if(OpenALInitDriver())
+ {
+ %this.MusicOn = true;
+ %this.SoundOn = true;
+ %this.setMasterVolume(1);
+ %this.SetMusicVolume(1);
+ %this.SetSoundVolume(1);
+ %this.CurrentSong = "";
+ }
+}
+
+function Audio::destroy( %this )
+{
+ alxStopAll();
+ OpenALShutdownDriver();
+}
+
+function Audio::setMasterVolume(%this, %volume)
+{
+ %this.MasterVolume = mClamp(%volume, 0, 1);
+ alxListenerf(AL_GAIN_LINEAR, %this.MasterVolume);
+}
+
+function Audio::SetMusicVolume(%this, %volume)
+{
+ %this.MusicVolume = mClamp(%volume, 0, 1);
+ alxSetChannelVolume(0, %this.MusicVolume);
+}
+
+function Audio::SetSoundVolume(%this, %volume)
+{
+ %this.SoundVolume = mClamp(%volume, 0,1);
+ alxSetChannelVolume(1, %this.SoundVolume);
+}
+
+function Audio::SetPitch(%this, %noise, %pitch)
+{
+ alxSourcef(%noise, AL_PITCH, %pitch);
+}
+
+function Audio::StopAllAndPlayMusic(%this, %song)
+{
+ if(%song !$= %this.CurrentSong)
+ {
+ alxStopAll();
+ %this.CurrentSong = %song;
+ if(%this.MusicOn)
+ {
+ cancel(%this.fadeMusicSchedule);
+ %this.Music = alxPlay(%song);
+ }
+ }
+}
+
+function Audio::PlayMusic(%this, %song)
+{
+ if(%song !$= %this.CurrentSong)
+ {
+ %this.CurrentSong = %song;
+ if(%this.MusicOn)
+ {
+ cancel(%this.fadeMusicSchedule);
+ alxStop(%this.Music);
+ %this.Music = alxPlay(%song);
+ }
+ }
+}
+
+function Audio::RestartMusic(%this)
+{
+ cancel(%this.fadeMusicSchedule);
+ alxStop(%this.Music);
+ %this.Music = alxPlay(%this.CurrentSong);
+}
+
+function Audio::StopMusic(%this)
+{
+ alxStop(%this.Music);
+}
+
+function Audio::PlaySound(%this, %name)
+{
+ if(%this.SoundOn)
+ {
+ %sound = alxPlay(%name);
+ }
+
+ return %sound;
+}
+
+function Audio::FadeMusicVolumeTo(%this, %time, %volume)
+{
+ //Time is in milliseconds
+ %volume = mClamp(%volume, 0,1);
+
+ if(%volume == %this.MusicVolume)
+ {
+ return;
+ }
+
+ %difference = %volume - %this.MusicVolume;
+ %rate = 50;
+ %steps = mCeil(%time / %rate);
+ %delta = %difference / %steps;
+ %this.fadeMusicSchedule = %this.schedule(%rate, "FadeMusicStep", %volume, %rate, %delta);
+}
+
+function Audio::FadeMusicStep(%this, %targetVolume, %rate, %delta)
+{
+ if(mAbs(%targetVolume - %this.MusicVolume) < mAbs(%delta))
+ {
+ %this.SetMusicVolume(%targetVolume);
+ return;
+ }
+ %this.SetMusicVolume(%this.MusicVolume + %delta);
+ %this.fadeMusicSchedule = %this.schedule(%rate, "FadeMusicStep", %targetVolume, %rate, %delta);
+}
diff --git a/PlanetX/Audio/1/module.taml b/PlanetX/Audio/1/module.taml
new file mode 100644
index 000000000..a9d56c458
--- /dev/null
+++ b/PlanetX/Audio/1/module.taml
@@ -0,0 +1,10 @@
+
+
diff --git a/PlanetX/PlanetXGame/game.cs b/PlanetX/PlanetXGame/game.cs
new file mode 100644
index 000000000..2d223f144
--- /dev/null
+++ b/PlanetX/PlanetXGame/game.cs
@@ -0,0 +1,459 @@
+//-----------------------------------------------------------------------------
+// PlanetX - a twin-stick demo game for Torque2D 4.0.
+//
+// Game flow: title -> playing -> won/lost -> title. This file is the module
+// singleton and owns ONLY the state machine and the two things that outlive a
+// single level: the title screen and the win/lose dialogs. Everything that
+// belongs to a running level lives on the PlanetXLevel object (%this.level),
+// which builds and tears down its own world. See TORQUE_SCRIPT.md.
+//-----------------------------------------------------------------------------
+
+function PlanetXGame::create(%this)
+{
+ // One class per file; each is named for the class it defines.
+ exec("./scripts/settings.cs");
+ exec("./gui/titleScreen.cs");
+ exec("./gui/upgradeScreen.cs");
+ exec("./gui/optionsScreen.cs");
+ exec("./gui/pauseMenu.cs");
+ exec("./gui/keyCapture.cs");
+ exec("./scripts/level.cs");
+ exec("./scripts/sceneWindow.cs");
+ exec("./scripts/camera.cs");
+ exec("./scripts/tileMap.cs");
+ exec("./scripts/barrier.cs");
+ exec("./scripts/rock.cs");
+ exec("./scripts/rocket.cs");
+ exec("./scripts/crystal.cs");
+ exec("./scripts/crosshair.cs");
+ exec("./scripts/player.cs");
+ exec("./scripts/weapon.cs");
+ exec("./scripts/blaster.cs");
+ exec("./scripts/bullet.cs");
+ exec("./scripts/upgrades.cs");
+ exec("./scripts/burst.cs");
+ exec("./scripts/deathFx.cs");
+ exec("./scripts/enemy.cs");
+ exec("./scripts/bug.cs");
+ exec("./scripts/brute.cs");
+ exec("./scripts/hud.cs");
+ exec("./scripts/input.cs");
+
+ // ScreenFade posts SwapComplete when a canvas transition finishes.
+ %this.startListening(ScreenFade);
+
+ // User settings (volumes, key bindings, aim mode). Created before any audio
+ // plays or any GUI reads a binding; its onAdd seeds defaults, loads saved prefs
+ // over them, and pushes the volume levels into the shared Audio module.
+ %this.settings = new ScriptObject() { class = "PlanetXSettings"; };
+
+ // The weapon-upgrade catalog and this run's upgrade state. A named session
+ // singleton (like PlanetXWindow/PlanetXScene) so any file can reach it; reset()
+ // at the start of each run clears it back to the stock blaster. Naming it is
+ // all it needs - the name is already the namespace upgrades.cs writes its
+ // methods in, so a class saying the same word again would say nothing.
+ new ScriptObject(PlanetXUpgrades);
+
+ // Two-player co-op starts off; the title's "START 2 PLAYERS" turns it on.
+ $PlanetX::twoPlayer = false;
+
+ // The title screen owns its own GUI; it lives for the whole session.
+ %this.titleScreen = new ScriptObject() { class = "PlanetXTitleScreen"; };
+
+ // The options window also lives for the whole session - it is opened from the
+ // title and from the pause menu, and refreshed from prefs on each open.
+ %this.optionsScreen = new ScriptObject() { class = "PlanetXOptionsScreen"; };
+
+ // Neutral placeholder shown while a level is being torn down and rebuilt.
+ %this.blankGui = new GuiControl()
+ {
+ Profile = "PlanetXEmptyProfile";
+ HorizSizing = "relative";
+ VertSizing = "relative";
+ Position = "0 0";
+ Extent = "1024 768";
+ };
+
+ // (Volume levels were applied by PlanetXSettings above.)
+ $PlanetX::state = "";
+ $PlanetX::paused = false;
+ %this.showTitle(true);
+ echo("PlanetX: title screen ready (" @ getEngineVersion() @ ")");
+}
+
+function PlanetXGame::destroy(%this)
+{
+ %this.stopListening(ScreenFade);
+
+ // Deleting the level cascades teardown of its whole world (see
+ // PlanetXLevel::onRemove); the title screen frees its own GUI.
+ if (isObject(%this.level))
+ %this.level.delete();
+ if (isObject(%this.titleScreen))
+ %this.titleScreen.delete();
+
+ // The upgrade picker replaces the old victory dialog; it and the catalog
+ // singleton outlive a level, so free them here.
+ if (isObject(%this.upgradeScreen))
+ %this.upgradeScreen.delete();
+ if (isObject(PlanetXUpgrades))
+ PlanetXUpgrades.delete();
+
+ if (isObject(%this.gameOverGui))
+ %this.gameOverGui.delete();
+ if (isObject(%this.blankGui))
+ %this.blankGui.delete();
+
+ // The options and pause dialogs outlive a level; free them here.
+ if (isObject(%this.optionsScreen))
+ %this.optionsScreen.delete();
+ if (isObject(%this.pauseMenu))
+ %this.pauseMenu.delete();
+
+ // Persist any last changes, then free the settings singleton.
+ if (isObject(%this.settings))
+ {
+ %this.settings.save();
+ %this.settings.delete();
+ }
+}
+
+/// Shared click for the menu and dialog buttons (their Command fields call this
+/// before the action, so the asset id lives in one place).
+function PlanetXGame::playClick(%this)
+{
+ Audio.PlaySound("PlanetXGame:uiClick");
+}
+
+//-----------------------------------------------------------------------------
+// PlanetX creates no GUI profiles of its own. Everything it wears comes from the
+// PlanetX theme (PlanetX/themes/PlanetX.taml, loaded by AppCore, edited in the
+// GUI Profile Editor) - including the two that are not just a look: the heat
+// bar's coral Progress variant and the key-capture control's focusable Empty
+// variant, both extra profiles inside the theme.
+//
+// Where a screen wants text at a different size from the theme's, the control
+// sets FontSizeAdjust (a multiplier on its profile's font size) rather than
+// cloning the profile. So retuning the theme's font size moves the whole game.
+//-----------------------------------------------------------------------------
+
+//-----------------------------------------------------------------------------
+// State transitions.
+//-----------------------------------------------------------------------------
+
+/// Show the title screen. %instant skips the fade (used on first boot when
+/// there is no previous canvas content to fade from).
+function PlanetXGame::showTitle(%this, %instant)
+{
+ // Leaving play for the title always clears the pause flag (Main Menu from the
+ // pause dialog lands here).
+ $PlanetX::paused = false;
+ $PlanetX::optionsOpen = false;
+ $PlanetX::state = "title";
+
+ %this.titleScreen.open(%instant);
+
+ Canvas.showCursor();
+ Audio.PlayMusic("PlanetXGame:planetfall");
+}
+
+/// A fresh run from the title screen always begins at level 1. %twoPlayer picks
+/// solo or co-op (the two START buttons pass it; see titleGui.gui.taml).
+function PlanetXGame::startGame(%this, %twoPlayer)
+{
+ if ($PlanetX::state $= "playing")
+ return;
+
+ $PlanetX::twoPlayer = %twoPlayer;
+
+ // A fresh run starts with the stock blaster - wipe last run's upgrades.
+ PlanetXUpgrades.reset();
+
+ %this.levelNum = 1;
+ %this.launchLevel();
+}
+
+/// Build and enter the current level (used by new runs, retries, and
+/// level-to-level advancement). Creating the PlanetXLevel builds its world in
+/// onAdd; we hold the object so deleting it later tears the world back down.
+function PlanetXGame::launchLevel(%this)
+{
+ $PlanetX::state = "playing";
+
+ %this.level = new ScriptObject()
+ {
+ class = "PlanetXLevel";
+ number = %this.levelNum;
+ };
+
+ ScreenFade.swapCanvas(PlanetXRoot, "48 0 34", 800);
+
+ echo("PlanetX: level" SPC %this.levelNum SPC "started -" SPC PlanetXScene.getCount() SPC "scene objects");
+}
+
+/// Crystal secured: tear the level down and build the next, harder one.
+function PlanetXGame::nextLevel(%this)
+{
+ %this.activeDialog.postEvent("dialogClose");
+
+ // Park the canvas on a blank control so the old level can be deleted.
+ Canvas.setContent(%this.blankGui);
+ $PlanetX::state = "";
+ %this.teardownLevel();
+
+ %this.levelNum++;
+ %this.launchLevel();
+}
+
+function PlanetXGame::returnToTitle(%this)
+{
+ if ($PlanetX::state $= "title")
+ return;
+
+ // Stop input immediately; the level itself is torn down once the fade
+ // has moved the canvas off it (see onSwapComplete).
+ if (isObject(%this.level))
+ %this.level.suspend();
+ %this.teardownPending = true;
+ %this.showTitle(false);
+}
+
+/// ScreenFade callback: a canvas swap finished.
+function PlanetXGame::onSwapComplete(%this)
+{
+ if (%this.teardownPending)
+ {
+ %this.teardownPending = false;
+ %this.teardownLevel();
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Pause and options. ESC during play raises the pause dialog instead of quitting
+// to the title. We keep $PlanetX::state == "playing" and freeze the world with a
+// separate $PlanetX::paused flag, so the gameplay loops (aim, footsteps, auto-
+// repair, co-op camera) keep rescheduling themselves and just skip their work
+// while paused - nothing has to be restarted on resume. See input.cs.
+//-----------------------------------------------------------------------------
+
+function PlanetXGame::pauseGame(%this)
+{
+ if ($PlanetX::state !$= "playing" || $PlanetX::paused)
+ return;
+
+ $PlanetX::paused = true;
+ %this.level.pause();
+
+ // Built once and reused; it lives until the game shuts down (see destroy).
+ if (!isObject(%this.pauseMenu))
+ %this.pauseMenu = new ScriptObject() { class = "PlanetXPauseMenu"; };
+
+ %this.activeDialog = %this.pauseMenu.dialog;
+ ScreenFade.openDialog(%this.pauseMenu.dialog, "48 0 34 220", 300);
+}
+
+/// Continue: drop the dialog and unfreeze exactly where we left off.
+function PlanetXGame::resumeGame(%this)
+{
+ if (!$PlanetX::paused)
+ return;
+
+ %this.pauseMenu.dialog.postEvent("dialogClose");
+ %this.level.resume();
+ $PlanetX::paused = false;
+}
+
+/// Open the options window over the title screen (Back closes it, revealing the
+/// title again).
+function PlanetXGame::openOptions(%this)
+{
+ %this.optionsContext = "title";
+ $PlanetX::optionsOpen = true;
+
+ %this.optionsScreen.refresh();
+ %this.activeDialog = %this.optionsScreen.dialog;
+ ScreenFade.openDialog(%this.optionsScreen.dialog, "48 0 34 220", 300);
+}
+
+/// Open the options window from the pause menu by SWAPPING the visible dialog, so
+/// the pause backdrop stays up and Back can swap straight back to the pause menu.
+function PlanetXGame::openOptionsFromPause(%this)
+{
+ %this.optionsContext = "pause";
+ $PlanetX::optionsOpen = true;
+
+ %this.optionsScreen.refresh();
+ %this.activeDialog.postEvent("dialogSwap", %this.optionsScreen.dialog);
+ %this.activeDialog = %this.optionsScreen.dialog;
+}
+
+/// Options Back: save the settings, then return to wherever it was opened from -
+/// the title (close the dialog) or the pause menu (swap back to it).
+function PlanetXGame::closeOptions(%this)
+{
+ $PlanetX::optionsOpen = false;
+ %this.settings.save();
+
+ if (%this.optionsContext $= "pause")
+ {
+ %this.activeDialog.postEvent("dialogSwap", %this.pauseMenu.dialog);
+ %this.activeDialog = %this.pauseMenu.dialog;
+ }
+ else
+ %this.activeDialog.postEvent("dialogClose");
+}
+
+//-----------------------------------------------------------------------------
+// Winning.
+//-----------------------------------------------------------------------------
+
+function PlanetXGame::onWin(%this)
+{
+ if ($PlanetX::state !$= "playing")
+ return;
+
+ Audio.PlaySound("PlanetXGame:crystalGet");
+
+ $PlanetX::state = "won";
+ %this.level.suspend();
+
+ // Offer upgrades in place of the old victory dialog: two cards solo, three in
+ // co-op (each player claims a different one). Build a fresh screen each win - the
+ // offered set changes - and free the previous one, whose close animation is long
+ // finished. The screen builds its dialog in onAdd from the fields set here.
+ if (isObject(%this.upgradeScreen))
+ %this.upgradeScreen.delete();
+
+ %needed = $PlanetX::twoPlayer ? 3 : 2;
+ %offered = PlanetXUpgrades.offer(%needed);
+ %this.upgradeScreen = new ScriptObject()
+ {
+ class = "PlanetXUpgradeScreen";
+ levelNum = %this.levelNum;
+ offered = %offered;
+ };
+
+ %this.activeDialog = %this.upgradeScreen.dialog;
+ ScreenFade.openDialog(%this.upgradeScreen.dialog, "48 0 34 220", 400);
+}
+
+//-----------------------------------------------------------------------------
+// Losing.
+//-----------------------------------------------------------------------------
+
+// A death holds for a beat before the game freezes and the game-over dialog opens,
+// so the death effect can play over the still-live world (suspend() would freeze
+// it). The co-op camera lingers on a downed player for the same beat, so a teammate
+// sees the effect before it eases to the survivor (see PlanetXCamera::isFramed).
+$PlanetX::DeathLingerMs = 1000;
+
+/// A player's health hit zero. In single-player that ends the run. In co-op the
+/// player is downed and out of play; a living teammate can revive them at the
+/// rocket (see camera.cs / PlanetXLevel::revivePlayer), and the run only ends
+/// once BOTH players are down.
+function PlanetXGame::onPlayerDown(%this, %player)
+{
+ if ($PlanetX::state !$= "playing")
+ return;
+
+ // Single-player: a death is game over.
+ if (!$PlanetX::twoPlayer)
+ {
+ %this.onPlayerDeath(%player);
+ return;
+ }
+
+ // Co-op: take this player out of play.
+ Audio.PlaySound("PlanetXGame:playerDeathBurst");
+ %player.playDeathFx();
+ %player.goDown();
+
+ // If the teammate is already down too, the run is over.
+ if (%player.playerIndex == 1)
+ %mate = %this.level.player2;
+ else
+ %mate = %this.level.player;
+
+ if (!isObject(%mate) || %mate.downed)
+ %this.onPlayerDeath(%player);
+}
+
+/// Terminal game over: the fallen player bursts, then after a short beat - so the
+/// death effect plays over the still-live world - the level freezes and the game-over
+/// dialog opens. onPlayerDown routes here for a single-player death and for the
+/// second co-op down.
+function PlanetXGame::onPlayerDeath(%this, %player)
+{
+ if ($PlanetX::state !$= "playing")
+ return;
+
+ $PlanetX::state = "lost";
+
+ // Burst/hide the player that just fell - unless co-op already downed them (their
+ // effect played back in onPlayerDown). Quiet the body so it can't drift or loose
+ // ghost bolts off-screen during the hold before the dialog.
+ if (isObject(%player) && !%player.downed)
+ {
+ Audio.PlaySound("PlanetXGame:playerDeathBurst");
+ %player.playDeathFx();
+ %player.stopFiring();
+ %player.setLinearVelocity(0, 0);
+ %player.setVisible(false);
+ }
+
+ // Hold on the death, then freeze and prompt. Cancelled/guarded in case a teardown
+ // (a quick quit-to-title) beats the timer - see showGameOver and teardownLevel.
+ %this.gameOverEvent = %this.schedule($PlanetX::DeathLingerMs, "showGameOver");
+}
+
+/// Freeze the level and raise the game-over dialog, a beat after onPlayerDeath. Bails
+/// if the hold was cut short - a retry or a quit tore the level down first.
+function PlanetXGame::showGameOver(%this)
+{
+ if ($PlanetX::state !$= "lost" || !isObject(%this.level))
+ return;
+
+ %this.level.suspend();
+
+ if (!isObject(%this.gameOverGui))
+ %this.gameOverGui = TamlRead(expandPath("^PlanetXGame/gui/gameOverGui.gui.taml"));
+
+ %this.activeDialog = %this.gameOverGui;
+ ScreenFade.openDialog(%this.gameOverGui, "48 0 34 220", 400);
+}
+
+/// Death retry: replay the CURRENT level (fresh seed, same difficulty).
+function PlanetXGame::retryMission(%this)
+{
+ %this.activeDialog.postEvent("dialogClose");
+
+ // Park the canvas on a blank control so the old level can be deleted,
+ // then rebuild from scratch.
+ Canvas.setContent(%this.blankGui);
+ $PlanetX::state = "";
+ %this.teardownLevel();
+ %this.launchLevel();
+}
+
+function PlanetXGame::dialogToTitle(%this)
+{
+ %this.activeDialog.postEvent("dialogClose");
+ %this.teardownPending = true;
+ %this.showTitle(false);
+}
+
+//-----------------------------------------------------------------------------
+
+/// Destroy the current level. One delete cascades through PlanetXLevel::onRemove
+/// to free the scene, HUD, input, and every object they own.
+function PlanetXGame::teardownLevel(%this)
+{
+ // A death-hold timer may still be pending (player died, then quit before the
+ // dialog) - drop it so it can't fire suspend() into a torn-down level.
+ if (isEventPending(%this.gameOverEvent))
+ cancel(%this.gameOverEvent);
+
+ if (isObject(%this.level))
+ %this.level.delete();
+ %this.level = "";
+}
diff --git a/PlanetX/PlanetXGame/gui/gameOverGui.gui.taml b/PlanetX/PlanetXGame/gui/gameOverGui.gui.taml
new file mode 100644
index 000000000..5af4b3bd8
--- /dev/null
+++ b/PlanetX/PlanetXGame/gui/gameOverGui.gui.taml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
diff --git a/PlanetX/PlanetXGame/gui/images/planetXBG.image.taml b/PlanetX/PlanetXGame/gui/images/planetXBG.image.taml
new file mode 100644
index 000000000..535505c9c
--- /dev/null
+++ b/PlanetX/PlanetXGame/gui/images/planetXBG.image.taml
@@ -0,0 +1,3 @@
+
diff --git a/PlanetX/PlanetXGame/gui/images/planetXBG.png b/PlanetX/PlanetXGame/gui/images/planetXBG.png
new file mode 100644
index 000000000..d29d12841
Binary files /dev/null and b/PlanetX/PlanetXGame/gui/images/planetXBG.png differ
diff --git a/PlanetX/PlanetXGame/gui/images/upgradePlaceholder.image.taml b/PlanetX/PlanetXGame/gui/images/upgradePlaceholder.image.taml
new file mode 100644
index 000000000..b76514d18
--- /dev/null
+++ b/PlanetX/PlanetXGame/gui/images/upgradePlaceholder.image.taml
@@ -0,0 +1,4 @@
+
diff --git a/PlanetX/PlanetXGame/gui/images/upgradePlaceholder.png b/PlanetX/PlanetXGame/gui/images/upgradePlaceholder.png
new file mode 100644
index 000000000..adbb1d904
Binary files /dev/null and b/PlanetX/PlanetXGame/gui/images/upgradePlaceholder.png differ
diff --git a/PlanetX/PlanetXGame/gui/images/upgrade_autorepair.image.taml b/PlanetX/PlanetXGame/gui/images/upgrade_autorepair.image.taml
new file mode 100644
index 000000000..cf21d0b57
--- /dev/null
+++ b/PlanetX/PlanetXGame/gui/images/upgrade_autorepair.image.taml
@@ -0,0 +1,4 @@
+
diff --git a/PlanetX/PlanetXGame/gui/images/upgrade_autorepair.png b/PlanetX/PlanetXGame/gui/images/upgrade_autorepair.png
new file mode 100644
index 000000000..199c5984f
Binary files /dev/null and b/PlanetX/PlanetXGame/gui/images/upgrade_autorepair.png differ
diff --git a/PlanetX/PlanetXGame/gui/images/upgrade_damage.image.taml b/PlanetX/PlanetXGame/gui/images/upgrade_damage.image.taml
new file mode 100644
index 000000000..d4ff81f10
--- /dev/null
+++ b/PlanetX/PlanetXGame/gui/images/upgrade_damage.image.taml
@@ -0,0 +1,4 @@
+
diff --git a/PlanetX/PlanetXGame/gui/images/upgrade_damage.png b/PlanetX/PlanetXGame/gui/images/upgrade_damage.png
new file mode 100644
index 000000000..5b63a7fdd
Binary files /dev/null and b/PlanetX/PlanetXGame/gui/images/upgrade_damage.png differ
diff --git a/PlanetX/PlanetXGame/gui/images/upgrade_firerate.image.taml b/PlanetX/PlanetXGame/gui/images/upgrade_firerate.image.taml
new file mode 100644
index 000000000..ab61692ed
--- /dev/null
+++ b/PlanetX/PlanetXGame/gui/images/upgrade_firerate.image.taml
@@ -0,0 +1,4 @@
+
diff --git a/PlanetX/PlanetXGame/gui/images/upgrade_firerate.png b/PlanetX/PlanetXGame/gui/images/upgrade_firerate.png
new file mode 100644
index 000000000..56d8799cb
Binary files /dev/null and b/PlanetX/PlanetXGame/gui/images/upgrade_firerate.png differ
diff --git a/PlanetX/PlanetXGame/gui/images/upgrade_lessheat.image.taml b/PlanetX/PlanetXGame/gui/images/upgrade_lessheat.image.taml
new file mode 100644
index 000000000..29ce9e499
--- /dev/null
+++ b/PlanetX/PlanetXGame/gui/images/upgrade_lessheat.image.taml
@@ -0,0 +1,4 @@
+
diff --git a/PlanetX/PlanetXGame/gui/images/upgrade_lessheat.png b/PlanetX/PlanetXGame/gui/images/upgrade_lessheat.png
new file mode 100644
index 000000000..8137cd851
Binary files /dev/null and b/PlanetX/PlanetXGame/gui/images/upgrade_lessheat.png differ
diff --git a/PlanetX/PlanetXGame/gui/images/upgrade_maxheat.image.taml b/PlanetX/PlanetXGame/gui/images/upgrade_maxheat.image.taml
new file mode 100644
index 000000000..1a0fe30a3
--- /dev/null
+++ b/PlanetX/PlanetXGame/gui/images/upgrade_maxheat.image.taml
@@ -0,0 +1,4 @@
+
diff --git a/PlanetX/PlanetXGame/gui/images/upgrade_maxheat.png b/PlanetX/PlanetXGame/gui/images/upgrade_maxheat.png
new file mode 100644
index 000000000..d3493e826
Binary files /dev/null and b/PlanetX/PlanetXGame/gui/images/upgrade_maxheat.png differ
diff --git a/PlanetX/PlanetXGame/gui/images/upgrade_selfdestruct.image.taml b/PlanetX/PlanetXGame/gui/images/upgrade_selfdestruct.image.taml
new file mode 100644
index 000000000..0c01573b5
--- /dev/null
+++ b/PlanetX/PlanetXGame/gui/images/upgrade_selfdestruct.image.taml
@@ -0,0 +1,4 @@
+
diff --git a/PlanetX/PlanetXGame/gui/images/upgrade_selfdestruct.png b/PlanetX/PlanetXGame/gui/images/upgrade_selfdestruct.png
new file mode 100644
index 000000000..0c100a58a
Binary files /dev/null and b/PlanetX/PlanetXGame/gui/images/upgrade_selfdestruct.png differ
diff --git a/PlanetX/PlanetXGame/gui/images/upgrade_split.image.taml b/PlanetX/PlanetXGame/gui/images/upgrade_split.image.taml
new file mode 100644
index 000000000..c344399d9
--- /dev/null
+++ b/PlanetX/PlanetXGame/gui/images/upgrade_split.image.taml
@@ -0,0 +1,4 @@
+
diff --git a/PlanetX/PlanetXGame/gui/images/upgrade_split.png b/PlanetX/PlanetXGame/gui/images/upgrade_split.png
new file mode 100644
index 000000000..18b1a5227
Binary files /dev/null and b/PlanetX/PlanetXGame/gui/images/upgrade_split.png differ
diff --git a/PlanetX/PlanetXGame/gui/images/upgrade_tighten.image.taml b/PlanetX/PlanetXGame/gui/images/upgrade_tighten.image.taml
new file mode 100644
index 000000000..8e5ef0cea
--- /dev/null
+++ b/PlanetX/PlanetXGame/gui/images/upgrade_tighten.image.taml
@@ -0,0 +1,4 @@
+
diff --git a/PlanetX/PlanetXGame/gui/images/upgrade_tighten.png b/PlanetX/PlanetXGame/gui/images/upgrade_tighten.png
new file mode 100644
index 000000000..57e995dde
Binary files /dev/null and b/PlanetX/PlanetXGame/gui/images/upgrade_tighten.png differ
diff --git a/PlanetX/PlanetXGame/gui/images/upgrade_ventfast.image.taml b/PlanetX/PlanetXGame/gui/images/upgrade_ventfast.image.taml
new file mode 100644
index 000000000..66ae080b2
--- /dev/null
+++ b/PlanetX/PlanetXGame/gui/images/upgrade_ventfast.image.taml
@@ -0,0 +1,4 @@
+
diff --git a/PlanetX/PlanetXGame/gui/images/upgrade_ventfast.png b/PlanetX/PlanetXGame/gui/images/upgrade_ventfast.png
new file mode 100644
index 000000000..0b32c6b7c
Binary files /dev/null and b/PlanetX/PlanetXGame/gui/images/upgrade_ventfast.png differ
diff --git a/PlanetX/PlanetXGame/gui/keyCapture.cs b/PlanetX/PlanetXGame/gui/keyCapture.cs
new file mode 100644
index 000000000..88af075b4
--- /dev/null
+++ b/PlanetX/PlanetXGame/gui/keyCapture.cs
@@ -0,0 +1,65 @@
+//-----------------------------------------------------------------------------
+// PlanetXKeyCapture: the "press a key" capture control for rebinding. The options
+// screen raises one (PlanetXOptionsScreen::captureKey) when a rebind button is
+// clicked. It is a GuiInputCtrl - the engine's raw-input control, which on wake
+// mouse-locks and becomes first responder, then reports the next non-modifier press
+// through onInputEvent(device, action, make) with strings ActionMap.bind accepts.
+//
+// The options screen (the spawner) sets the fields this control needs: the target
+// pref key and the modal overlay the control lives in. On a key it writes the pref
+// (swapping with any action that already holds that key), asks the options screen to
+// repaint its labels, and tears the overlay down. GuiInputCtrl does not render its
+// children, which is why the visible prompt is a sibling in the overlay, not a child
+// of the control. See TORQUE_SCRIPT.md.
+//-----------------------------------------------------------------------------
+
+/// GuiInputCtrl callback. %make is "1" on press, "0" on a (modifier) release; bare
+/// modifiers never arrive as a press, which suits us - the movement/fire map is
+/// non-modifier keys.
+function PlanetXKeyCapture::onInputEvent(%this, %device, %action, %make)
+{
+ if (%make !$= "1")
+ return;
+
+ // Escape cancels the rebind, leaving the binding unchanged.
+ if (%action $= "escape")
+ {
+ %this.finish();
+ return;
+ }
+
+ // Only keyboard keys are bindable (the map is built with bind("keyboard", ...)).
+ if (%device !$= "keyboard")
+ return;
+
+ %settings = PlanetXGame.settings;
+
+ // If another action already holds this key, SWAP: hand that action the key we're
+ // replacing, so no two actions ever share one key.
+ %conflict = %settings.actionForKey(%action, %this.prefKey);
+ if (%conflict !$= "")
+ %settings.set(%conflict, %settings.get(%this.prefKey));
+
+ %settings.set(%this.prefKey, %action);
+ $PlanetX::bindingsDirty = true;
+
+ // Repaint every binding label - this button and any swapped one.
+ PlanetXGame.optionsScreen.refresh();
+
+ // Apply immediately if a level is live (rebinding from the pause menu); from the
+ // title there is no map yet - the next level builds it from these prefs.
+ if (isObject(PlanetXGame.level) && isObject(PlanetXGame.level.input))
+ PlanetXGame.level.input.rebuildMoveMap();
+
+ %this.finish();
+}
+
+/// Tear the overlay down NEXT tick and from the OPTIONS SCREEN, not from here: we
+/// are inside this control's own onInputEvent, and the overlay owns this control, so
+/// popping/deleting it synchronously (or scheduling the delete on the overlay
+/// itself) frees %this while its callback is still on the stack - which trips the
+/// engine's "deleted whilst performing a script callback" guard.
+function PlanetXKeyCapture::finish(%this)
+{
+ PlanetXGame.optionsScreen.schedule(1, "closeCapture", %this.overlay);
+}
diff --git a/PlanetX/PlanetXGame/gui/optionsScreen.cs b/PlanetX/PlanetXGame/gui/optionsScreen.cs
new file mode 100644
index 000000000..26d5c50e8
--- /dev/null
+++ b/PlanetX/PlanetXGame/gui/optionsScreen.cs
@@ -0,0 +1,334 @@
+//-----------------------------------------------------------------------------
+// PlanetXOptionsScreen: the options window - sound volumes, per-player key
+// bindings, and a per-player aim-mode toggle. A manager ScriptObject (same shape
+// as PlanetXUpgradeScreen): it builds one GuiControl dialog of controls in onAdd
+// and deletes it in onRemove, so the whole screen frees with a single delete.
+//
+// It is a session singleton held by PlanetXGame (built once, reused). It opens
+// two ways: over the title screen (PlanetXGame::openOptions) and from the pause
+// menu (PlanetXGame::openOptionsFromPause, a dialog swap). refresh() reseeds every
+// slider and label from the current prefs on each open, since prefs can change
+// between openings. Back saves and returns to wherever it was opened from
+// (PlanetXGame::closeOptions).
+//
+// The volume sliders write straight to their $pref:: global (Variable) and apply
+// live through the Audio module (AltCommand); the key buttons open the key-capture
+// overlay (keyCapture.cs). See TORQUE_SCRIPT.md.
+//-----------------------------------------------------------------------------
+
+// Player column heading colors - the same green/red the hull bars and upgrade
+// cards use for the two players.
+$PlanetX::OptionsP1Color = "33 191 132 255";
+$PlanetX::OptionsP2Color = "246 75 72 255";
+
+function PlanetXOptionsScreen::onAdd(%this)
+{
+ %this.build();
+}
+
+/// The dialog and every control in it hang off %this.dialog, so one delete frees
+/// the whole screen. ScreenFade only ever removes the dialog from its backdrop; it
+/// never deletes it, so ownership stays here.
+function PlanetXOptionsScreen::onRemove(%this)
+{
+ if (isObject(%this.dialog))
+ %this.dialog.delete();
+}
+
+//-----------------------------------------------------------------------------
+// Construction.
+//-----------------------------------------------------------------------------
+
+function PlanetXOptionsScreen::build(%this)
+{
+ %w = 820;
+ %h = 660;
+
+ %this.dialog = new GuiControl()
+ {
+ Profile = "PlanetXWindowProfile";
+ HorizSizing = "center";
+ VertSizing = "center";
+ Position = ((1024 - %w) / 2) SPC ((768 - %h) / 2);
+ Extent = %w SPC %h;
+ };
+
+ %this.addLabel(0, 16, %w, 40, "OPTIONS", "PlanetXLabelProfile", "center", "", 2);
+
+ // --- Sound ---------------------------------------------------------------
+ %this.addLabel(60, 64, 300, 26, "SOUND", "PlanetXLabelProfile", "left", "", 1.24);
+ %this.buildSlider(98, "MASTER", "MasterVolume", "Audio.setMasterVolume($pref::PlanetX::MasterVolume);");
+ %this.buildSlider(136, "MUSIC", "MusicVolume", "Audio.SetMusicVolume($pref::PlanetX::MusicVolume);");
+ %this.buildSlider(174, "SFX", "SoundVolume", "Audio.SetSoundVolume($pref::PlanetX::SoundVolume);");
+
+ // --- Controls ------------------------------------------------------------
+ %this.addLabel(60, 214, 300, 26, "CONTROLS", "PlanetXLabelProfile", "left", "", 1.24);
+
+ %p1x = 70;
+ %p2x = 440;
+
+ %this.buildColumnHeader(%p1x, "PLAYER 1", $PlanetX::OptionsP1Color);
+ %this.buildColumnHeader(%p2x, "PLAYER 2 (CO-OP)", $PlanetX::OptionsP2Color);
+
+ %this.buildAimToggle(%p1x, 288, "P1");
+ %this.buildAimToggle(%p2x, 288, "P2");
+
+ // One row per movement/fire action, both columns.
+ %actions = "Up" TAB "Down" TAB "Left" TAB "Right" TAB "Fire";
+ for (%i = 0; %i < getFieldCount(%actions); %i++)
+ {
+ %action = getField(%actions, %i);
+ %y = 328 + %i * 38;
+ %this.buildKeyRow(%p1x, %y, "P1", %action);
+ %this.buildKeyRow(%p2x, %y, "P2", %action);
+ }
+
+ // --- Back ----------------------------------------------------------------
+ %back = new GuiButtonCtrl()
+ {
+ Profile = "PlanetXButtonProfile";
+ FontSizeAdjust = 2;
+ HorizSizing = "center";
+ VertSizing = "bottom";
+ Position = ((%w - 260) / 2) SPC 588;
+ Extent = "260 50";
+ Text = "BACK";
+ Command = "PlanetXGame.playClick(); PlanetXGame.closeOptions();";
+ };
+ %this.dialog.add(%back);
+}
+
+/// Small helper: a plain text control added to the dialog. %color "" leaves the
+/// profile's default font color.
+// %fontAdjust multiplies the profile's font size (blank = leave it alone). It is
+// how this screen gets menu-sized and card-sized text out of the theme's one
+// Label profile, instead of cloning the profile per size.
+function PlanetXOptionsScreen::addLabel(%this, %x, %y, %w, %h, %text, %profile, %align, %color, %fontAdjust)
+{
+ %label = new GuiControl()
+ {
+ Profile = %profile;
+ HorizSizing = "right";
+ VertSizing = "bottom";
+ Position = %x SPC %y;
+ Extent = %w SPC %h;
+ Text = %text;
+ Align = %align;
+ };
+
+ if (%fontAdjust !$= "")
+ {
+ %label.FontSizeAdjust = %fontAdjust;
+ }
+
+ if (%color !$= "")
+ {
+ %label.OverrideFontColor = "1";
+ %label.FontColor = %color;
+ }
+
+ %this.dialog.add(%label);
+ return %label;
+}
+
+/// A volume row: a label plus a slider bound to $pref::PlanetX::. The
+/// slider writes the pref as it moves (Variable), applies live through the Audio
+/// module (AltCommand), and saves once on release (Command).
+function PlanetXOptionsScreen::buildSlider(%this, %y, %labelText, %prefKey, %audioCall)
+{
+ %this.addLabel(80, %y, 180, 26, %labelText, "PlanetXLabelProfile", "left", "", 1.24);
+
+ // The theme's Slider profile draws the groove; its thumb comes from the
+ // SliderThumb profile the control picks up by name.
+ %slider = new GuiSliderCtrl()
+ {
+ Profile = "PlanetXSliderProfile";
+ ThumbProfile = "PlanetXSliderThumbProfile";
+ HorizSizing = "right";
+ VertSizing = "bottom";
+ Position = "270" SPC (%y - 2);
+ Extent = "470 26";
+ Range = "0 1";
+ Ticks = "0";
+ Value = PlanetXGame.settings.get(%prefKey);
+ Variable = "$pref::PlanetX::" @ %prefKey;
+ AltCommand = %audioCall;
+ Command = "PlanetXGame.settings.save();";
+ };
+ %this.dialog.add(%slider);
+ %this.slider[%prefKey] = %slider;
+}
+
+/// A player column heading in that player's color.
+function PlanetXOptionsScreen::buildColumnHeader(%this, %colX, %text, %color)
+{
+ %this.addLabel(%colX + 8, 252, 300, 28, %text, "PlanetXLabelProfile", "left", %color, 1.24);
+}
+
+/// A rebind row: the action label plus a button showing the current key. Clicking
+/// the button raises the key-capture overlay (keyCapture.cs), which relabels it.
+function PlanetXOptionsScreen::buildKeyRow(%this, %colX, %y, %player, %action)
+{
+ %prefKey = %player @ %action;
+ %actionLabel = strupr(%action);
+
+ %this.addLabel(%colX + 8, %y, 100, 30, %actionLabel, "PlanetXLabelProfile", "left", "", 1.24);
+
+ %btn = new GuiButtonCtrl()
+ {
+ Profile = "PlanetXButtonProfile";
+ HorizSizing = "right";
+ VertSizing = "bottom";
+ Position = (%colX + 118) SPC %y;
+ Extent = "180 32";
+ Text = PlanetXGame.settings.keyLabel(PlanetXGame.settings.get(%prefKey));
+ prefKey = %prefKey;
+ actionLabel = %actionLabel;
+ };
+ %btn.Command = "PlanetXGame.playClick(); PlanetXGame.optionsScreen.captureKey(" @ %btn @ ");";
+
+ %this.dialog.add(%btn);
+ %this.keyButton[%prefKey] = %btn;
+}
+
+/// Raise the "press a key" overlay for a rebind button (which carries .prefKey and
+/// .actionLabel). We build the modal overlay and the PlanetXKeyCapture control that
+/// captures the next key; the control writes the pref, relabels the button, and
+/// tears this overlay down when it finishes (keyCapture.cs).
+function PlanetXOptionsScreen::captureKey(%this, %button)
+{
+ // The capture control wears PlanetXCaptureProfile, an Empty variant in the
+ // theme with canKeyFocus and tab turned on. GuiInputCtrl only becomes the
+ // keyboard first responder if its profile allows key focus
+ // (GuiControl::setFirstResponder is gated on canKeyFocus, which is off in
+ // every other profile), so without it the "press a key" prompt can never be
+ // answered or dismissed.
+
+ // Full-screen modal layer: blocks the options controls beneath and holds the
+ // prompt. Transparent itself - the panel inside carries the visible box.
+ %overlay = new GuiControl()
+ {
+ Profile = "PlanetXEmptyProfile";
+ HorizSizing = "relative";
+ VertSizing = "relative";
+ Position = "0 0";
+ Extent = "1024 768";
+ };
+
+ %panel = new GuiControl()
+ {
+ Profile = "PlanetXWindowProfile";
+ HorizSizing = "center";
+ VertSizing = "center";
+ Position = "312 324";
+ Extent = "400 120";
+ };
+ %overlay.add(%panel);
+
+ %prompt = new GuiControl()
+ {
+ Profile = "PlanetXLabelProfile";
+ FontSizeAdjust = 2;
+ HorizSizing = "center";
+ VertSizing = "center";
+ Position = "20 24";
+ Extent = "360 72";
+ Text = "PRESS A KEY FOR" SPC %button.actionLabel;
+ Align = "center";
+ TextWrap = "1";
+ };
+ %panel.add(%prompt);
+
+ // The capture control: invisible, full-screen, first responder on wake. It does
+ // not render children, so the prompt panel above is a sibling, not a child. The
+ // spawner sets the fields it controls; behavior lives in keyCapture.cs.
+ %input = new GuiInputCtrl()
+ {
+ class = "PlanetXKeyCapture";
+ Profile = "PlanetXCaptureProfile";
+ HorizSizing = "relative";
+ VertSizing = "relative";
+ Position = "0 0";
+ Extent = "1024 768";
+ prefKey = %button.prefKey;
+ overlay = %overlay;
+ };
+ %overlay.add(%input);
+
+ Canvas.pushDialog(%overlay);
+}
+
+/// Pop and free a key-capture overlay. Scheduled from PlanetXKeyCapture::finish so
+/// the delete runs a tick later, outside the capture control's own input callback
+/// (the overlay owns that control, so freeing it mid-callback would be a use-after-
+/// free the engine guards against).
+function PlanetXOptionsScreen::closeCapture(%this, %overlay)
+{
+ if (!isObject(%overlay))
+ return;
+
+ Canvas.popDialog(%overlay);
+ %overlay.delete();
+}
+
+/// The Mouse/Automatic aim toggle for a player.
+function PlanetXOptionsScreen::buildAimToggle(%this, %colX, %y, %player)
+{
+ %this.addLabel(%colX + 8, %y, 100, 30, "AIM", "PlanetXLabelProfile", "left", "", 1.24);
+
+ %btn = new GuiButtonCtrl()
+ {
+ Profile = "PlanetXButtonProfile";
+ HorizSizing = "right";
+ VertSizing = "bottom";
+ Position = (%colX + 118) SPC %y;
+ Extent = "180 32";
+ Text = strupr(PlanetXGame.settings.get(%player @ "Aim"));
+ Command = "PlanetXGame.playClick(); PlanetXGame.optionsScreen.toggleAim(\"" @ %player @ "\");";
+ };
+ %this.dialog.add(%btn);
+ %this.aimButton[%player] = %btn;
+}
+
+//-----------------------------------------------------------------------------
+// Live changes.
+//-----------------------------------------------------------------------------
+
+/// Flip a player's aim between mouse and automatic, relabel the button, and apply
+/// it live if a level is running (aim mode was toggled from the pause menu).
+function PlanetXOptionsScreen::toggleAim(%this, %player)
+{
+ %key = %player @ "Aim";
+ %mode = (PlanetXGame.settings.get(%key) $= "mouse") ? "auto" : "mouse";
+
+ PlanetXGame.settings.set(%key, %mode);
+ %this.aimButton[%player].setText(strupr(%mode));
+
+ if (isObject(PlanetXGame.level) && isObject(PlanetXGame.level.input))
+ PlanetXGame.level.input.applyAimModes();
+}
+
+/// Reseed every control from the current prefs. Called on each open, because the
+/// screen is a reused singleton and prefs may have changed since it was last shown.
+function PlanetXOptionsScreen::refresh(%this)
+{
+ %this.slider["MasterVolume"].setValue(PlanetXGame.settings.get("MasterVolume"));
+ %this.slider["MusicVolume"].setValue(PlanetXGame.settings.get("MusicVolume"));
+ %this.slider["SoundVolume"].setValue(PlanetXGame.settings.get("SoundVolume"));
+
+ %players = "P1" TAB "P2";
+ %actions = "Up" TAB "Down" TAB "Left" TAB "Right" TAB "Fire";
+
+ for (%p = 0; %p < getFieldCount(%players); %p++)
+ {
+ %player = getField(%players, %p);
+
+ for (%i = 0; %i < getFieldCount(%actions); %i++)
+ {
+ %key = %player @ getField(%actions, %i);
+ %this.keyButton[%key].setText(PlanetXGame.settings.keyLabel(PlanetXGame.settings.get(%key)));
+ }
+
+ %this.aimButton[%player].setText(strupr(PlanetXGame.settings.get(%player @ "Aim")));
+ }
+}
diff --git a/PlanetX/PlanetXGame/gui/pauseMenu.cs b/PlanetX/PlanetXGame/gui/pauseMenu.cs
new file mode 100644
index 000000000..6c405c4b8
--- /dev/null
+++ b/PlanetX/PlanetXGame/gui/pauseMenu.cs
@@ -0,0 +1,72 @@
+//-----------------------------------------------------------------------------
+// PlanetXPauseMenu: the "GAME PAUSED" dialog raised by Esc during play. A manager
+// ScriptObject (same shape as PlanetXUpgradeScreen): it builds one GuiControl
+// dialog in onAdd and deletes it in onRemove, so the whole menu frees with a single
+// delete. PlanetXGame builds it once (lazily) and reuses it.
+//
+// The buttons call back into PlanetXGame - the state machine. Continue resumes the
+// frozen level; Options swaps in the shared options window; Main Menu tears the
+// level down to the title (dialogToTitle, shared with the game-over dialog); Quit
+// exits. See game.cs and TORQUE_SCRIPT.md.
+//-----------------------------------------------------------------------------
+
+function PlanetXPauseMenu::onAdd(%this)
+{
+ %this.build();
+}
+
+function PlanetXPauseMenu::onRemove(%this)
+{
+ if (isObject(%this.dialog))
+ %this.dialog.delete();
+}
+
+function PlanetXPauseMenu::build(%this)
+{
+ %w = 480;
+ %h = 430;
+
+ %this.dialog = new GuiControl()
+ {
+ Profile = "PlanetXWindowProfile";
+ HorizSizing = "center";
+ VertSizing = "center";
+ Position = ((1024 - %w) / 2) SPC ((768 - %h) / 2);
+ Extent = %w SPC %h;
+ };
+
+ %heading = new GuiControl()
+ {
+ Profile = "PlanetXLabelProfile";
+ FontSizeAdjust = 2;
+ HorizSizing = "center";
+ VertSizing = "bottom";
+ Position = "0 30";
+ Extent = %w SPC 44;
+ Text = "GAME PAUSED";
+ Align = "center";
+ };
+ %this.dialog.add(%heading);
+
+ %this.addButton(110, "CONTINUE", "PlanetXGame.resumeGame();");
+ %this.addButton(180, "OPTIONS", "PlanetXGame.openOptionsFromPause();");
+ %this.addButton(250, "MAIN MENU", "PlanetXGame.dialogToTitle();");
+ %this.addButton(320, "QUIT", "quit();");
+}
+
+/// One centered menu button. Every button plays the shared click before its action.
+function PlanetXPauseMenu::addButton(%this, %y, %text, %action)
+{
+ %btn = new GuiButtonCtrl()
+ {
+ Profile = "PlanetXButtonProfile";
+ FontSizeAdjust = 2;
+ HorizSizing = "center";
+ VertSizing = "bottom";
+ Position = "100" SPC %y;
+ Extent = "280 52";
+ Text = %text;
+ Command = "PlanetXGame.playClick(); " @ %action;
+ };
+ %this.dialog.add(%btn);
+}
diff --git a/PlanetX/PlanetXGame/gui/titleGui.gui.taml b/PlanetX/PlanetXGame/gui/titleGui.gui.taml
new file mode 100644
index 000000000..66c665e39
--- /dev/null
+++ b/PlanetX/PlanetXGame/gui/titleGui.gui.taml
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
diff --git a/PlanetX/PlanetXGame/gui/titleScreen.cs b/PlanetX/PlanetXGame/gui/titleScreen.cs
new file mode 100644
index 000000000..45b728c73
--- /dev/null
+++ b/PlanetX/PlanetXGame/gui/titleScreen.cs
@@ -0,0 +1,37 @@
+//-----------------------------------------------------------------------------
+// PlanetXTitleScreen: the title menu, a manager ScriptObject that owns its own
+// GUI. It builds the GUI (layout in titleGui.gui.taml, plus the engine version
+// label TAML can't fill) in onAdd and frees it in onRemove. open()/close() show
+// and hide it. It lives for the whole session, so the game just calls open()
+// each time it returns to the title.
+//
+// The menu buttons' Command fields call back into PlanetXGame (startGame/quit),
+// which is the state machine - see titleGui.gui.taml.
+//-----------------------------------------------------------------------------
+
+function PlanetXTitleScreen::onAdd(%this)
+{
+ %this.gui = TamlRead(expandPath("^PlanetXGame/gui/titleGui.gui.taml"));
+ TitleVersionLabel.setText(getEngineVersion());
+}
+
+function PlanetXTitleScreen::onRemove(%this)
+{
+ if (isObject(%this.gui))
+ %this.gui.delete();
+}
+
+/// Show the title. %instant skips the fade (first boot, with no prior content).
+function PlanetXTitleScreen::open(%this, %instant)
+{
+ if (%instant)
+ Canvas.setContent(%this.gui);
+ else
+ ScreenFade.swapCanvas(%this.gui, "48 0 34", 800);
+}
+
+/// The canvas moves to the level (or the blank control) on its own when play
+/// starts, so there is nothing to tear down between showings.
+function PlanetXTitleScreen::close(%this)
+{
+}
diff --git a/PlanetX/PlanetXGame/gui/upgradeScreen.cs b/PlanetX/PlanetXGame/gui/upgradeScreen.cs
new file mode 100644
index 000000000..40189f5fa
--- /dev/null
+++ b/PlanetX/PlanetXGame/gui/upgradeScreen.cs
@@ -0,0 +1,336 @@
+//-----------------------------------------------------------------------------
+// PlanetXUpgradeScreen: the end-of-level upgrade picker that stands in for the old
+// victory dialog. A manager ScriptObject (same shape as PlanetXHud): it builds one
+// GuiControl dialog of upgrade cards in onAdd and deletes it in onRemove, so the
+// whole screen is freed with a single delete.
+//
+// The offered upgrade keys are handed in as %this.offered (a space-separated list
+// from PlanetXUpgrades::offer). Each card shows a 3:4 image, a title, and a short
+// description, with a CHOOSE/UNDO button beneath it. Selection is mouse-driven and
+// sequential: a free card is claimed by the next player who still needs a pick
+// (player 1, then player 2 in co-op); clicking a claimed card releases it (undo).
+// DEPLOY appears once everyone has chosen and advances to the next level.
+//
+// Player 1's color is green (#21bf84); player 2's is red (#f64b48) - the same colors
+// their hull bars use. See TORQUE_SCRIPT.md.
+//-----------------------------------------------------------------------------
+
+// Card art tints: a claimed card glows in its owner's color; a free card is white.
+$PlanetX::UpgradeFreeTint = "255 255 255 255";
+$PlanetX::UpgradeP1Tint = "90 230 170 255";
+$PlanetX::UpgradeP2Tint = "255 120 118 255";
+
+// Text colors matching the two players (used for titles, prompt, and badges).
+$PlanetX::UpgradeP1Color = "33 191 132 255";
+$PlanetX::UpgradeP2Color = "246 75 72 255";
+
+function PlanetXUpgradeScreen::onAdd(%this)
+{
+ %this.count = getWordCount(%this.offered);
+
+ // No card claimed by either player yet (-1 = this player hasn't chosen).
+ %this.pick[1] = -1;
+ %this.pick[2] = -1;
+
+ %this.build();
+ %this.refresh();
+}
+
+/// The dialog and every control in it hang off %this.dialog, so one delete frees the
+/// whole screen. ScreenFade only ever removes the dialog from its backdrop; it never
+/// deletes it, so ownership stays here.
+function PlanetXUpgradeScreen::onRemove(%this)
+{
+ if (isObject(%this.dialog))
+ %this.dialog.delete();
+}
+
+//-----------------------------------------------------------------------------
+// Construction.
+//-----------------------------------------------------------------------------
+
+function PlanetXUpgradeScreen::build(%this)
+{
+ %dialogW = 800;
+ %dialogH = 606;
+ %cardW = 232;
+ %cardH = 406;
+ %gap = 20;
+ %cardsY = 112;
+
+ // The cards are centered as a row, so two cards sit as neatly as three.
+ %cardsW = %this.count * %cardW + (%this.count - 1) * %gap;
+ %startX = (%dialogW - %cardsW) / 2;
+
+ %this.dialog = new GuiControl()
+ {
+ Profile = "PlanetXWindowProfile";
+ HorizSizing = "center";
+ VertSizing = "center";
+ Position = ((1024 - %dialogW) / 2) SPC ((768 - %dialogH) / 2);
+ Extent = %dialogW SPC %dialogH;
+ };
+
+ %heading = new GuiControl()
+ {
+ Profile = "PlanetXLabelProfile";
+ FontSizeAdjust = 2;
+ HorizSizing = "center";
+ VertSizing = "bottom";
+ Position = "0 24";
+ Extent = %dialogW SPC 44;
+ Text = "LEVEL" SPC %this.levelNum SPC "CLEARED";
+ Align = "center";
+ OverrideFontColor = "1";
+ FontColor = $PlanetX::UpgradeP1Color;
+ };
+ %this.dialog.add(%heading);
+
+ // Prompt line: whose turn it is, or that they are ready to deploy.
+ %this.prompt = new GuiControl()
+ {
+ Profile = "PlanetXLabelProfile";
+ FontSizeAdjust = 1.24;
+ HorizSizing = "center";
+ VertSizing = "bottom";
+ Position = "0 74";
+ Extent = %dialogW SPC 36;
+ Align = "center";
+ OverrideFontColor = "1";
+ };
+ %this.dialog.add(%this.prompt);
+
+ for (%i = 0; %i < %this.count; %i++)
+ {
+ %x = %startX + %i * (%cardW + %gap);
+ %this.buildCard(%i, getWord(%this.offered, %i), %x, %cardsY, %cardW, %cardH);
+ }
+
+ // DEPLOY is hidden until everyone has chosen (see refresh).
+ %this.deployButton = new GuiButtonCtrl()
+ {
+ Profile = "PlanetXButtonProfile";
+ FontSizeAdjust = 2;
+ HorizSizing = "center";
+ VertSizing = "bottom";
+ Position = ((%dialogW - 280) / 2) SPC 530;
+ Extent = "280 52";
+ Text = "DEPLOY";
+ Command = "PlanetXGame.playClick(); PlanetXGame.upgradeScreen.deploy();";
+ };
+ %this.dialog.add(%this.deployButton);
+}
+
+/// Build card %i for upgrade %key at (%x,%y). Stashes the pieces refresh() recolors.
+function PlanetXUpgradeScreen::buildCard(%this, %i, %key, %x, %y, %w, %h)
+{
+ %titleText = PlanetXUpgrades.title[%key];
+ %descText = PlanetXUpgrades.desc[%key];
+ %imgAsset = PlanetXUpgrades.image[%key];
+
+ %card = new GuiControl()
+ {
+ Profile = "PlanetXEmptyProfile";
+ HorizSizing = "right";
+ VertSizing = "bottom";
+ Position = %x SPC %y;
+ Extent = %w SPC %h;
+ };
+ %this.dialog.add(%card);
+
+ // Image: a 3:4 panel, centered near the top of the card.
+ %imgW = 168;
+ %imgH = 224;
+ %img = new GuiSpriteCtrl()
+ {
+ Profile = "PlanetXEmptyProfile";
+ HorizSizing = "right";
+ VertSizing = "bottom";
+ Position = ((%w - %imgW) / 2) SPC 8;
+ Extent = %imgW SPC %imgH;
+ Image = %imgAsset;
+ SingleFrameBitmap = "1";
+ FullSize = "1";
+ ConstrainProportions = "0";
+ ClampImage = "0";
+ ImageColor = $PlanetX::UpgradeFreeTint;
+ };
+ %card.add(%img);
+
+ // Title wraps to a second line for the longer names.
+ %title = new GuiControl()
+ {
+ Profile = "PlanetXLabelProfile";
+ FontSizeAdjust = 1.24;
+ HorizSizing = "right";
+ VertSizing = "bottom";
+ Position = "6 236";
+ Extent = (%w - 12) SPC 50;
+ Text = %titleText;
+ Align = "center";
+ TextWrap = "1";
+ };
+ %card.add(%title);
+
+ %desc = new GuiControl()
+ {
+ Profile = "PlanetXLabelProfile";
+ HorizSizing = "right";
+ VertSizing = "bottom";
+ Position = "12 290";
+ Extent = (%w - 24) SPC 72;
+ Text = %descText;
+ Align = "center";
+ TextWrap = "1";
+ };
+ %card.add(%desc);
+
+ %btn = new GuiButtonCtrl()
+ {
+ Profile = "PlanetXButtonProfile";
+ HorizSizing = "right";
+ VertSizing = "bottom";
+ Position = ((%w - 176) / 2) SPC 366;
+ Extent = "176 34";
+ Text = "CHOOSE";
+ Command = "PlanetXGame.playClick(); PlanetXGame.upgradeScreen.clickCard(" @ %i @ ");";
+ };
+ %card.add(%btn);
+
+ %this.cardKey[%i] = %key;
+ %this.cardOwner[%i] = 0;
+ %this.cardImage[%i] = %img;
+ %this.cardTitle[%i] = %title;
+ %this.cardButton[%i] = %btn;
+}
+
+//-----------------------------------------------------------------------------
+// Selection.
+//-----------------------------------------------------------------------------
+
+/// A card was clicked. Clicking your own card releases it (undo). Clicking a free
+/// card claims it for whoever is choosing - and if that player already holds another
+/// card, moves their pick to this one, so a solo player switches just by clicking a
+/// different card. One owner per card keeps the two co-op players on distinct upgrades.
+function PlanetXUpgradeScreen::clickCard(%this, %i)
+{
+ %owner = %this.cardOwner[%i];
+
+ if (%owner != 0)
+ {
+ // Undo: hand the card back.
+ %this.cardOwner[%i] = 0;
+ %this.pick[%owner] = -1;
+ }
+ else
+ {
+ // Whoever is choosing claims this card. Solo, that is always player 1, so a
+ // click on a different card just switches the pick. In co-op it is player 1
+ // until they have chosen, then player 2; once both have chosen, changing a
+ // pick means undoing it first (click your own card).
+ %who = 0;
+ if (%this.pick[1] == -1)
+ %who = 1;
+ else if ($PlanetX::twoPlayer && %this.pick[2] == -1)
+ %who = 2;
+ else if (!$PlanetX::twoPlayer)
+ %who = 1;
+
+ if (%who == 0)
+ return;
+
+ // Switching: release the card this player was holding before taking the new one.
+ if (%this.pick[%who] != -1)
+ %this.cardOwner[%this.pick[%who]] = 0;
+
+ %this.cardOwner[%i] = %who;
+ %this.pick[%who] = %i;
+ }
+
+ %this.refresh();
+}
+
+/// Have all the choosers (one solo, both in co-op) picked a card?
+function PlanetXUpgradeScreen::isReady(%this)
+{
+ if (%this.pick[1] == -1)
+ return false;
+ if ($PlanetX::twoPlayer && %this.pick[2] == -1)
+ return false;
+ return true;
+}
+
+/// Repaint every card to its owner state, update the prompt, and show DEPLOY once
+/// the picks are in.
+function PlanetXUpgradeScreen::refresh(%this)
+{
+ for (%i = 0; %i < %this.count; %i++)
+ {
+ %owner = %this.cardOwner[%i];
+ %title = PlanetXUpgrades.title[%this.cardKey[%i]];
+
+ if (%owner == 1)
+ {
+ %this.cardImage[%i].setImageColor($PlanetX::UpgradeP1Tint);
+ %this.colorText(%this.cardTitle[%i], %title, "1", $PlanetX::UpgradeP1Color);
+ %this.cardButton[%i].setText("UNDO");
+ }
+ else if (%owner == 2)
+ {
+ %this.cardImage[%i].setImageColor($PlanetX::UpgradeP2Tint);
+ %this.colorText(%this.cardTitle[%i], %title, "1", $PlanetX::UpgradeP2Color);
+ %this.cardButton[%i].setText("UNDO");
+ }
+ else
+ {
+ %this.cardImage[%i].setImageColor($PlanetX::UpgradeFreeTint);
+ %this.colorText(%this.cardTitle[%i], %title, "0", $PlanetX::UpgradeFreeTint);
+ %this.cardButton[%i].setText("CHOOSE");
+ }
+ }
+
+ %ready = %this.isReady();
+ if (%ready)
+ %this.setPrompt("READY TO DEPLOY", "255 255 255 255");
+ else if (%this.pick[1] == -1)
+ %this.setPrompt($PlanetX::twoPlayer ? "PLAYER 1 - CHOOSE" : "CHOOSE AN UPGRADE", $PlanetX::UpgradeP1Color);
+ else
+ %this.setPrompt("PLAYER 2 - CHOOSE", $PlanetX::UpgradeP2Color);
+
+ %this.deployButton.setVisible(%ready);
+}
+
+/// Recolor a text control. OverrideFontColor/FontColor only take effect on the next
+/// setText, so we re-set the text to force the repaint (same trick as PlanetXHud).
+function PlanetXUpgradeScreen::colorText(%this, %ctrl, %text, %override, %color)
+{
+ %ctrl.OverrideFontColor = %override;
+ %ctrl.FontColor = %color;
+ %ctrl.setText(%text);
+}
+
+function PlanetXUpgradeScreen::setPrompt(%this, %text, %color)
+{
+ %this.prompt.OverrideFontColor = "1";
+ %this.prompt.FontColor = %color;
+ %this.prompt.setText(%text);
+}
+
+//-----------------------------------------------------------------------------
+// Deploy.
+//-----------------------------------------------------------------------------
+
+/// Bank each player's chosen upgrade and advance. nextLevel closes this dialog (it
+/// posts dialogClose to the active dialog), tears the level down, and rebuilds - the
+/// new players re-apply the banked upgrades in their onAdd.
+function PlanetXUpgradeScreen::deploy(%this)
+{
+ if (!%this.isReady())
+ return;
+
+ PlanetXUpgrades.take(%this.cardKey[%this.pick[1]], 1);
+ if ($PlanetX::twoPlayer)
+ PlanetXUpgrades.take(%this.cardKey[%this.pick[2]], 2);
+
+ PlanetXGame.nextLevel();
+}
diff --git a/PlanetX/PlanetXGame/module.taml b/PlanetX/PlanetXGame/module.taml
new file mode 100644
index 000000000..d2c7ee18a
--- /dev/null
+++ b/PlanetX/PlanetXGame/module.taml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
diff --git a/PlanetX/PlanetXGame/music/planetfall.audio.taml b/PlanetX/PlanetXGame/music/planetfall.audio.taml
new file mode 100644
index 000000000..bd1c03f97
--- /dev/null
+++ b/PlanetX/PlanetXGame/music/planetfall.audio.taml
@@ -0,0 +1,8 @@
+
diff --git a/PlanetX/PlanetXGame/music/planetfall.ogg b/PlanetX/PlanetXGame/music/planetfall.ogg
new file mode 100644
index 000000000..8ee910163
Binary files /dev/null and b/PlanetX/PlanetXGame/music/planetfall.ogg differ
diff --git a/PlanetX/PlanetXGame/particles/bugDeath.particle.taml b/PlanetX/PlanetXGame/particles/bugDeath.particle.taml
new file mode 100644
index 000000000..8720008b9
--- /dev/null
+++ b/PlanetX/PlanetXGame/particles/bugDeath.particle.taml
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PlanetX/PlanetXGame/particles/playerDeath.particle.taml b/PlanetX/PlanetXGame/particles/playerDeath.particle.taml
new file mode 100644
index 000000000..0470d88fd
--- /dev/null
+++ b/PlanetX/PlanetXGame/particles/playerDeath.particle.taml
@@ -0,0 +1,203 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PlanetX/PlanetXGame/particles/steam.particle.taml b/PlanetX/PlanetXGame/particles/steam.particle.taml
new file mode 100644
index 000000000..24aece450
--- /dev/null
+++ b/PlanetX/PlanetXGame/particles/steam.particle.taml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PlanetX/PlanetXGame/scripts/barrier.cs b/PlanetX/PlanetXGame/scripts/barrier.cs
new file mode 100644
index 000000000..c012ede9f
--- /dev/null
+++ b/PlanetX/PlanetXGame/scripts/barrier.cs
@@ -0,0 +1,15 @@
+//-----------------------------------------------------------------------------
+// PlanetXBarrier: an invisible static wall. The level sets its position and
+// size; the barrier gives itself a matching box collision shape.
+//-----------------------------------------------------------------------------
+
+function PlanetXBarrier::onAdd(%this)
+{
+ %size = %this.getSize();
+ %width = getWord(%size, 0);
+ %height = getWord(%size, 1);
+
+ %this.setBodyType("static");
+ %this.setSceneGroup($PlanetX::WallGroup);
+ %this.createPolygonBoxCollisionShape(%width, %height);
+}
diff --git a/PlanetX/PlanetXGame/scripts/blaster.cs b/PlanetX/PlanetXGame/scripts/blaster.cs
new file mode 100644
index 000000000..4d264b13c
--- /dev/null
+++ b/PlanetX/PlanetXGame/scripts/blaster.cs
@@ -0,0 +1,19 @@
+//-----------------------------------------------------------------------------
+// PlanetXBlaster: the standard-issue blaster the spaceman lands with. A concrete
+// PlanetXWeapon - it runs the base weapon's setup, then dials in its own feel.
+// This is the template for adding a new gun: copy the file, give it a class,
+// and override the stats. The player never changes.
+//-----------------------------------------------------------------------------
+
+function PlanetXBlaster::onAdd(%this)
+{
+ %this.init(); // base weapon: bullet pool, steam vent, heat loop, defaults
+
+ // The blaster's tuning.
+ %this.fireCooldown = 200;
+ %this.bulletSpeed = 40;
+ %this.bulletLife = 1200;
+ %this.heatPerShot = 0.13;
+ %this.heatDecayPerSecond = 0.32;
+ %this.heatResumeThreshold = 0.35;
+}
diff --git a/PlanetX/PlanetXGame/scripts/brute.cs b/PlanetX/PlanetXGame/scripts/brute.cs
new file mode 100644
index 000000000..393f7ddc2
--- /dev/null
+++ b/PlanetX/PlanetXGame/scripts/brute.cs
@@ -0,0 +1,25 @@
+//-----------------------------------------------------------------------------
+// PlanetXBrute: a hulking dark-shelled alien with far more health than a bug.
+// Same PlanetXEnemy behavior, just a bigger body and a different sprite; the
+// level hands it the brute health via its constructor parameters.
+//-----------------------------------------------------------------------------
+
+function PlanetXBrute::onAdd(%this)
+{
+ %this.init(); // shared enemy setup (marker, scene group, AI tick, ...)
+
+ %this.setSize($PlanetX::BruteSize);
+ %this.playAnimation("PlanetXGame:bruteWalkAnim");
+
+ // Feet-centric collision, feet-centric Y-sort key - scaled up.
+ %this.createCircleCollisionShape(1.1, 0, -0.45);
+ %this.setCollisionGroups($PlanetX::PlayerGroup SPC $PlanetX::AlienGroup SPC
+ $PlanetX::BulletGroup SPC $PlanetX::WallGroup);
+ %this.setSortPoint(0, -1.6);
+
+ // A hulking brute barely flinches when shot.
+ %this.knockResist = 0.35;
+
+ // ...and goes out with a bigger blast than a bug (same green pop, scaled up).
+ %this.deathFxScale = $PlanetX::BruteDeathFxScale;
+}
diff --git a/PlanetX/PlanetXGame/scripts/bug.cs b/PlanetX/PlanetXGame/scripts/bug.cs
new file mode 100644
index 000000000..2061fe87f
--- /dev/null
+++ b/PlanetX/PlanetXGame/scripts/bug.cs
@@ -0,0 +1,19 @@
+//-----------------------------------------------------------------------------
+// PlanetXBug: the common alien - a coral critter that wanders the surface and
+// swarms the spaceman when he gets close. All of its behavior is PlanetXEnemy;
+// this class only builds the bug's body.
+//-----------------------------------------------------------------------------
+
+function PlanetXBug::onAdd(%this)
+{
+ %this.init(); // shared enemy setup (marker, scene group, AI tick, ...)
+
+ %this.setSize("2 2");
+ %this.playAnimation("PlanetXGame:alienWalkAnim");
+
+ // Feet-centric collision, feet-centric Y-sort key.
+ %this.createCircleCollisionShape(0.65, 0, -0.25);
+ %this.setCollisionGroups($PlanetX::PlayerGroup SPC $PlanetX::AlienGroup SPC
+ $PlanetX::BulletGroup SPC $PlanetX::WallGroup);
+ %this.setSortPoint(0, -0.9);
+}
diff --git a/PlanetX/PlanetXGame/scripts/bullet.cs b/PlanetX/PlanetXGame/scripts/bullet.cs
new file mode 100644
index 000000000..c7c5d2aeb
--- /dev/null
+++ b/PlanetX/PlanetXGame/scripts/bullet.cs
@@ -0,0 +1,78 @@
+//-----------------------------------------------------------------------------
+// PlanetXBullet: a laser bolt. Pooled by the weapon and parked off-world when
+// idle, so firing never allocates. The weapon positions and launches it; the
+// bullet handles its own collision, recycling, and parking.
+//-----------------------------------------------------------------------------
+
+function PlanetXBullet::onAdd(%this)
+{
+ %this.setSize("1 0.5");
+ %this.setSceneLayer($PlanetX::BulletLayer);
+ %this.setSceneGroup($PlanetX::BulletGroup);
+ %this.setImage("PlanetXGame:bolt");
+ %this.createCircleCollisionShape(0.25);
+ %this.setCollisionGroups($PlanetX::AlienGroup SPC $PlanetX::WallGroup);
+ %this.setCollisionCallback(true);
+ %this.setBullet(true);
+
+ // A sensor, not a solid: the bolt still reports its hit (onCollision fires, as it
+ // does for the crystal sensor) but takes no impulse from the contact. Without this,
+ // when a forked volley lands two bolts on the same alien in one step, the bolt that
+ // does NOT land the kill gets physically deflected but never receives its recycle
+ // callback - the alien's death suppresses its collision mid-step - so it drifts off
+ // slowly. As a sensor it just flies straight through and recycles on its next hit
+ // or when its life expires.
+ %this.setCollisionShapeIsSensor(0, true);
+
+ // Bolts fly at the angle they were fired at. Without this, a collision gives
+ // the body angular velocity which survives park() and recycling - pooled
+ // bullets came back visibly spinning.
+ %this.setFixedAngle(true);
+
+ // The weapon stamps the real per-shot damage onto each bolt as it launches it
+ // (launchBolt); this is just the un-upgraded default for a bolt that never was.
+ %this.damage = 1;
+}
+
+/// Cancel any pending recycle so no orphaned event outlives the bullet.
+function PlanetXBullet::onRemove(%this)
+{
+ if (isEventPending(%this.recycleEvent))
+ cancel(%this.recycleEvent);
+}
+
+function PlanetXBullet::onCollision(%this, %object, %collisionDetails)
+{
+ if (%object.isEnemy)
+ {
+ %object.takeDamage(%this.damage);
+
+ // Shove a surviving alien back along the bolt's travel - the physical knock the
+ // old solid bolts used to give, now that the bolt is a sensor. A dead alien
+ // (health spent) is on its way out, so skip it.
+ if (isObject(%object) && %object.health > 0)
+ %object.knockback(%this.getAngle());
+ }
+
+ if (isObject(PlanetXGame.level))
+ PlanetXGame.level.playBurst(%this.getPosition());
+
+ %this.recycle();
+}
+
+function PlanetXBullet::recycle(%this)
+{
+ if (isEventPending(%this.recycleEvent))
+ cancel(%this.recycleEvent);
+
+ %this.park();
+}
+
+/// Deactivate and stash the bullet outside the world until it is fired again.
+function PlanetXBullet::park(%this)
+{
+ %this.setLinearVelocity(0, 0);
+ %this.setActive(false);
+ %this.setVisible(false);
+ %this.setPosition(0, -500);
+}
diff --git a/PlanetX/PlanetXGame/scripts/burst.cs b/PlanetX/PlanetXGame/scripts/burst.cs
new file mode 100644
index 000000000..6a2bbc127
--- /dev/null
+++ b/PlanetX/PlanetXGame/scripts/burst.cs
@@ -0,0 +1,21 @@
+//-----------------------------------------------------------------------------
+// PlanetXBurst: a one-shot impact flash. Pooled by the level and replayed by
+// PlanetXLevel::playBurst where a bullet lands. It hides itself again when its
+// animation ends. (Deaths pop particle bursts instead - see deathFx.cs and the
+// player's own effect in player.cs.)
+//-----------------------------------------------------------------------------
+
+function PlanetXBurst::onAdd(%this)
+{
+ %this.setSize("2.5 2.5");
+ %this.setSceneLayer($PlanetX::EffectLayer);
+ %this.setImage("PlanetXGame:burst");
+ %this.setBodyType("static");
+ %this.setCollisionSuppress(true);
+ %this.setVisible(false);
+}
+
+function PlanetXBurst::onAnimationEnd(%this)
+{
+ %this.setVisible(false);
+}
diff --git a/PlanetX/PlanetXGame/scripts/camera.cs b/PlanetX/PlanetXGame/scripts/camera.cs
new file mode 100644
index 000000000..6faa53a76
--- /dev/null
+++ b/PlanetX/PlanetXGame/scripts/camera.cs
@@ -0,0 +1,162 @@
+//-----------------------------------------------------------------------------
+// PlanetXCamera: the shared co-op camera, a manager ScriptObject created by the
+// level only in two-player mode (single-player keeps the rigid mount). Each tick
+// it centers the window between the two players and zooms out just enough to keep
+// both framed, then eases back in as they regroup. The same tick watches for the
+// living player reaching the rocket to revive a downed teammate.
+//
+// It drives the window directly (setCameraPosition/setCameraSize), so the window
+// must NOT be mounted while this runs. It owns its follow schedule and cancels it
+// in onRemove; the level deletes this before it deletes the window.
+//-----------------------------------------------------------------------------
+
+// Camera framing. Height is the vertical world span shown; width follows the
+// window aspect (as in sceneWindow.cs). Base matches the single-player camera.
+$PlanetX::CameraBaseHeight = 45;
+$PlanetX::CameraMaxHeight = 90; // never zoom past this - keeps the view in-world
+$PlanetX::CameraZoomMargin = 18; // world padding around the two players
+$PlanetX::CameraZoomLerp = 0.12; // per-tick easing of the zoom toward its target
+$PlanetX::CameraTickMs = 16; // ~60 Hz follow
+
+// How close the living player must get to the rocket to revive a downed teammate.
+$PlanetX::ReviveRadius = 4;
+
+function PlanetXCamera::onAdd(%this)
+{
+ %this.height = $PlanetX::CameraBaseHeight;
+ %this.followTick();
+}
+
+function PlanetXCamera::onRemove(%this)
+{
+ if (isEventPending(%this.followEvent))
+ cancel(%this.followEvent);
+}
+
+/// Follow + revive loop. Stops rescheduling once the game leaves play (win, loss,
+/// or teardown), same as the aim loop; onRemove cancels any pending tick.
+function PlanetXCamera::followTick(%this)
+{
+ if ($PlanetX::state !$= "playing")
+ return;
+
+ // Keep the loop alive across a pause; hold the frame (and don't revive) while
+ // the world is frozen.
+ if (!$PlanetX::paused)
+ {
+ %this.updateFrame();
+ %this.reviveCheck();
+ }
+
+ %this.followEvent = %this.schedule($PlanetX::CameraTickMs, "followTick");
+}
+
+/// A player is framed while alive, and for a grace beat after they go down, so the
+/// camera lingers on the death spot (and its effect) before easing to the survivor.
+/// $PlanetX::DeathLingerMs is the shared "watch the death" beat, defined in game.cs.
+function PlanetXCamera::isFramed(%this, %p)
+{
+ if (!isObject(%p))
+ return false;
+ if (!%p.downed)
+ return true;
+ return (getSimTime() - %p.downedAt) < $PlanetX::DeathLingerMs;
+}
+
+/// Center on the framed players' midpoint and zoom to frame both (plus a margin).
+function PlanetXCamera::updateFrame(%this)
+{
+ // The level hands us a direct reference (PlanetXGame.level is not yet assigned
+ // while the level is still building, but its players already exist).
+ %level = %this.level;
+ if (!isObject(%level))
+ return;
+
+ // Gather the framed players: everyone still up, plus a just-downed one for a
+ // grace beat so its death effect is seen before the camera eases to the survivor.
+ %a = "";
+ %b = "";
+ %p = %level.player;
+ if (%this.isFramed(%p))
+ %a = %p;
+ %p = %level.player2;
+ if (%this.isFramed(%p))
+ {
+ if (%a $= "")
+ %a = %p;
+ else
+ %b = %p;
+ }
+
+ if (%a $= "")
+ return; // both down and past the grace; the game is ending - hold the last frame
+
+ %posA = %a.getPosition();
+
+ if (%b $= "")
+ {
+ // One player left: follow it, no extra zoom.
+ %midpoint = %posA;
+ %separation = 0;
+ }
+ else
+ {
+ %posB = %b.getPosition();
+ %midX = (getWord(%posA, 0) + getWord(%posB, 0)) * 0.5;
+ %midY = (getWord(%posA, 1) + getWord(%posB, 1)) * 0.5;
+ %midpoint = %midX SPC %midY;
+ %separation = Vector2Length(Vector2Sub(%posA, %posB));
+ }
+
+ // Ease the height toward one that frames the pair; width follows the aspect.
+ %target = mClamp(%separation + $PlanetX::CameraZoomMargin,
+ $PlanetX::CameraBaseHeight, $PlanetX::CameraMaxHeight);
+ %this.height += (%target - %this.height) * $PlanetX::CameraZoomLerp;
+
+ %extent = Canvas.extent;
+ %aspect = %extent.x / %extent.y;
+ %camWidth = %this.height * %aspect;
+
+ // setViewLimitOn (set in the level) clamps the position at the world edge.
+ PlanetXWindow.setCameraSize(%camWidth SPC %this.height);
+ PlanetXWindow.setCameraPosition(%midpoint);
+}
+
+/// While one player is down, revive them once the survivor reaches the rocket.
+function PlanetXCamera::reviveCheck(%this)
+{
+ %level = %this.level;
+ if (!isObject(%level) || !isObject(%level.rocket))
+ return;
+
+ // Exactly one of the two is down while the game is still playing (both down
+ // ends the game), so the other is the rescuer.
+ %downed = "";
+ %rescuer = "";
+
+ %p = %level.player;
+ if (isObject(%p))
+ {
+ if (%p.downed)
+ %downed = %p;
+ else
+ %rescuer = %p;
+ }
+ %p = %level.player2;
+ if (isObject(%p))
+ {
+ if (%p.downed)
+ %downed = %p;
+ else
+ %rescuer = %p;
+ }
+
+ if (!isObject(%downed) || !isObject(%rescuer))
+ return;
+
+ // Measured to the door (center-bottom of the rocket) - where a player walks in.
+ %door = %level.rocket.getDoorPosition();
+ %dist = Vector2Length(Vector2Sub(%rescuer.getPosition(), %door));
+ if (%dist <= $PlanetX::ReviveRadius)
+ %level.revivePlayer(%downed);
+}
diff --git a/PlanetX/PlanetXGame/scripts/crosshair.cs b/PlanetX/PlanetXGame/scripts/crosshair.cs
new file mode 100644
index 000000000..6ab626d56
--- /dev/null
+++ b/PlanetX/PlanetXGame/scripts/crosshair.cs
@@ -0,0 +1,13 @@
+//-----------------------------------------------------------------------------
+// PlanetXCrosshair: the aiming reticle - a decorative sprite that follows the
+// mouse (repositioned every aim tick by PlanetXInput).
+//-----------------------------------------------------------------------------
+
+function PlanetXCrosshair::onAdd(%this)
+{
+ %this.setSize("1.5 1.5");
+ %this.setSceneLayer($PlanetX::EffectLayer);
+ %this.setImage("PlanetXGame:crosshair");
+ %this.setBodyType("static");
+ %this.setCollisionSuppress(true);
+}
diff --git a/PlanetX/PlanetXGame/scripts/crystal.cs b/PlanetX/PlanetXGame/scripts/crystal.cs
new file mode 100644
index 000000000..9c8dd4ba4
--- /dev/null
+++ b/PlanetX/PlanetXGame/scripts/crystal.cs
@@ -0,0 +1,51 @@
+//-----------------------------------------------------------------------------
+// PlanetXCrystal: the crystal the spaceman is here for. A static sensor dropped
+// at a random spot each level (the level pushes it away from the rocket);
+// touching it clears the level. The level sets only its position; the crystal
+// configures its own look, collision, and glow pulse, and cancels the pulse
+// when it is deleted.
+//-----------------------------------------------------------------------------
+
+function PlanetXCrystal::onAdd(%this)
+{
+ %this.setSize("2.5 2.5");
+ %this.setSceneLayer($PlanetX::EntityLayer);
+ %this.setSceneGroup($PlanetX::PickupGroup);
+ %this.setImage("PlanetXGame:crystal");
+
+ %this.setBodyType("static");
+ %this.createCircleCollisionShape(1.2);
+ %this.setSortPoint(0, -1);
+
+ // A sensor detects contacts without physically blocking them.
+ %this.setCollisionShapeIsSensor(0, true);
+ %this.setCollisionCallback(true);
+
+ %this.pulse();
+}
+
+/// Cancel the self-rescheduling pulse so no orphaned event survives the crystal.
+function PlanetXCrystal::onRemove(%this)
+{
+ if (isEventPending(%this.pulseEvent))
+ cancel(%this.pulseEvent);
+}
+
+/// A slow glow pulse so the crystal reads as the goal from across the map.
+function PlanetXCrystal::pulse(%this)
+{
+ %this.bright = !%this.bright;
+
+ if (%this.bright)
+ %this.setBlendColor(1, 1, 1);
+ else
+ %this.setBlendColor(0.7, 0.85, 0.78);
+
+ %this.pulseEvent = %this.schedule(600, "pulse");
+}
+
+function PlanetXCrystal::onCollision(%this, %object, %collisionDetails)
+{
+ if (%object.class $= "PlanetXPlayer")
+ PlanetXGame.onWin();
+}
diff --git a/PlanetX/PlanetXGame/scripts/deathFx.cs b/PlanetX/PlanetXGame/scripts/deathFx.cs
new file mode 100644
index 000000000..5894ef3a8
--- /dev/null
+++ b/PlanetX/PlanetXGame/scripts/deathFx.cs
@@ -0,0 +1,30 @@
+//-----------------------------------------------------------------------------
+// PlanetXDeathFx: a one-shot particle burst for a dying enemy. A ParticlePlayer
+// that self-configures in onAdd and then sits idle until pop() drives it. The
+// level owns a small round-robin pool (PlanetXLevel::createDeathFxPool), so a bug
+// dying never allocates. Brutes reuse this same green pop at a larger scale.
+//
+// The "bugDeath" asset runs in STOP mode: each pop() emits a quick burst for the
+// asset's short lifetime, then the engine stops emission and parks the player for
+// reuse (unlike KILL, which would delete it and defeat the pool). See TORQUE_SCRIPT.md.
+//-----------------------------------------------------------------------------
+
+function PlanetXDeathFx::onAdd(%this)
+{
+ %this.Particle = "PlanetXGame:bugDeath";
+ %this.setSceneLayer($PlanetX::EffectLayer);
+ %this.setParticleInterpolation(true);
+ %this.setBodyType("static");
+ %this.setCollisionSuppress(true);
+ // No stop() here - the player isn't in a scene yet. Adding it to a scene is what
+ // auto-plays it, so the pool calls stop() right after PlanetXScene.add (see level.cs).
+}
+
+/// Fire the burst at %position, scaled by %scale (1 for a bug, larger for a brute).
+function PlanetXDeathFx::pop(%this, %position, %scale)
+{
+ %this.setPosition(%position);
+ %this.setSizeScale(%scale);
+ %this.setForceScale(%scale); // a bigger blast throws its debris wider too
+ %this.play(true);
+}
diff --git a/PlanetX/PlanetXGame/scripts/enemy.cs b/PlanetX/PlanetXGame/scripts/enemy.cs
new file mode 100644
index 000000000..58fe0caf8
--- /dev/null
+++ b/PlanetX/PlanetXGame/scripts/enemy.cs
@@ -0,0 +1,198 @@
+//-----------------------------------------------------------------------------
+// PlanetXEnemy: the base class for the planet's hostiles. It owns all the shared
+// behavior - wander-then-chase AI, a health pool with a hit flash and a death
+// burst, and contact damage against the player. The two concrete kinds
+// (PlanetXBug, PlanetXBrute) differ only in their body (size, sprite, collision)
+// and the stats the level hands them.
+//
+// The level passes target/health/chaseSpeed/contactDamage in as constructor
+// parameters, so init() must NOT set those (it would clobber them); it sets only
+// the values that are always the same. Only the most-derived ::onAdd fires, so
+// each subclass calls %this.init() and then builds its body. See TORQUE_SCRIPT.md.
+//-----------------------------------------------------------------------------
+
+$PlanetX::AlienHealth = 3;
+$PlanetX::AlienChaseSpeed = 7;
+$PlanetX::AlienWanderSpeed = 3;
+$PlanetX::AlienAggroRadius = 20;
+$PlanetX::AlienContactDamage = 10;
+$PlanetX::AlienDamageCooldownMs = 500;
+$PlanetX::ChaseTickMs = 400;
+
+// A bolt's hit shoves the alien back along the shot; the AI reclaims its velocity on
+// the next chase tick, so it reads as a brief recoil. BulletKnockbackSpeed is the
+// velocity bump a bug takes (brutes take a fraction - see knockResist). One shove per
+// cooldown de-dups a forked volley, and the max-speed gate keeps a fast gun from
+// piling shoves up between AI ticks into a launch.
+$PlanetX::BulletKnockbackSpeed = 6;
+$PlanetX::BulletKnockbackCooldownMs = 70;
+$PlanetX::BulletKnockbackMaxSpeed = 14;
+
+// The brute: a hulking dark-shelled variant.
+$PlanetX::BruteSize = "3.5 2.1875";
+
+// A brute's death pop is the bug's green burst scaled up, to match its bigger body.
+$PlanetX::BruteDeathFxScale = 1.4;
+
+/// Shared setup. Sets only the always-the-same values (the passed-in target and
+/// stats are left untouched) and starts the AI tick.
+function PlanetXEnemy::init(%this)
+{
+ // Marker so a bullet can tell an enemy from a wall without knowing the type.
+ %this.isEnemy = true;
+
+ %this.setSceneLayer($PlanetX::EntityLayer);
+ %this.setSceneGroup($PlanetX::AlienGroup);
+ %this.setCollisionCallback(true);
+
+ // The AI flips the sprite toward its heading; contacts must not spin it.
+ %this.setFixedAngle(true);
+
+ %this.wanderSpeed = $PlanetX::AlienWanderSpeed;
+ %this.aggroRadius = $PlanetX::AlienAggroRadius;
+ %this.wanderTicks = 0;
+ %this.lastContactDamage = 0;
+ %this.lastKnockback = 0;
+ %this.knockResist = 1; // fraction of a bolt's shove this alien takes (brutes < 1)
+
+ // Scale of the green death pop this alien fires when it dies (brutes override up).
+ %this.deathFxScale = 1;
+
+ // Chase/wander state, so the sound events fire only on the transition.
+ %this.chasing = false;
+
+ %this.chaseEvent = %this.schedule($PlanetX::ChaseTickMs, "updateChase");
+}
+
+function PlanetXEnemy::onAdd(%this)
+{
+ %this.init();
+}
+
+/// Cancel the AI tick so no orphaned reschedule outlives the enemy.
+function PlanetXEnemy::onRemove(%this)
+{
+ if (isEventPending(%this.chaseEvent))
+ cancel(%this.chaseEvent);
+}
+
+//-----------------------------------------------------------------------------
+// AI: wander aimlessly until the target comes within the aggro radius, then
+// chase it. (Sim schedules keep firing while the scene is paused for a dialog,
+// so the tick only acts while the game is actually playing.)
+//-----------------------------------------------------------------------------
+
+function PlanetXEnemy::updateChase(%this)
+{
+ %this.chaseEvent = %this.schedule($PlanetX::ChaseTickMs, "updateChase");
+
+ if ($PlanetX::state !$= "playing")
+ return;
+
+ // Co-op: chase whichever living player is nearest, re-evaluated each tick so
+ // a downed player is dropped and a revived one is picked back up. In single-
+ // player the target stays the sole player the level handed us.
+ if ($PlanetX::twoPlayer && isObject(PlanetXGame.level))
+ %this.target = PlanetXGame.level.nearestLivingPlayer(%this.getPosition());
+
+ if (!isObject(%this.target))
+ return;
+
+ %toTarget = Vector2Sub(%this.target.getPosition(), %this.getPosition());
+ %distance = Vector2Length(%toTarget);
+
+ if (%distance < %this.aggroRadius)
+ {
+ // Just locked on - announce it once (the level turns this into a sound).
+ if (!%this.chasing)
+ {
+ %this.chasing = true;
+ %this.postEvent("EnemyStartChase");
+ }
+
+ // Chase: head straight for the target.
+ %angle = mAtan(%toTarget);
+ %this.setLinearVelocityPolar(%angle, %this.chaseSpeed);
+ %this.setFlipX(getWord(Vector2Direction(%angle, 1), 0) < 0);
+ %this.wanderTicks = 0;
+ return;
+ }
+
+ // Lost the target - announce giving up once.
+ if (%this.chasing)
+ {
+ %this.chasing = false;
+ %this.postEvent("EnemyStopChase");
+ }
+
+ // Wander: hold a random heading for a few ticks, then pick a new one.
+ %this.wanderTicks--;
+ if (%this.wanderTicks <= 0)
+ {
+ %this.wanderTicks = getRandom(4, 9);
+ %angle = getRandom(0, 359);
+ %this.setLinearVelocityPolar(%angle, %this.wanderSpeed);
+ %this.setFlipX(getWord(Vector2Direction(%angle, 1), 0) < 0);
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Health and damage.
+//-----------------------------------------------------------------------------
+
+function PlanetXEnemy::takeDamage(%this, %amount)
+{
+ if (%this.health <= 0)
+ return;
+
+ %this.health -= %amount;
+
+ if (%this.health <= 0)
+ {
+ if (isObject(PlanetXGame.level))
+ PlanetXGame.level.playDeathFx(%this.getPosition(), %this.deathFxScale);
+ %this.postEvent("EnemyDeath");
+ %this.setCollisionSuppress(true);
+ %this.safeDelete();
+ return;
+ }
+
+ // Hit flash.
+ %this.setBlendColor(1, 0.45, 0.45);
+ %this.schedule(120, "setBlendColor", 1, 1, 1);
+}
+
+/// A bolt's hit shoves the alien back along the shot direction. The impulse is mass x
+/// the desired velocity bump, so the recoil speed is consistent whatever the body
+/// size, then scaled by knockResist (a heavy brute barely budges). The next chase tick
+/// re-derives velocity, so it's a brief pop; a per-alien cooldown keeps a forked volley
+/// from stacking several shoves into a launch.
+function PlanetXEnemy::knockback(%this, %angle)
+{
+ %now = getSimTime();
+ if (%now - %this.lastKnockback < $PlanetX::BulletKnockbackCooldownMs)
+ return;
+ %this.lastKnockback = %now;
+
+ // Already flying from earlier shoves this AI window - don't pile on and launch it.
+ if (Vector2Length(%this.getLinearVelocity()) >= $PlanetX::BulletKnockbackMaxSpeed)
+ return;
+
+ %deltaV = $PlanetX::BulletKnockbackSpeed * %this.knockResist;
+ %this.applyLinearImpulse(Vector2Direction(%angle, %this.getMass() * %deltaV), %this.getPosition());
+}
+
+/// Contact damage against the player, with a per-enemy cooldown so a lingering
+/// contact doesn't drain health every physics step.
+function PlanetXEnemy::onCollision(%this, %object, %collisionDetails)
+{
+ if (%object.class !$= "PlanetXPlayer" || %object.downed)
+ return;
+
+ %now = getSimTime();
+ if (%now - %this.lastContactDamage < $PlanetX::AlienDamageCooldownMs)
+ return;
+ %this.lastContactDamage = %now;
+
+ %object.takeDamage(%this.contactDamage);
+}
diff --git a/PlanetX/PlanetXGame/scripts/hud.cs b/PlanetX/PlanetXGame/scripts/hud.cs
new file mode 100644
index 000000000..2f7e4d8dc
--- /dev/null
+++ b/PlanetX/PlanetXGame/scripts/hud.cs
@@ -0,0 +1,204 @@
+//-----------------------------------------------------------------------------
+// PlanetXHud: the in-game heads-up display - hull (health) bar, gun-heat bar,
+// level readout, and the objective hint. A manager ScriptObject: it builds all
+// its controls into one panel over the scene window in onAdd and deletes that
+// panel in onRemove, so the level frees the whole HUD with a single delete.
+//
+// The level passes the level number (%this.number) for the readout. The player
+// and weapon push updates through setHealth/setHeat.
+//-----------------------------------------------------------------------------
+
+function PlanetXHud::onAdd(%this)
+{
+ // The heat bar wears PlanetXHeatProfile - a coral Progress variant that lives
+ // in the theme as an extra profile, not built here, so it is editable in the
+ // Profile Editor and follows the palette like everything else.
+
+ // One panel holds every HUD widget; deleting it frees them all. It is
+ // full-screen and sits ON TOP of the scene window, so it MUST be mouse-
+ // transparent (useInput = false) or it would swallow every aim/fire event
+ // meant for the scene beneath it. useInput is inherited by hit-testing:
+ // with it off, the panel and all its children are skipped by findHitControl.
+ %this.panel = new GuiControl()
+ {
+ Profile = "PlanetXEmptyProfile";
+ HorizSizing = "relative";
+ VertSizing = "relative";
+ Position = "0 0";
+ Extent = "1024 768";
+ useInput = false;
+ };
+ PlanetXRoot.add(%this.panel);
+
+ %hullLabel = new GuiControl()
+ {
+ Profile = "PlanetXLabelProfile";
+ FontSizeAdjust = 2;
+ HorizSizing = "right";
+ VertSizing = "bottom";
+ Position = "20 14";
+ Extent = "90 34";
+ Text = "HULL";
+ };
+ %this.panel.add(%hullLabel);
+
+ %this.healthBar = new GuiProgressCtrl()
+ {
+ Profile = "PlanetXProgressProfile";
+ HorizSizing = "right";
+ VertSizing = "bottom";
+ Position = "116 20";
+ Extent = "240 26";
+ };
+ %this.panel.add(%this.healthBar);
+ %this.healthBar.setProgress(1);
+
+ %this.heatLabel = new GuiControl()
+ {
+ Profile = "PlanetXLabelProfile";
+ FontSizeAdjust = 2;
+ HorizSizing = "right";
+ VertSizing = "bottom";
+ Position = "20 54";
+ Extent = "90 30";
+ Text = "HEAT";
+ };
+ %this.panel.add(%this.heatLabel);
+
+ %this.heatBar = new GuiProgressCtrl()
+ {
+ Profile = "PlanetXHeatProfile";
+ HorizSizing = "right";
+ VertSizing = "bottom";
+ Position = "116 60";
+ Extent = "240 16";
+ };
+ %this.panel.add(%this.heatBar);
+ %this.heatBar.setProgress(0);
+
+ // In co-op, player 2 gets a matching hull/heat readout mirrored along the
+ // top-right edge (player 1 stays top-left). Built only in two-player mode.
+ if ($PlanetX::twoPlayer)
+ {
+ %hullLabel2 = new GuiControl()
+ {
+ Profile = "PlanetXLabelProfile";
+ FontSizeAdjust = 2;
+ HorizSizing = "left";
+ VertSizing = "bottom";
+ Position = "914 14";
+ Extent = "90 34";
+ Text = "HULL";
+ Align = "right";
+ };
+ %this.panel.add(%hullLabel2);
+
+ %this.healthBar2 = new GuiProgressCtrl()
+ {
+ Profile = "PlanetXProgressProfile";
+ HorizSizing = "left";
+ VertSizing = "bottom";
+ Position = "668 20";
+ Extent = "240 26";
+ };
+ %this.panel.add(%this.healthBar2);
+ %this.healthBar2.setProgress(1);
+
+ %this.heatLabel2 = new GuiControl()
+ {
+ Profile = "PlanetXLabelProfile";
+ FontSizeAdjust = 2;
+ HorizSizing = "left";
+ VertSizing = "bottom";
+ Position = "914 54";
+ Extent = "90 30";
+ Text = "HEAT";
+ Align = "right";
+ };
+ %this.panel.add(%this.heatLabel2);
+
+ %this.heatBar2 = new GuiProgressCtrl()
+ {
+ Profile = "PlanetXHeatProfile";
+ HorizSizing = "left";
+ VertSizing = "bottom";
+ Position = "668 60";
+ Extent = "240 16";
+ };
+ %this.panel.add(%this.heatBar2);
+ %this.heatBar2.setProgress(0);
+
+ %this.heatWarning2 = false;
+ }
+
+ // The level readout sits in the top center in both modes - there is room and
+ // it reads better there than tucked in a corner.
+ %levelLabel = new GuiControl()
+ {
+ Profile = "PlanetXLabelProfile";
+ FontSizeAdjust = 2;
+ HorizSizing = "center";
+ VertSizing = "bottom";
+ Position = "362 14";
+ Extent = "300 36";
+ Align = "center";
+ };
+ %this.panel.add(%levelLabel);
+ %levelLabel.setText("LEVEL" SPC %this.number);
+
+ %this.hint = new GuiControl()
+ {
+ Profile = "PlanetXLabelProfile";
+ FontSizeAdjust = 2;
+ HorizSizing = "center";
+ VertSizing = "bottom";
+ Position = "262 56";
+ Extent = "500 34";
+ Text = "FIND THE CRYSTAL";
+ Align = "center";
+ OverrideFontColor = "1";
+ FontColor = "234 72 72 255";
+ };
+ %this.panel.add(%this.hint);
+ %this.hintEvent = %this.hint.schedule(6000, "setVisible", false);
+
+ %this.heatWarning = false;
+}
+
+function PlanetXHud::onRemove(%this)
+{
+ if (isEventPending(%this.hintEvent))
+ cancel(%this.hintEvent);
+ if (isObject(%this.panel))
+ %this.panel.delete();
+}
+
+function PlanetXHud::setHealth(%this, %index, %health)
+{
+ %bar = (%index == 2) ? %this.healthBar2 : %this.healthBar;
+ if (isObject(%bar))
+ %bar.setProgress(%health / $PlanetX::PlayerMaxHealth, 150);
+}
+
+function PlanetXHud::setHeat(%this, %index, %heat, %overheated, %tickMs)
+{
+ %bar = (%index == 2) ? %this.heatBar2 : %this.heatBar;
+ %label = (%index == 2) ? %this.heatLabel2 : %this.heatLabel;
+ %warned = (%index == 2) ? %this.heatWarning2 : %this.heatWarning;
+
+ if (isObject(%bar))
+ %bar.setProgress(%heat, %tickMs);
+
+ // The label doubles as the overheat warning.
+ if (isObject(%label) && %overheated != %warned)
+ {
+ if (%index == 2)
+ %this.heatWarning2 = %overheated;
+ else
+ %this.heatWarning = %overheated;
+
+ %label.OverrideFontColor = %overheated;
+ %label.FontColor = "234 72 72 255";
+ %label.setText(%overheated ? "VENT!" : "HEAT");
+ }
+}
diff --git a/PlanetX/PlanetXGame/scripts/input.cs b/PlanetX/PlanetXGame/scripts/input.cs
new file mode 100644
index 000000000..83b36ee87
--- /dev/null
+++ b/PlanetX/PlanetXGame/scripts/input.cs
@@ -0,0 +1,399 @@
+//-----------------------------------------------------------------------------
+// PlanetXInput: the level's input controller, a manager ScriptObject. The level
+// hands it a direct reference to itself (%this.level) so it works even while the
+// level is still building and PlanetXGame.level is not yet assigned.
+//
+// Keyboard: an ActionMap built from the saved bindings (PlanetXSettings), pushed
+// in onAdd and popped in onRemove. Each player has its own move keys and a fire
+// key; rebuilding the map (rebuildMoveMap) picks up rebindings live. The bound
+// handlers must be global functions (the engine calls them by bare name), so they
+// stay global and just forward to the right player.
+//
+// Aiming is per-player and configurable (PlanetXSettings P1Aim/P2Aim):
+// - "mouse": the player follows the cursor and fires with the mouse. Only one
+// player can hold the mouse (there is a single cursor); %this.mousePlayer is
+// the index that does, or 0 for none. The last cursor position is kept as a
+// *window* point and re-projected into the world every aim tick, so aim and
+// crosshair stay correct while the camera moves even when the mouse doesn't.
+// - "auto": the player points at the nearest alien in range each tick and fires
+// with its fire key.
+//
+// Pause: pauseGame keeps $PlanetX::state == "playing" and sets $PlanetX::paused;
+// the aim loop keeps rescheduling but skips its work, and the move/fire handlers
+// early-return, so the level freezes without any loop having to be restarted on
+// resume. See game.cs. See TORQUE_SCRIPT.md.
+//-----------------------------------------------------------------------------
+
+$PlanetX::AimTickMs = 32;
+
+// Auto-aim only locks onto aliens within this range; past it a player holds its
+// last heading instead of pointing at something across the whole map.
+$PlanetX::AutoAimRange = 30;
+
+function PlanetXInput::onAdd(%this)
+{
+ %this.buildMoveMap();
+
+ PlanetXWindow.addInputListener(%this);
+
+ // Start with the aim slightly right of the window center (the camera is
+ // centered on the player, so this reads as "straight ahead"). Window
+ // coordinates stay valid while the camera moves.
+ $PlanetX::aimWindow = "612 384";
+
+ // Set each player's aim mode from prefs and work out who holds the mouse.
+ %this.applyAimModes();
+
+ // Hide the OS cursor (the crosshair sprite replaces it). Note: hideCursor,
+ // NOT cursorOff - cursorOff stops the canvas from processing mouse events
+ // entirely (guiCanvas.cc gates mouse handling on the cursor being on).
+ Canvas.hideCursor();
+ %this.aimTick();
+}
+
+function PlanetXInput::onRemove(%this)
+{
+ %this.stopFiring();
+
+ if (isEventPending(%this.aimEvent))
+ cancel(%this.aimEvent);
+
+ if (isObject(PlanetXWindow))
+ PlanetXWindow.removeInputListener(%this);
+
+ Canvas.showCursor();
+
+ if (isObject(%this.moveMap))
+ {
+ %this.moveMap.pop();
+ %this.moveMap.delete();
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Key bindings. Built from the saved prefs so a rebind takes effect on the next
+// buildMoveMap. Player 1 is always bound; player 2 only in co-op. Escape is fixed
+// (not rebindable - the key-capture control refuses to assign it).
+//-----------------------------------------------------------------------------
+
+function PlanetXInput::buildMoveMap(%this)
+{
+ %s = PlanetXGame.settings;
+ %map = new ActionMap();
+
+ %map.bind("keyboard", %s.get("P1Up"), "planetXP1Up");
+ %map.bind("keyboard", %s.get("P1Down"), "planetXP1Down");
+ %map.bind("keyboard", %s.get("P1Left"), "planetXP1Left");
+ %map.bind("keyboard", %s.get("P1Right"), "planetXP1Right");
+ %map.bind("keyboard", %s.get("P1Fire"), "planetXP1Fire");
+
+ if ($PlanetX::twoPlayer)
+ {
+ %map.bind("keyboard", %s.get("P2Up"), "planetXP2Up");
+ %map.bind("keyboard", %s.get("P2Down"), "planetXP2Down");
+ %map.bind("keyboard", %s.get("P2Left"), "planetXP2Left");
+ %map.bind("keyboard", %s.get("P2Right"), "planetXP2Right");
+ %map.bind("keyboard", %s.get("P2Fire"), "planetXP2Fire");
+ }
+
+ %map.bind("keyboard", "escape", "planetXEscape");
+ %map.push();
+ %this.moveMap = %map;
+}
+
+/// Rebuild the live map from the current prefs (called after a rebinding while a
+/// level is running).
+function PlanetXInput::rebuildMoveMap(%this)
+{
+ if (isObject(%this.moveMap))
+ {
+ %this.moveMap.pop();
+ %this.moveMap.delete();
+ }
+ %this.buildMoveMap();
+}
+
+/// Convenience: the players own their triggers; input just relays. Stops both so
+/// a teardown or a pause mid-fire leaves no weapon looping.
+function PlanetXInput::stopFiring(%this)
+{
+ %level = %this.level;
+ if (!isObject(%level))
+ return;
+
+ if (isObject(%level.player))
+ %level.player.stopFiring();
+ if (isObject(%level.player2))
+ %level.player2.stopFiring();
+}
+
+//-----------------------------------------------------------------------------
+// Key handlers. These MUST be global (ActionMap bind targets). %val is 1 on
+// press, 0 on release. Each forwards to its player; all early-return while paused
+// so a key pressed over the pause dialog never mutates player state.
+//-----------------------------------------------------------------------------
+
+function planetXP1Up(%val) { if ($PlanetX::paused) return; %p = PlanetXGame.level.player; if (isObject(%p)) { %p.inUp = %val; %p.updateVelocity(); } }
+function planetXP1Down(%val) { if ($PlanetX::paused) return; %p = PlanetXGame.level.player; if (isObject(%p)) { %p.inDown = %val; %p.updateVelocity(); } }
+function planetXP1Left(%val) { if ($PlanetX::paused) return; %p = PlanetXGame.level.player; if (isObject(%p)) { %p.inLeft = %val; %p.updateVelocity(); } }
+function planetXP1Right(%val) { if ($PlanetX::paused) return; %p = PlanetXGame.level.player; if (isObject(%p)) { %p.inRight = %val; %p.updateVelocity(); } }
+
+function planetXP2Up(%val) { if ($PlanetX::paused) return; %p = PlanetXGame.level.player2; if (isObject(%p)) { %p.inUp = %val; %p.updateVelocity(); } }
+function planetXP2Down(%val) { if ($PlanetX::paused) return; %p = PlanetXGame.level.player2; if (isObject(%p)) { %p.inDown = %val; %p.updateVelocity(); } }
+function planetXP2Left(%val) { if ($PlanetX::paused) return; %p = PlanetXGame.level.player2; if (isObject(%p)) { %p.inLeft = %val; %p.updateVelocity(); } }
+function planetXP2Right(%val) { if ($PlanetX::paused) return; %p = PlanetXGame.level.player2; if (isObject(%p)) { %p.inRight = %val; %p.updateVelocity(); } }
+
+function planetXP1Fire(%val)
+{
+ if ($PlanetX::paused)
+ return;
+
+ %p = PlanetXGame.level.player;
+ if (!isObject(%p) || %p.downed)
+ return;
+
+ if (%val)
+ %p.startFiring();
+ else
+ %p.stopFiring();
+}
+
+function planetXP2Fire(%val)
+{
+ if ($PlanetX::paused)
+ return;
+
+ %p = PlanetXGame.level.player2;
+ if (!isObject(%p) || %p.downed)
+ return;
+
+ if (%val)
+ %p.startFiring();
+ else
+ %p.stopFiring();
+}
+
+function planetXEscape(%val)
+{
+ if (!%val)
+ return;
+
+ // While the options window is up, Esc backs out of it rather than toggling
+ // pause. Deferred one tick (like the toggle below) so the dialog swap does not
+ // run from inside a bound handler mid-dispatch.
+ if ($PlanetX::optionsOpen)
+ {
+ PlanetXGame.schedule(1, "closeOptions");
+ return;
+ }
+
+ if ($PlanetX::state !$= "playing")
+ return;
+
+ // Toggle: pause if running, resume if already paused. Deferred one tick so we
+ // don't post dialog events or touch the ActionMap from inside one of its own
+ // bound handlers while the key event is still dispatching.
+ if (!$PlanetX::paused)
+ PlanetXGame.schedule(1, "pauseGame");
+ else
+ PlanetXGame.schedule(1, "resumeGame");
+}
+
+//-----------------------------------------------------------------------------
+// Mouse handlers - always drive whichever player holds the mouse (mousePlayer).
+// %worldPosition arrives already converted to world space. All no-op while paused
+// (and the modal pause dialog captures the cursor anyway).
+//-----------------------------------------------------------------------------
+
+function PlanetXInput::onTouchDown(%this, %touchID, %worldPosition)
+{
+ if ($PlanetX::paused)
+ return;
+
+ %this.setAimFromWorld(%worldPosition);
+
+ %player = %this.mousePlayerObject();
+ if (isObject(%player) && !%player.downed)
+ %player.startFiring();
+}
+
+function PlanetXInput::onTouchUp(%this, %touchID, %worldPosition)
+{
+ %player = %this.mousePlayerObject();
+ if (isObject(%player))
+ %player.stopFiring();
+}
+
+function PlanetXInput::onTouchMoved(%this, %touchID, %worldPosition)
+{
+ if ($PlanetX::paused)
+ return;
+ %this.setAimFromWorld(%worldPosition);
+}
+
+function PlanetXInput::onTouchDragged(%this, %touchID, %worldPosition)
+{
+ if ($PlanetX::paused)
+ return;
+ %this.setAimFromWorld(%worldPosition);
+}
+
+//-----------------------------------------------------------------------------
+// Aiming.
+//-----------------------------------------------------------------------------
+
+/// Read each player's aim mode from prefs and decide who holds the mouse. Prefer
+/// player 1 if both are set to mouse (there is only one cursor); 0 = nobody. Also
+/// toggles the crosshair, which only makes sense when someone aims with the mouse.
+function PlanetXInput::applyAimModes(%this)
+{
+ %level = %this.level;
+ if (!isObject(%level))
+ return;
+
+ %s = PlanetXGame.settings;
+
+ if (isObject(%level.player))
+ %level.player.aimMode = %s.get("P1Aim");
+ if (isObject(%level.player2))
+ %level.player2.aimMode = %s.get("P2Aim");
+
+ %this.mousePlayer = 0;
+ if (isObject(%level.player) && %level.player.aimMode $= "mouse")
+ %this.mousePlayer = 1;
+ else if (isObject(%level.player2) && %level.player2.aimMode $= "mouse")
+ %this.mousePlayer = 2;
+
+ if (isObject(%level.crosshair))
+ %level.crosshair.setVisible(%this.mousePlayer != 0);
+}
+
+/// The player that currently holds the mouse, or "" if none.
+function PlanetXInput::mousePlayerObject(%this)
+{
+ %level = %this.level;
+ if (!isObject(%level))
+ return "";
+
+ if (%this.mousePlayer == 1)
+ return %level.player;
+ if (%this.mousePlayer == 2)
+ return %level.player2;
+ return "";
+}
+
+function PlanetXInput::setAimFromWorld(%this, %worldPosition)
+{
+ if (!isObject(PlanetXWindow))
+ return;
+
+ $PlanetX::aimWindow = PlanetXWindow.getWindowPoint(%worldPosition);
+ %this.updateAim();
+}
+
+/// Re-project the stored window point into the world, then point the mouse player
+/// (and the crosshair) at it. No-op if nobody holds the mouse.
+function PlanetXInput::updateAim(%this)
+{
+ if ($PlanetX::state !$= "playing" || $PlanetX::paused)
+ return;
+
+ %player = %this.mousePlayerObject();
+ if (!isObject(%player))
+ return;
+
+ %world = PlanetXWindow.getWorldPoint($PlanetX::aimWindow);
+
+ %level = %this.level;
+ if (isObject(%level.crosshair))
+ %level.crosshair.setPosition(%world);
+
+ %player.setAim(mAtan(Vector2Sub(%world, %player.getPosition())));
+}
+
+/// Point an auto-aim player at the nearest alien in range (firing rides on the aim
+/// angle, so holding its fire key shoots whatever is closest).
+function PlanetXInput::autoAimPlayer(%this, %player)
+{
+ if (!isObject(%player) || %player.downed)
+ return;
+
+ %level = %this.level;
+ %enemy = %level.nearestEnemy(%player.getPosition(), $PlanetX::AutoAimRange);
+ if (isObject(%enemy))
+ %player.setAim(mAtan(Vector2Sub(%enemy.getPosition(), %player.getPosition())));
+}
+
+function PlanetXInput::aimTick(%this)
+{
+ if ($PlanetX::state !$= "playing")
+ return;
+
+ // Keep the loop alive across a pause (state stays "playing"); just skip the aim
+ // work while frozen.
+ if (!$PlanetX::paused)
+ {
+ %level = %this.level;
+ if (isObject(%level))
+ {
+ %this.updateAim();
+
+ if (isObject(%level.player) && %level.player.aimMode $= "auto")
+ %this.autoAimPlayer(%level.player);
+ if (isObject(%level.player2) && %level.player2.aimMode $= "auto")
+ %this.autoAimPlayer(%level.player2);
+ }
+ }
+
+ %this.aimEvent = %this.schedule($PlanetX::AimTickMs, "aimTick");
+}
+
+//-----------------------------------------------------------------------------
+// Pause / resume. The ActionMap stays pushed through a pause so Esc still toggles;
+// only the handlers and the aim work are gated (on $PlanetX::paused).
+//-----------------------------------------------------------------------------
+
+function PlanetXInput::pause(%this)
+{
+ // Stop any in-progress fire and zero every held-move flag, so a key held across
+ // the pause doesn't drive motion on resume (the scene is frozen regardless).
+ %this.stopFiring();
+
+ %level = %this.level;
+ if (isObject(%level))
+ {
+ if (isObject(%level.player))
+ %this.clearMovement(%level.player);
+ if (isObject(%level.player2))
+ %this.clearMovement(%level.player2);
+ }
+
+ // The crosshair hides the OS cursor during play; show it so the dialog buttons
+ // are clickable.
+ Canvas.showCursor();
+}
+
+function PlanetXInput::resume(%this)
+{
+ Canvas.hideCursor();
+
+ // Bindings and aim modes may have changed in the options screen while paused.
+ if ($PlanetX::bindingsDirty)
+ {
+ %this.rebuildMoveMap();
+ $PlanetX::bindingsDirty = false;
+ }
+ %this.applyAimModes();
+ // aimTick kept rescheduling through the pause (gated), so there is nothing to
+ // restart here.
+}
+
+/// Zero a player's held-move flags and settle its velocity/animation to idle.
+function PlanetXInput::clearMovement(%this, %player)
+{
+ %player.inUp = 0;
+ %player.inDown = 0;
+ %player.inLeft = 0;
+ %player.inRight = 0;
+ %player.updateVelocity();
+}
diff --git a/PlanetX/PlanetXGame/scripts/level.cs b/PlanetX/PlanetXGame/scripts/level.cs
new file mode 100644
index 000000000..771450a5a
--- /dev/null
+++ b/PlanetX/PlanetXGame/scripts/level.cs
@@ -0,0 +1,652 @@
+//-----------------------------------------------------------------------------
+// PlanetXLevel: owns one playthrough level - the scene, the scene window, the
+// HUD, the input controller, and every object placed in the world. Creating a
+// PlanetXLevel builds the whole world in onAdd; deleting it tears the world
+// back down in onRemove. PlanetXGame holds exactly one of these as %this.level.
+//
+// Spawner methods here set only what the LEVEL controls (which class, where it
+// goes, and per-level difficulty stats); each object configures the rest of
+// itself in its own onAdd. See TORQUE_SCRIPT.md.
+//
+// World is 192x144 units: a 96x72 tile map of 2-unit tiles centered on the
+// origin. The camera shows a 60x45 window (width refit to the window aspect).
+//-----------------------------------------------------------------------------
+
+// Scene group ids. Collision groups are whitelists: a body only collides with
+// the groups it lists.
+$PlanetX::PlayerGroup = 1;
+$PlanetX::AlienGroup = 2;
+$PlanetX::BulletGroup = 3;
+$PlanetX::WallGroup = 4;
+$PlanetX::PickupGroup = 5;
+
+// Scene layers, back (high) to front (low). Ground entities share EntityLayer,
+// depth-sorted by -Y (lower on screen = drawn in front).
+$PlanetX::TileLayer = 30;
+$PlanetX::EntityLayer = 20;
+$PlanetX::BulletLayer = 12;
+$PlanetX::EffectLayer = 10;
+
+$PlanetX::WorldHalfWidth = 96;
+$PlanetX::WorldHalfHeight = 72;
+
+// Objective placement: margin from the world edge and the minimum distance the
+// crystal must be from the rocket.
+$PlanetX::PlacementMargin = 10;
+$PlanetX::MinObjectiveDistance = 100;
+
+// Nest generation: bugs spawn where a noise channel exceeds the threshold, so
+// they come in clusters. The safe radius keeps the landing site clear.
+$PlanetX::AlienNestZoom = 0.08;
+$PlanetX::AlienNestThreshold = 0.7;
+$PlanetX::AlienNestStep = 6;
+$PlanetX::AlienSafeRadius = 26;
+$PlanetX::MaxAliens = 90;
+
+$PlanetX::BurstPoolSize = 8;
+
+// Dying enemies pop a green particle burst from this pool (bugs at scale 1, brutes
+// larger - see enemy.cs). Kept small and quick, so slots free fast even in a swarm.
+$PlanetX::DeathFxPoolSize = 10;
+
+// A per-asset throttle keeps a mass aggro or a chain of deaths from blaring the
+// same enemy sound across the whole swarm at once.
+$PlanetX::EnemySoundThrottleMs = 200;
+
+//-----------------------------------------------------------------------------
+// Lifecycle.
+//-----------------------------------------------------------------------------
+
+function PlanetXLevel::onAdd(%this)
+{
+ %this.applyDifficulty();
+ %this.buildScene();
+
+ // One seed describes the whole level. The Perlin generator handles the
+ // FIELDS (terrain colors, alien nests); the standard RNG - reseeded with
+ // the same seed - handles the POINTS (placements, counts, tile variants).
+ %this.levelSeed = getRandom(1, 999999);
+ %this.generator = new NoiseGenerator();
+ %this.generator.setSeed(%this.levelSeed);
+ setRandomSeed(%this.levelSeed);
+ echo("PlanetX: level seed" SPC %this.levelSeed);
+
+ %this.spawnTileMap();
+ %this.spawnBarriers();
+ %this.spawnRocks();
+
+ // Drop the rocket at a random spot, then draw crystal spots until one lands
+ // far enough away, keeping the farthest candidate seen.
+ %rocketPosition = %this.randomWorldPoint();
+
+ %crystalPosition = %rocketPosition;
+ %bestDistance = 0;
+ for (%try = 0; %try < 20; %try++)
+ {
+ %candidate = %this.randomWorldPoint();
+ %distance = Vector2Length(Vector2Sub(%candidate, %rocketPosition));
+
+ if (%distance > %bestDistance)
+ {
+ %bestDistance = %distance;
+ %crystalPosition = %candidate;
+ }
+
+ if (%bestDistance >= $PlanetX::MinObjectiveDistance)
+ break;
+ }
+
+ echo("PlanetX: objectives" SPC Vector2Length(Vector2Sub(%crystalPosition, %rocketPosition)) SPC "apart");
+
+ %this.spawnRocket(%rocketPosition);
+ %this.spawnCrystal(%crystalPosition);
+ %this.createBurstPool();
+ %this.createDeathFxPool();
+ %this.spawnCrosshair();
+
+ // The spaceman steps out beside his rocket. Aim mode comes from the saved
+ // settings (default: player 1 mouse, player 2 automatic).
+ %spawn = %this.clampToWorld(Vector2Add(%rocketPosition, "6 -3"));
+ %this.player = %this.spawnPlayer(%spawn, 1, "PlanetXGame:spacemanIdle",
+ "PlanetXGame:spacemanWalkAnim", PlanetXGame.settings.get("P1Aim"));
+
+ if ($PlanetX::twoPlayer)
+ {
+ // The second spaceman lands right beside the first, in his own colors.
+ %spawn2 = %this.clampToWorld(Vector2Add(%rocketPosition, "3 0"));
+ %this.player2 = %this.spawnPlayer(%spawn2, 2, "PlanetXGame:spacemanIdle2",
+ "PlanetXGame:spacemanWalkAnim2", PlanetXGame.settings.get("P2Aim"));
+ }
+
+ // Camera: one player -> rigid follow; two -> a shared camera that frames both
+ // (camera.cs). The two-player camera positions the window itself each tick, so
+ // it must NOT be mounted.
+ if ($PlanetX::twoPlayer)
+ %this.camera = new ScriptObject() { class = "PlanetXCamera"; level = %this; };
+ else
+ PlanetXWindow.mount(%this.player, "0 0", 0, true, false);
+
+ // The level tracks every live enemy (a non-owning set) so player 2's auto-aim
+ // and enemy retargeting can find the nearest one. Must exist before spawning.
+ %this.enemies = new SimSet();
+ %this.spawnBugs();
+ %this.spawnBrutes();
+
+ // The HUD and input controller are the level's non-visual managers; each
+ // builds and tears down its own pieces.
+ %this.hud = new ScriptObject() { class = "PlanetXHud"; number = %this.number; };
+ %this.input = new ScriptObject() { class = "PlanetXInput"; level = %this; };
+
+ %this.generator.delete();
+
+ // Level generation is done: give the gameplay RNG (bug wander, the next
+ // level's seed) fresh time-based entropy so retries differ.
+ setRandomSeed();
+
+ // The mission begins. (One site: new runs, next levels, and retries all build
+ // a fresh level, so this fires for every level start.)
+ Audio.PlaySound("PlanetXGame:levelStart");
+}
+
+/// Tear the whole level down. Deleting the scene fires every SceneObject's
+/// onRemove (crystal cancels its pulse, bugs cancel their chase, the player
+/// deletes its weapon, ...); deleting the root frees the window and any GUI.
+function PlanetXLevel::onRemove(%this)
+{
+ // The camera owns a schedule that pokes the window; kill it before the window.
+ if (isObject(%this.camera))
+ %this.camera.delete();
+ if (isObject(%this.input))
+ %this.input.delete();
+ if (isObject(%this.hud))
+ %this.hud.delete();
+
+ if (isObject(PlanetXRoot))
+ PlanetXRoot.delete();
+ if (isObject(PlanetXScene))
+ PlanetXScene.delete();
+
+ // The enemy set only references its members (the scene owned and just freed
+ // them), so this frees an already-empty set.
+ if (isObject(%this.enemies))
+ %this.enemies.delete();
+}
+
+/// Freeze the level and stop input without tearing it down - used while a
+/// win/lose dialog or a fade-out is showing over the still-visible world.
+function PlanetXLevel::suspend(%this)
+{
+ if (isObject(PlanetXScene))
+ PlanetXScene.setScenePause(true);
+
+ // Deleting the input controller restores the cursor (so dialog buttons are
+ // clickable), pops the ActionMap, and cancels the aim loop.
+ if (isObject(%this.input))
+ %this.input.delete();
+}
+
+/// Pause the level in place: freeze the world and quiet input, but keep everything
+/// alive so resume() continues exactly where it left off. Used by the pause dialog
+/// (contrast suspend(), which is for a terminal win/lose and tears input down).
+function PlanetXLevel::pause(%this)
+{
+ if (isObject(PlanetXScene))
+ PlanetXScene.setScenePause(true);
+
+ if (isObject(%this.input))
+ %this.input.pause();
+}
+
+/// Unfreeze after a pause. Input restores the crosshair/cursor and re-reads any
+/// bindings or aim modes changed in the options screen; the scene resumes stepping.
+function PlanetXLevel::resume(%this)
+{
+ if (isObject(%this.input))
+ %this.input.resume();
+
+ if (isObject(PlanetXScene))
+ PlanetXScene.setScenePause(false);
+}
+
+//-----------------------------------------------------------------------------
+// Difficulty. Base stats describe level 1; each level up makes the swarm denser
+// and every bug tougher, faster, and harder-hitting. The per-level results are
+// stored on the level and passed to each bug/brute as constructor parameters.
+//-----------------------------------------------------------------------------
+
+function PlanetXLevel::applyDifficulty(%this)
+{
+ %step = %this.number - 1;
+
+ %this.bugHealth = $PlanetX::AlienHealth + mFloor(%step / 2);
+ %this.bruteHealth = 4 * %this.bugHealth;
+ %this.chaseSpeed = mClamp($PlanetX::AlienChaseSpeed + 0.25 * %step, 0, 11);
+ %this.contactDamage = mClamp($PlanetX::AlienContactDamage + %step, 0, 25);
+ %this.nestThreshold = mClamp($PlanetX::AlienNestThreshold - 0.015 * %step, 0.62, 1);
+ %this.bruteBonus = mClamp(%step, 0, 8);
+
+ echo("PlanetX: difficulty for level" SPC %this.number
+ SPC "- hp" SPC %this.bugHealth
+ SPC "speed" SPC %this.chaseSpeed
+ SPC "damage" SPC %this.contactDamage
+ SPC "threshold" SPC %this.nestThreshold);
+}
+
+//-----------------------------------------------------------------------------
+// Scene, root GUI, and camera window.
+//-----------------------------------------------------------------------------
+
+function PlanetXLevel::buildScene(%this)
+{
+ new Scene(PlanetXScene);
+ PlanetXScene.setGravity(0, 0);
+
+ // Y-sort the ground entities: higher world-Y renders first (behind).
+ PlanetXScene.setLayerSortMode($PlanetX::EntityLayer, "-Y");
+
+ // Root GUI control so HUD elements can overlay the scene window.
+ new GuiControl(PlanetXRoot)
+ {
+ Profile = "PlanetXEmptyProfile";
+ HorizSizing = "relative";
+ VertSizing = "relative";
+ Position = "0 0";
+ Extent = "1024 768";
+ };
+
+ new SceneWindow(PlanetXWindow)
+ {
+ class = "PlanetXSceneWindow";
+ Profile = "PlanetXEmptyProfile";
+ HorizSizing = "relative";
+ VertSizing = "relative";
+ Position = "0 0";
+ Extent = "1024 768";
+ };
+ PlanetXRoot.add(PlanetXWindow);
+
+ PlanetXWindow.setScene(PlanetXScene);
+ PlanetXWindow.setCameraSize(60, 45);
+ PlanetXWindow.updateCameraAspect();
+ PlanetXWindow.setViewLimitOn(-$PlanetX::WorldHalfWidth, -$PlanetX::WorldHalfHeight,
+ $PlanetX::WorldHalfWidth, $PlanetX::WorldHalfHeight);
+}
+
+/// A uniformly random point inside the world bounds (seeded RNG).
+function PlanetXLevel::randomWorldPoint(%this)
+{
+ %rangeX = $PlanetX::WorldHalfWidth - $PlanetX::PlacementMargin;
+ %rangeY = $PlanetX::WorldHalfHeight - $PlanetX::PlacementMargin;
+
+ return getRandom(-%rangeX, %rangeX) SPC getRandom(-%rangeY, %rangeY);
+}
+
+/// Clamp a point to the world bounds, respecting the placement margin.
+function PlanetXLevel::clampToWorld(%this, %point)
+{
+ %rangeX = $PlanetX::WorldHalfWidth - $PlanetX::PlacementMargin;
+ %rangeY = $PlanetX::WorldHalfHeight - $PlanetX::PlacementMargin;
+
+ return mClamp(getWord(%point, 0), -%rangeX, %rangeX) SPC
+ mClamp(getWord(%point, 1), -%rangeY, %rangeY);
+}
+
+//-----------------------------------------------------------------------------
+// Spawners. Each sets the class and only the values the level decides; the
+// object's onAdd does the rest.
+//-----------------------------------------------------------------------------
+
+function PlanetXLevel::spawnTileMap(%this)
+{
+ // The tile map builds its own grid from the level's noise generator.
+ %map = new CompositeSprite() { class = "PlanetXTileMap"; generator = %this.generator; };
+ PlanetXScene.add(%map);
+ %this.tileMap = %map;
+}
+
+/// Four invisible static walls just outside the visible world.
+function PlanetXLevel::spawnBarriers(%this)
+{
+ %w = $PlanetX::WorldHalfWidth;
+ %h = $PlanetX::WorldHalfHeight;
+ %t = 4;
+
+ %this.spawnBarrier(-(%w + %t / 2), 0, %t, 2 * %h + 4 * %t);
+ %this.spawnBarrier(%w + %t / 2, 0, %t, 2 * %h + 4 * %t);
+ %this.spawnBarrier(0, -(%h + %t / 2), 2 * %w, %t);
+ %this.spawnBarrier(0, %h + %t / 2, 2 * %w, %t);
+}
+
+function PlanetXLevel::spawnBarrier(%this, %x, %y, %width, %height)
+{
+ %wall = new SceneObject()
+ {
+ class = "PlanetXBarrier";
+ Position = %x SPC %y;
+ Size = %width SPC %height;
+ };
+ PlanetXScene.add(%wall);
+}
+
+/// Boulders, hand-placed to break the open ground into loose lanes.
+function PlanetXLevel::spawnRocks(%this)
+{
+ %rocks = "-60 -30 3" TAB "-40 -55 2.5" TAB "-30 -10 3.5" TAB "-55 20 3" TAB
+ "-20 40 2.5" TAB "-5 -40 3" TAB "0 15 3.5" TAB "20 -20 2.5" TAB
+ "25 55 3" TAB "40 -55 3.5" TAB "45 10 2.5" TAB "60 -30 3" TAB
+ "60 40 3.5" TAB "75 20 2.5" TAB "35 30 3" TAB "-75 55 3";
+
+ for (%i = 0; %i < getFieldCount(%rocks); %i++)
+ {
+ %field = getField(%rocks, %i);
+ %size = getWord(%field, 2);
+
+ // Position, size, and which of the two rock images: all level-decided.
+ %rock = new Sprite()
+ {
+ class = "PlanetXRock";
+ Position = getWord(%field, 0) SPC getWord(%field, 1);
+ Size = %size SPC %size;
+ variant = 1 + (%i % 2);
+ };
+ PlanetXScene.add(%rock);
+ }
+}
+
+function PlanetXLevel::spawnRocket(%this, %position)
+{
+ %rocket = new Sprite() { class = "PlanetXRocket"; Position = %position; };
+ PlanetXScene.add(%rocket);
+ %this.rocket = %rocket;
+}
+
+function PlanetXLevel::spawnCrystal(%this, %position)
+{
+ %crystal = new Sprite() { class = "PlanetXCrystal"; Position = %position; };
+ PlanetXScene.add(%crystal);
+ %this.crystal = %crystal;
+}
+
+function PlanetXLevel::spawnCrosshair(%this)
+{
+ %crosshair = new Sprite() { class = "PlanetXCrosshair"; };
+ PlanetXScene.add(%crosshair);
+ %this.crosshair = %crosshair;
+}
+
+/// Spawn a spaceman. The level decides only its identity - where it lands, which
+/// player it is, its sprites, and how it aims; the player configures the rest of
+/// itself in onAdd.
+function PlanetXLevel::spawnPlayer(%this, %position, %index, %idleImage, %walkAnim, %aimMode)
+{
+ %player = new CompositeSprite()
+ {
+ class = "PlanetXPlayer";
+ Position = %position;
+ playerIndex = %index;
+ idleImage = %idleImage;
+ walkAnim = %walkAnim;
+ aimMode = %aimMode;
+ };
+ PlanetXScene.add(%player);
+ return %player;
+}
+
+//-----------------------------------------------------------------------------
+// Co-op helpers: nearest living player (enemy targeting), nearest enemy (player
+// 2 auto-aim), and reviving a downed teammate at the rocket.
+//-----------------------------------------------------------------------------
+
+/// The closest player that is still up (not downed) to %position. In single-
+/// player it is always the sole player. Returns "" only if every player is down.
+function PlanetXLevel::nearestLivingPlayer(%this, %position)
+{
+ %best = "";
+ %bestDist = 0;
+
+ %p = %this.player;
+ if (isObject(%p) && !%p.downed)
+ {
+ %best = %p;
+ %bestDist = Vector2Length(Vector2Sub(%p.getPosition(), %position));
+ }
+
+ %p = %this.player2;
+ if (isObject(%p) && !%p.downed)
+ {
+ %dist = Vector2Length(Vector2Sub(%p.getPosition(), %position));
+ if (%best $= "" || %dist < %bestDist)
+ %best = %p;
+ }
+
+ return %best;
+}
+
+/// The closest live enemy to %position within %maxRange, or "" if none - backs
+/// player 2's auto-aim. Every spawned enemy is kept in %this.enemies.
+function PlanetXLevel::nearestEnemy(%this, %position, %maxRange)
+{
+ if (!isObject(%this.enemies))
+ return "";
+
+ %best = "";
+ %bestDist = %maxRange;
+
+ %count = %this.enemies.getCount();
+ for (%i = 0; %i < %count; %i++)
+ {
+ %enemy = %this.enemies.getObject(%i);
+ %dist = Vector2Length(Vector2Sub(%enemy.getPosition(), %position));
+ if (%dist <= %bestDist)
+ {
+ %bestDist = %dist;
+ %best = %enemy;
+ }
+ }
+
+ return %best;
+}
+
+/// Bring a downed teammate back at the rocket's door at full health.
+function PlanetXLevel::revivePlayer(%this, %player)
+{
+ %player.revive(%this.clampToWorld(%this.rocket.getDoorPosition()));
+ Audio.PlaySound("PlanetXGame:levelStart");
+}
+
+/// Populate the planet from the level's noise field: sample a coarse grid, spawn
+/// a bug wherever the nest channel runs hot. Clustered by nature.
+function PlanetXLevel::spawnBugs(%this)
+{
+ %count = 0;
+ %step = $PlanetX::AlienNestStep;
+ %rangeX = $PlanetX::WorldHalfWidth - %step;
+ %rangeY = $PlanetX::WorldHalfHeight - %step;
+
+ for (%wy = -%rangeY; %wy <= %rangeY; %wy += %step)
+ {
+ for (%wx = -%rangeX; %wx <= %rangeX; %wx += %step)
+ {
+ %value = %this.generator.getNoise(
+ %wx * $PlanetX::AlienNestZoom + 700.13,
+ %wy * $PlanetX::AlienNestZoom + 700.13);
+
+ if (%value < %this.nestThreshold)
+ continue;
+
+ // Jitter off the grid using a second, finer noise channel.
+ %jx = (%this.generator.getNoise(%wx * 0.31 + 1300.7, %wy * 0.31) - 0.5) * %step;
+ %jy = (%this.generator.getNoise(%wx * 0.31, %wy * 0.31 + 1300.7) - 0.5) * %step;
+ %position = %wx + %jx SPC %wy + %jy;
+
+ // Keep the landing site clear.
+ if (Vector2Length(Vector2Sub(%position, %this.player.getPosition())) < $PlanetX::AlienSafeRadius)
+ continue;
+
+ %this.spawnBug(%position);
+ %count++;
+
+ if (%count >= $PlanetX::MaxAliens)
+ {
+ echo("PlanetX:" SPC %count SPC "bugs (capped)");
+ return;
+ }
+ }
+ }
+
+ echo("PlanetX:" SPC %count SPC "bugs spawned");
+}
+
+/// Three or four brutes - plus one more per level, capped - at random spots,
+/// nudged away from the landing site if one lands on it.
+function PlanetXLevel::spawnBrutes(%this)
+{
+ %count = getRandom(3, 4) + %this.bruteBonus;
+
+ for (%i = 0; %i < %count; %i++)
+ {
+ %position = %this.randomWorldPoint();
+
+ %toPlayer = Vector2Sub(%position, %this.player.getPosition());
+ if (Vector2Length(%toPlayer) < $PlanetX::AlienSafeRadius * 2)
+ {
+ %angle = mAtan(%toPlayer);
+ %position = %this.clampToWorld(Vector2Add(%this.player.getPosition(),
+ Vector2Direction(%angle, $PlanetX::AlienSafeRadius * 2)));
+ }
+
+ %this.spawnBrute(%position);
+ }
+
+ echo("PlanetX:" SPC %count SPC "brutes spawned");
+}
+
+function PlanetXLevel::spawnBug(%this, %position)
+{
+ %bug = new Sprite()
+ {
+ class = "PlanetXBug";
+ superclass = "PlanetXEnemy";
+ Position = %position;
+ target = %this.player;
+ health = %this.bugHealth;
+ chaseSpeed = %this.chaseSpeed;
+ contactDamage = %this.contactDamage;
+ };
+ PlanetXScene.add(%bug);
+ %this.enemies.add(%bug);
+
+ // The level listens for this enemy's sound events (see onEnemyStartChase etc.).
+ %this.startListening(%bug);
+ return %bug;
+}
+
+function PlanetXLevel::spawnBrute(%this, %position)
+{
+ %brute = new Sprite()
+ {
+ class = "PlanetXBrute";
+ superclass = "PlanetXEnemy";
+ Position = %position;
+ target = %this.player;
+ health = %this.bruteHealth;
+ chaseSpeed = %this.chaseSpeed;
+ contactDamage = %this.contactDamage;
+ };
+ PlanetXScene.add(%brute);
+ %this.enemies.add(%brute);
+
+ // The level listens for this enemy's sound events (see onEnemyStartChase etc.).
+ %this.startListening(%brute);
+ return %brute;
+}
+
+//-----------------------------------------------------------------------------
+// Impact bursts: a small pool of one-shot animated sprites, fired where a bullet
+// lands. A level-wide effects service. (Deaths use particle bursts instead - see
+// createDeathFxPool below and the player's own effect in player.cs.)
+//-----------------------------------------------------------------------------
+
+function PlanetXLevel::createBurstPool(%this)
+{
+ for (%i = 0; %i < $PlanetX::BurstPoolSize; %i++)
+ {
+ %burst = new Sprite() { class = "PlanetXBurst"; };
+ PlanetXScene.add(%burst);
+ %this.burst[%i] = %burst;
+ }
+ %this.nextBurst = 0;
+}
+
+function PlanetXLevel::playBurst(%this, %position)
+{
+ %burst = %this.burst[%this.nextBurst];
+ %this.nextBurst = (%this.nextBurst + 1) % $PlanetX::BurstPoolSize;
+
+ %burst.setPosition(%position);
+ %burst.setVisible(true);
+ %burst.playAnimation("PlanetXGame:burstAnim");
+}
+
+//-----------------------------------------------------------------------------
+// Enemy death pops: a pool of pre-built particle bursts, replayed by playDeathFx
+// wherever an alien dies. Pre-building the ParticlePlayers here means a death in
+// the thick of a swarm never allocates. Brutes pass a larger scale for a bigger
+// blast off the same green effect (see PlanetXDeathFx in deathFx.cs).
+//-----------------------------------------------------------------------------
+
+function PlanetXLevel::createDeathFxPool(%this)
+{
+ for (%i = 0; %i < $PlanetX::DeathFxPoolSize; %i++)
+ {
+ %fx = new ParticlePlayer() { class = "PlanetXDeathFx"; };
+ PlanetXScene.add(%fx);
+ %fx.stop(); // added-to-scene auto-plays; park it until the first pop
+ %this.deathFx[%i] = %fx;
+ }
+ %this.nextDeathFx = 0;
+}
+
+function PlanetXLevel::playDeathFx(%this, %position, %scale)
+{
+ %fx = %this.deathFx[%this.nextDeathFx];
+ %this.nextDeathFx = (%this.nextDeathFx + 1) % $PlanetX::DeathFxPoolSize;
+
+ %fx.pop(%position, %scale);
+}
+
+//-----------------------------------------------------------------------------
+// Enemy sounds: a level-wide service, alongside playBurst. Enemies announce state
+// changes as object-to-object events (see enemy.cs); the level listens to every
+// enemy it spawns and turns those events into distance-attenuated, throttled
+// sounds. One listener for the whole swarm makes the throttle naturally global.
+//-----------------------------------------------------------------------------
+
+function PlanetXLevel::onEnemyStartChase(%this)
+{
+ %this.playEnemySound("PlanetXGame:enemyChase");
+}
+
+function PlanetXLevel::onEnemyStopChase(%this)
+{
+ %this.playEnemySound("PlanetXGame:enemyGiveUp");
+}
+
+function PlanetXLevel::onEnemyDeath(%this)
+{
+ %this.playEnemySound("PlanetXGame:enemyDeath");
+}
+
+/// Play an enemy event's sound at full volume, rate-limited per asset name across
+/// the whole swarm so a mass aggro or a chain of deaths does not blare. Uses only
+/// the Audio module's public API.
+function PlanetXLevel::playEnemySound(%this, %name)
+{
+ // Per-asset throttle (keyed by asset id), shared across every enemy.
+ %now = getSimTime();
+ if (%now - %this.lastEnemySound[%name] < $PlanetX::EnemySoundThrottleMs)
+ return;
+ %this.lastEnemySound[%name] = %now;
+
+ Audio.PlaySound(%name);
+}
diff --git a/PlanetX/PlanetXGame/scripts/player.cs b/PlanetX/PlanetXGame/scripts/player.cs
new file mode 100644
index 000000000..6896515c4
--- /dev/null
+++ b/PlanetX/PlanetXGame/scripts/player.cs
@@ -0,0 +1,457 @@
+//-----------------------------------------------------------------------------
+// PlanetXPlayer: the spaceman. A CompositeSprite of two batch sprites - the
+// side-view "body" (which flips left/right and plays the walk cycle) and the
+// "gun" (which rotates to the true 360-degree aim angle). One SceneObject means
+// the gun tracks the body with zero lag, and one body/Y-sort key.
+//
+// The player HAS-A weapon (%this.weapon): a swappable PlanetXWeapon subclass
+// that owns all firing behavior. The player builds both its sprites and its
+// weapon in onAdd, and deletes the weapon in onRemove.
+//
+// The level sets only the spawn position. Movement comes from held-key flags
+// (input.cs); aim from the mouse (input.cs). Angle convention: 0 degrees = +X,
+// counter-clockwise positive - all directional art is drawn facing +X.
+//-----------------------------------------------------------------------------
+
+$PlanetX::PlayerSpeed = 15;
+$PlanetX::PlayerMaxHealth = 100;
+$PlanetX::GunMuzzleLength = 0.9;
+
+// One footstep per foot-plant: half of the 400ms walk cycle (spacemanWalkAnim).
+$PlanetX::FootstepIntervalMs = 200;
+
+// How often the Nanite Weave auto-repair upgrade mends the suit back up.
+$PlanetX::RepairTickMs = 250;
+
+// Collision knockback. The player is driven at a fixed key-velocity, so whatever
+// velocity a physics contact adds sticks until the next key event - a hit or a
+// rock used to leave them drifting. On a contact we shove them off, then re-read
+// the held keys a moment later to snap velocity back to intent. Enemies shove hard
+// and recover slowly; solid props (rocks, walls) barely nudge and recover fast.
+$PlanetX::EnemyKnockbackSpeed = 28;
+$PlanetX::EnemyKnockbackMs = 220;
+$PlanetX::PropBounceSpeed = 6;
+$PlanetX::PropBounceMs = 70;
+$PlanetX::KnockbackCooldownMs = 260;
+
+function PlanetXPlayer::onAdd(%this)
+{
+ %this.setSceneLayer($PlanetX::EntityLayer);
+ %this.setSceneGroup($PlanetX::PlayerGroup);
+
+ // Identity: the level sets these per player (index, sprites, aim mode) in
+ // spawnPlayer. Default them so a bare PlanetXPlayer is still player 1 - the
+ // single-player path behaves exactly as before.
+ if (%this.playerIndex $= "")
+ %this.playerIndex = 1;
+ if (%this.idleImage $= "")
+ %this.idleImage = "PlanetXGame:spacemanIdle";
+ if (%this.walkAnim $= "")
+ %this.walkAnim = "PlanetXGame:spacemanWalkAnim";
+ if (%this.aimMode $= "")
+ %this.aimMode = "mouse";
+
+ // "off" layout: addSprite's args are the sprite's local position.
+ %this.setBatchLayout("off");
+ %this.setBatchSortMode("Z");
+ %this.setDefaultSpriteSize(2, 2);
+
+ // Body: drawn behind the gun (higher depth renders first).
+ %this.addSprite("0 0");
+ %this.setSpriteName("body");
+ %this.setSpriteImage(%this.idleImage);
+ %this.setSpriteDepth(1);
+
+ // Gun: pivots at the sprite center, held slightly above body center.
+ %this.addSprite("0 -0.15");
+ %this.setSpriteName("gun");
+ %this.setSpriteImage("PlanetXGame:gun");
+ %this.setSpriteSize(1.5, 0.75);
+ %this.setSpriteDepth(0);
+
+ // Feet-centric collision, feet-centric Y-sort key.
+ %this.createCircleCollisionShape(0.6, 0, -0.4);
+ %this.setCollisionGroups($PlanetX::AlienGroup SPC $PlanetX::WallGroup SPC $PlanetX::PickupGroup);
+ %this.setCollisionCallback(true);
+ %this.setSortPoint(0, -0.9);
+
+ // The body never rotates - the gun sprite carries the aim.
+ %this.setFixedAngle(true);
+
+ %this.health = $PlanetX::PlayerMaxHealth;
+ %this.moving = false;
+ %this.facingLeft = false;
+ %this.aimAngle = 0;
+ %this.downed = false;
+ %this.lastKnockback = 0;
+
+ // Upgrade-driven suit stats; PlanetXUpgrades::applyToPlayer sets the real values
+ // from this player's banked choices just below (0 = the upgrade isn't owned).
+ %this.selfDestructRadius = 0;
+ %this.autoRepairRate = 0;
+
+ // Per-player held-move flags (input.cs sets these; updateVelocity reads them).
+ %this.inUp = 0;
+ %this.inDown = 0;
+ %this.inLeft = 0;
+ %this.inRight = 0;
+
+ // The spaceman arrives armed. Swap this class to change the weapon; nothing
+ // else in the player needs to know which weapon it is holding.
+ %this.weapon = new ScriptObject()
+ {
+ class = "PlanetXBlaster";
+ superclass = "PlanetXWeapon";
+ owner = %this;
+ };
+
+ // The player's death burst - a dramatic two-tone effect built here so a death
+ // never allocates. Parked until the player falls, then fired by playDeathFx (see
+ // PlanetXGame::onPlayerDown/onPlayerDeath). Mirrors the weapon's steam vent:
+ // adding a ParticlePlayer to a scene auto-plays it, so stop it at once - and,
+ // being scene-owned, the scene frees it at teardown (the player only builds it).
+ %this.deathFx = new ParticlePlayer()
+ {
+ Particle = "PlanetXGame:playerDeath";
+ SceneLayer = $PlanetX::EffectLayer;
+ ParticleInterpolation = true;
+ };
+ %this.deathFx.setBodyType("static");
+ %this.deathFx.setCollisionSuppress(true);
+ PlanetXScene.add(%this.deathFx);
+ %this.deathFx.stop();
+
+ // Stamp this player's playthrough upgrades onto the fresh weapon and suit (the
+ // player is rebuilt every level, so upgrades are re-applied here each time), then
+ // start the auto-repair loop if that upgrade is owned.
+ PlanetXUpgrades.applyToPlayer(%this);
+ if (%this.autoRepairRate > 0)
+ %this.repairTick();
+}
+
+/// The player owns its weapon, so it deletes it. (The player's own sprites go when
+/// the scene tears down; the weapon is a ScriptObject and must be freed. The death
+/// burst is a ParticlePlayer added to the scene, so the scene frees it - deleting it
+/// here would double-free it, exactly as PlanetXWeapon::onRemove notes for the vent.)
+function PlanetXPlayer::onRemove(%this)
+{
+ if (isEventPending(%this.footstepEvent))
+ cancel(%this.footstepEvent);
+ if (isEventPending(%this.velocityResetEvent))
+ cancel(%this.velocityResetEvent);
+ if (isEventPending(%this.repairEvent))
+ cancel(%this.repairEvent);
+
+ if (isObject(%this.weapon))
+ %this.weapon.delete();
+}
+
+//-----------------------------------------------------------------------------
+// Firing is delegated to the weapon.
+//-----------------------------------------------------------------------------
+
+function PlanetXPlayer::startFiring(%this)
+{
+ if (isObject(%this.weapon))
+ %this.weapon.startFiring();
+}
+
+function PlanetXPlayer::stopFiring(%this)
+{
+ if (isObject(%this.weapon))
+ %this.weapon.stopFiring();
+}
+
+//-----------------------------------------------------------------------------
+// Aiming: flip the body toward the cursor, rotate the gun to the exact angle.
+//-----------------------------------------------------------------------------
+
+function PlanetXPlayer::setAim(%this, %angle)
+{
+ %this.aimAngle = %angle;
+
+ // Dead zone so straight-up/down aim doesn't jitter the facing on
+ // floating-point noise.
+ %x = getWord(Vector2Direction(%angle, 1), 0);
+ if (mAbs(%x) > 0.05)
+ {
+ %left = %x < 0;
+ if (%left != %this.facingLeft)
+ {
+ %this.facingLeft = %left;
+ %this.selectSpriteName("body");
+ %this.setSpriteFlipX(%left);
+ }
+ }
+
+ %this.selectSpriteName("gun");
+ %this.setSpriteAngle(%angle);
+
+ // Keep the gun right-side up when aiming left.
+ %this.setSpriteFlipY(%this.facingLeft);
+
+ // A stationary CompositeSprite does not re-render a sub-sprite's transform
+ // change until the composite itself moves (it only rebuilds its batch when
+ // spatially dirty), so the gun would freeze while the spaceman stands still.
+ // While idle, nudge the body's transform to itself to mark it dirty and
+ // refresh the batch. When walking, the movement already refreshes it.
+ if (!%this.moving)
+ %this.setPosition(%this.getPosition());
+}
+
+/// Where bullets leave the barrel, in world coordinates.
+function PlanetXPlayer::getMuzzlePosition(%this)
+{
+ %grip = Vector2Add(%this.getPosition(), "0 -0.15");
+ return Vector2Add(%grip, Vector2Direction(%this.aimAngle, $PlanetX::GunMuzzleLength));
+}
+
+//-----------------------------------------------------------------------------
+// Movement.
+//-----------------------------------------------------------------------------
+
+/// Re-derive velocity from the held-key flags. Called on every key make/break.
+function PlanetXPlayer::updateVelocity(%this)
+{
+ // A downed player is out of play until revived - ignore any held keys.
+ if (%this.downed)
+ {
+ %this.setLinearVelocity(0, 0);
+ return;
+ }
+
+ %x = %this.inRight - %this.inLeft;
+ %y = %this.inUp - %this.inDown;
+
+ if (%x == 0 && %y == 0)
+ {
+ %this.setLinearVelocity(0, 0);
+
+ if (%this.moving)
+ {
+ %this.selectSpriteName("body");
+ %this.setSpriteImage(%this.idleImage);
+ %this.moving = false;
+
+ // Halt the footstep loop the instant he stops.
+ if (isEventPending(%this.footstepEvent))
+ cancel(%this.footstepEvent);
+ }
+ return;
+ }
+
+ %length = mSqrt(%x * %x + %y * %y);
+ %this.setLinearVelocity(%x / %length * $PlanetX::PlayerSpeed,
+ %y / %length * $PlanetX::PlayerSpeed);
+
+ if (!%this.moving)
+ {
+ %this.selectSpriteName("body");
+ %this.setSpriteAnimation(%this.walkAnim);
+ %this.moving = true;
+
+ // Kick off the footstep loop (plays now, then reschedules while walking).
+ %this.footstep();
+ }
+}
+
+/// Repeating footstep while the spaceman walks. Self-cancels when he stops moving
+/// or the game leaves play; the pending event is also cancelled in onRemove and
+/// when movement stops.
+function PlanetXPlayer::footstep(%this)
+{
+ if ($PlanetX::state !$= "playing" || !%this.moving)
+ return;
+
+ Audio.PlaySound("PlanetXGame:footstep");
+ %this.footstepEvent = %this.schedule($PlanetX::FootstepIntervalMs, "footstep");
+}
+
+//-----------------------------------------------------------------------------
+// Collision knockback: shove off whatever we hit, then re-read the held keys a
+// moment later so a contact never leaves the player drifting. A per-player
+// cooldown keeps a lingering contact from re-shoving every physics step (the same
+// reason enemy contact damage has one).
+//-----------------------------------------------------------------------------
+
+function PlanetXPlayer::onCollision(%this, %object, %collisionDetails)
+{
+ if (%this.downed)
+ return;
+
+ %now = getSimTime();
+ if (%now - %this.lastKnockback < $PlanetX::KnockbackCooldownMs)
+ return;
+
+ if (%object.isEnemy)
+ {
+ %this.lastKnockback = %now;
+ %this.shoveFrom(%object, $PlanetX::EnemyKnockbackSpeed, $PlanetX::EnemyKnockbackMs);
+ }
+ else if (%object.getSceneGroup() == $PlanetX::WallGroup)
+ {
+ %this.lastKnockback = %now;
+ %this.shoveFrom(%object, $PlanetX::PropBounceSpeed, $PlanetX::PropBounceMs);
+ }
+ // Sensors (the crystal) don't knock the player around.
+}
+
+/// Push the player directly away from %object at %speed, then re-derive velocity
+/// from the held keys after %resetMs so control snaps back and the shove doesn't
+/// linger as drift.
+function PlanetXPlayer::shoveFrom(%this, %object, %speed, %resetMs)
+{
+ %away = mAtan(Vector2Sub(%this.getPosition(), %object.getPosition()));
+ %this.setLinearVelocityPolar(%away, %speed);
+
+ if (isEventPending(%this.velocityResetEvent))
+ cancel(%this.velocityResetEvent);
+ %this.velocityResetEvent = %this.schedule(%resetMs, "updateVelocity");
+}
+
+//-----------------------------------------------------------------------------
+// Damage.
+//-----------------------------------------------------------------------------
+
+function PlanetXPlayer::takeDamage(%this, %amount)
+{
+ if ($PlanetX::state !$= "playing" || %this.downed)
+ return;
+
+ %this.health -= %amount;
+
+ %level = PlanetXGame.level;
+ if (isObject(%level) && isObject(%level.hud))
+ %level.hud.setHealth(%this.playerIndex, %this.health);
+
+ // Hit feedback: coral flash and a camera jolt. Object-level blend color
+ // does not tint batch sprites, so flash each sprite individually.
+ %this.flash("1 0.45 0.45 1");
+ %this.schedule(120, "flash", "1 1 1 1");
+ PlanetXWindow.startCameraShake(4, 0.3);
+
+ if (%this.health <= 0)
+ PlanetXGame.onPlayerDown(%this);
+ else
+ Audio.PlaySound("PlanetXGame:playerHurt");
+}
+
+function PlanetXPlayer::flash(%this, %color)
+{
+ %this.selectSpriteName("body");
+ %this.setSpriteBlendColor(%color);
+ %this.selectSpriteName("gun");
+ %this.setSpriteBlendColor(%color);
+}
+
+/// The body bursts as the player falls - fire their owned death effect at their
+/// feet. Called from PlanetXGame::onPlayerDown (co-op) and ::onPlayerDeath (terminal).
+function PlanetXPlayer::playDeathFx(%this)
+{
+ %this.deathFx.setPosition(%this.getPosition());
+ %this.deathFx.play(true);
+}
+
+/// Auto-repair upgrade (Nanite Weave): while alive and in play, mend the suit back
+/// toward full a little each tick. Self-reschedules like the weapon's heat loop -
+/// started in onAdd only when the rate is above zero, cancelled in onRemove, and
+/// stopped whenever the game leaves play. A downed player and a full hull are no-ops
+/// but keep the loop alive so it resumes on revive / after the next hit.
+function PlanetXPlayer::repairTick(%this)
+{
+ if ($PlanetX::state !$= "playing")
+ return;
+
+ %this.repairEvent = %this.schedule($PlanetX::RepairTickMs, "repairTick");
+
+ // Keep the loop alive across a pause, but don't mend while frozen.
+ if ($PlanetX::paused || %this.downed || %this.health >= $PlanetX::PlayerMaxHealth)
+ return;
+
+ %this.health = mClamp(%this.health + %this.autoRepairRate * $PlanetX::RepairTickMs / 1000,
+ 0, $PlanetX::PlayerMaxHealth);
+
+ %level = PlanetXGame.level;
+ if (isObject(%level) && isObject(%level.hud))
+ %level.hud.setHealth(%this.playerIndex, %this.health);
+}
+
+//-----------------------------------------------------------------------------
+// Downed / revive (co-op). A killed player is downed - hidden and out of play -
+// until a living teammate reaches the rocket and revives them (see camera.cs and
+// PlanetXLevel::revivePlayer). In single-player nothing calls these.
+//-----------------------------------------------------------------------------
+
+/// Take this player out of play: stop firing/moving, hide, and drop out of
+/// collisions so aliens ignore the empty body.
+function PlanetXPlayer::goDown(%this)
+{
+ // Dead Man's Payload: detonate a clearing blast on the way down (co-op only, and
+ // only if this player owns the upgrade).
+ if (%this.selfDestructRadius > 0)
+ %this.detonate();
+
+ %this.downed = true;
+ %this.downedAt = getSimTime(); // the co-op camera lingers here for a grace beat
+ %this.stopFiring();
+ %this.setLinearVelocity(0, 0);
+
+ %this.inUp = 0;
+ %this.inDown = 0;
+ %this.inLeft = 0;
+ %this.inRight = 0;
+
+ %this.moving = false;
+ if (isEventPending(%this.footstepEvent))
+ cancel(%this.footstepEvent);
+ if (isEventPending(%this.velocityResetEvent))
+ cancel(%this.velocityResetEvent);
+
+ %this.setVisible(false);
+ %this.setCollisionSuppress(true);
+}
+
+/// Bring a downed player back at %position with full health.
+function PlanetXPlayer::revive(%this, %position)
+{
+ %this.setPosition(%position);
+ %this.setLinearVelocity(0, 0);
+ %this.health = $PlanetX::PlayerMaxHealth;
+ %this.downed = false;
+
+ %this.setVisible(true);
+ %this.setCollisionSuppress(false);
+
+ // Back to the idle look; aim refreshes on the next input/aim tick.
+ %this.selectSpriteName("body");
+ %this.setSpriteImage(%this.idleImage);
+ %this.moving = false;
+
+ %level = PlanetXGame.level;
+ if (isObject(%level) && isObject(%level.hud))
+ %level.hud.setHealth(%this.playerIndex, %this.health);
+}
+
+/// Self-destruct upgrade (Dead Man's Payload): wipe out every enemy within
+/// selfDestructRadius of where this player fell. Walks the level's live-enemy set
+/// from the top down because a kill removes the enemy from the set; overkill damage
+/// is fine (takeDamage no-ops once an enemy is dead).
+function PlanetXPlayer::detonate(%this)
+{
+ %level = PlanetXGame.level;
+ if (!isObject(%level) || !isObject(%level.enemies))
+ return;
+
+ %origin = %this.getPosition();
+
+ for (%i = %level.enemies.getCount() - 1; %i >= 0; %i--)
+ {
+ %enemy = %level.enemies.getObject(%i);
+ if (isObject(%enemy)
+ && Vector2Length(Vector2Sub(%enemy.getPosition(), %origin)) <= %this.selfDestructRadius)
+ %enemy.takeDamage(99999);
+ }
+
+ %level.playBurst(%origin);
+ PlanetXWindow.startCameraShake(6, 0.4);
+}
diff --git a/PlanetX/PlanetXGame/scripts/rock.cs b/PlanetX/PlanetXGame/scripts/rock.cs
new file mode 100644
index 000000000..8d2a601a3
--- /dev/null
+++ b/PlanetX/PlanetXGame/scripts/rock.cs
@@ -0,0 +1,19 @@
+//-----------------------------------------------------------------------------
+// PlanetXRock: a static boulder. The level sets its position, size, and which
+// of the two rock images (%this.variant); the rock derives its feet-centric
+// collision shape and Y-sort key from that size.
+//-----------------------------------------------------------------------------
+
+function PlanetXRock::onAdd(%this)
+{
+ %size = getWord(%this.getSize(), 0);
+
+ %this.setSceneLayer($PlanetX::EntityLayer);
+ %this.setSceneGroup($PlanetX::WallGroup);
+ %this.setImage("PlanetXGame:rock" @ %this.variant);
+ %this.setBodyType("static");
+
+ // Feet-centric: collision and Y-sort key sit at the boulder's base.
+ %this.createCircleCollisionShape(%size * 0.33, 0, -%size * 0.15);
+ %this.setSortPoint(0, -%size * 0.4);
+}
diff --git a/PlanetX/PlanetXGame/scripts/rocket.cs b/PlanetX/PlanetXGame/scripts/rocket.cs
new file mode 100644
index 000000000..2fc81d9c6
--- /dev/null
+++ b/PlanetX/PlanetXGame/scripts/rocket.cs
@@ -0,0 +1,24 @@
+//-----------------------------------------------------------------------------
+// PlanetXRocket: the spaceman's crashed rocket - a tall landmark at the spawn
+// corner with a small collision footprint at its base. The level sets only its
+// position.
+//-----------------------------------------------------------------------------
+
+function PlanetXRocket::onAdd(%this)
+{
+ %this.setSize("6 12");
+ %this.setSceneLayer($PlanetX::EntityLayer);
+ %this.setSceneGroup($PlanetX::WallGroup);
+ %this.setImage("PlanetXGame:rocket");
+ %this.setBodyType("static");
+ %this.createPolygonBoxCollisionShape(3.5, 2.5, "0 -4");
+ %this.setSortPoint(0, -5);
+}
+
+/// The door - center-bottom of the rocket sprite, where a spaceman walks in. This
+/// is where the surviving player revives a downed teammate (see camera.cs).
+function PlanetXRocket::getDoorPosition(%this)
+{
+ %halfHeight = getWord(%this.getSize(), 1) * 0.5;
+ return Vector2Add(%this.getPosition(), "0" SPC (-%halfHeight));
+}
diff --git a/PlanetX/PlanetXGame/scripts/sceneWindow.cs b/PlanetX/PlanetXGame/scripts/sceneWindow.cs
new file mode 100644
index 000000000..4d9224eb6
--- /dev/null
+++ b/PlanetX/PlanetXGame/scripts/sceneWindow.cs
@@ -0,0 +1,21 @@
+//-----------------------------------------------------------------------------
+// PlanetXSceneWindow: the camera window onto the level. Keeps the camera height
+// fixed and fits its width to the window's aspect ratio, so resizing widens or
+// narrows the view instead of stretching it (same pattern as the Sandbox).
+//-----------------------------------------------------------------------------
+
+function PlanetXSceneWindow::updateCameraAspect(%this)
+{
+ %extent = Canvas.extent;
+ %aspect = %extent.x / %extent.y;
+
+ %camera = %this.getCameraSize();
+ %camera.x = %camera.y * %aspect;
+ %this.setCameraSize(%camera);
+}
+
+/// Engine callback: fires on every live window resize.
+function PlanetXSceneWindow::onExtentChange(%this)
+{
+ %this.updateCameraAspect();
+}
diff --git a/PlanetX/PlanetXGame/scripts/settings.cs b/PlanetX/PlanetXGame/scripts/settings.cs
new file mode 100644
index 000000000..95769ba2b
--- /dev/null
+++ b/PlanetX/PlanetXGame/scripts/settings.cs
@@ -0,0 +1,130 @@
+//-----------------------------------------------------------------------------
+// PlanetXSettings: the game's user preferences - sound volumes, per-player key
+// bindings, and per-player aim mode. A session-long singleton (like
+// PlanetXUpgrades), reachable by name from any file. onAdd seeds a default for
+// anything unset, loads the saved prefs file over the defaults, and pushes the
+// volume levels into the shared Audio module. save() writes the prefs back out;
+// the options screen calls it whenever a setting changes.
+//
+// Everything lives in the $pref::PlanetX:: namespace so one export() captures it
+// all, mirroring the engine's own preference pattern (toybox/Sandbox/1/main.cs).
+// Bindings are read and written by key name through get()/set() so the options
+// screen and key-capture control can drive them generically. See TORQUE_SCRIPT.md.
+//-----------------------------------------------------------------------------
+
+// Saved next to the executable (working dir), exactly as the Sandbox toy does.
+$PlanetX::PrefsFile = "PlanetXPrefs.cs";
+
+function PlanetXSettings::onAdd(%this)
+{
+ %this.applyDefaults();
+
+ // Saved prefs, loaded after the defaults, win.
+ if (isFile($PlanetX::PrefsFile))
+ exec($PlanetX::PrefsFile);
+
+ %this.applyAudio();
+}
+
+/// Persist every PlanetX pref to disk in one shot. Called on any settings change
+/// and once more on shutdown (see PlanetXGame::destroy).
+function PlanetXSettings::save(%this)
+{
+ export("$pref::PlanetX::*", $PlanetX::PrefsFile, false);
+}
+
+//-----------------------------------------------------------------------------
+// Defaults. Seed only what is still blank so a first run - or a newly added
+// setting on an existing install - always has a sensible value, while saved prefs
+// loaded afterward take precedence.
+//-----------------------------------------------------------------------------
+
+function PlanetXSettings::applyDefaults(%this)
+{
+ %this.setDefault("MasterVolume", 1);
+ %this.setDefault("MusicVolume", 0.6);
+ %this.setDefault("SoundVolume", 1);
+
+ // Player 1: arrow keys; fires with Return (used when P1 aims automatically -
+ // in mouse mode the mouse fires); aims with the mouse.
+ %this.setDefault("P1Up", "up");
+ %this.setDefault("P1Down", "down");
+ %this.setDefault("P1Left", "left");
+ %this.setDefault("P1Right", "right");
+ %this.setDefault("P1Fire", "return");
+ %this.setDefault("P1Aim", "mouse");
+
+ // Player 2 (co-op): WASD; fires with Space; aims automatically (no second mouse).
+ // Distinct fire keys keep the two players from cross-firing.
+ %this.setDefault("P2Up", "w");
+ %this.setDefault("P2Down", "s");
+ %this.setDefault("P2Left", "a");
+ %this.setDefault("P2Right", "d");
+ %this.setDefault("P2Fire", "space");
+ %this.setDefault("P2Aim", "auto");
+}
+
+//-----------------------------------------------------------------------------
+// Generic pref access by key name. The names are built at runtime (the options
+// screen iterates over "P1Up", "P2Fire", ...), so we reach the $pref:: globals
+// through eval - the same idiom the Sandbox uses for dynamic setters.
+//-----------------------------------------------------------------------------
+
+/// Read $pref::PlanetX::.
+function PlanetXSettings::get(%this, %key)
+{
+ return eval("return $pref::PlanetX::" @ %key @ ";");
+}
+
+/// Write $pref::PlanetX::.
+function PlanetXSettings::set(%this, %key, %value)
+{
+ eval("$pref::PlanetX::" @ %key @ " = \"" @ %value @ "\";");
+}
+
+/// Write the key only if it is currently blank.
+function PlanetXSettings::setDefault(%this, %key, %value)
+{
+ if (%this.get(%key) $= "")
+ %this.set(%key, %value);
+}
+
+/// The display label for a bound key. The pref stores the raw action string (e.g.
+/// "up" / "space"); the rebind buttons show it uppercased.
+function PlanetXSettings::keyLabel(%this, %key)
+{
+ return strupr(%key);
+}
+
+/// The action (pref key, e.g. "P2Up") currently bound to %key, ignoring %exceptKey,
+/// or "" if none. The key-capture control uses this to SWAP bindings: when a key is
+/// reassigned, whatever action held it takes over the key being replaced, so no two
+/// actions ever share one key.
+function PlanetXSettings::actionForKey(%this, %key, %exceptKey)
+{
+ %actions = "P1Up" TAB "P1Down" TAB "P1Left" TAB "P1Right" TAB "P1Fire" TAB
+ "P2Up" TAB "P2Down" TAB "P2Left" TAB "P2Right" TAB "P2Fire";
+
+ for (%i = 0; %i < getFieldCount(%actions); %i++)
+ {
+ %action = getField(%actions, %i);
+ if (%action $= %exceptKey)
+ continue;
+ if (%this.get(%action) $= %key)
+ return %action;
+ }
+
+ return "";
+}
+
+//-----------------------------------------------------------------------------
+// Audio. Push the saved volume levels into the shared Audio module. Called once at
+// startup and again by each volume slider as it moves (via Audio directly).
+//-----------------------------------------------------------------------------
+
+function PlanetXSettings::applyAudio(%this)
+{
+ Audio.setMasterVolume($pref::PlanetX::MasterVolume);
+ Audio.SetMusicVolume($pref::PlanetX::MusicVolume);
+ Audio.SetSoundVolume($pref::PlanetX::SoundVolume);
+}
diff --git a/PlanetX/PlanetXGame/scripts/tileMap.cs b/PlanetX/PlanetXGame/scripts/tileMap.cs
new file mode 100644
index 000000000..1630aab2f
--- /dev/null
+++ b/PlanetX/PlanetXGame/scripts/tileMap.cs
@@ -0,0 +1,141 @@
+//-----------------------------------------------------------------------------
+// PlanetXTileMap: the terrain - a CompositeSprite of 96x72 tiles, each tinted
+// from a Perlin noise field so the ground reads as a rocky alien surface. The
+// level passes in its noise generator (%this.generator); the tile map builds
+// its whole grid in onAdd.
+//
+// Noise is sampled once per grid VERTEX ((N+1)x(M+1) samples); each tile's four
+// corners take the color of the vertices it shares with its neighbors, so the
+// Gouraud interpolation lines up seamlessly across tile seams.
+//-----------------------------------------------------------------------------
+
+// Tile grid: 96x72 tiles of 2 units, spanning -96..96 x -72..72.
+$PlanetX::TileSize = 2;
+$PlanetX::TileCountX = 96;
+$PlanetX::TileCountY = 72;
+
+// Perlin terrain tuning.
+$PlanetX::NoiseZoom = 0.06;
+$PlanetX::NoiseOctaves = 4;
+$PlanetX::NoisePersistence = 0.5;
+
+function PlanetXTileMap::onAdd(%this)
+{
+ %startTime = getRealTime();
+
+ %countX = $PlanetX::TileCountX;
+ %countY = $PlanetX::TileCountY;
+
+ %this.setSceneLayer($PlanetX::TileLayer);
+
+ // Pass 1: one ramp color per grid vertex.
+ for (%cy = 0; %cy <= %countY; %cy++)
+ {
+ for (%cx = 0; %cx <= %countX; %cx++)
+ {
+ %value = %this.generator.getComplexNoise(%cx * $PlanetX::NoiseZoom,
+ %cy * $PlanetX::NoiseZoom, $PlanetX::NoiseOctaves,
+ $PlanetX::NoisePersistence);
+ %corner[%cx, %cy] = %this.rocketRampColor(%value);
+ }
+ }
+
+ // Pass 2: the tile batch, one shared corner color per touching tile.
+ // Layout must be set before any sprite is added.
+ %this.setBatchLayout("rect");
+ %this.setBatchCulling(true);
+ %this.setBatchSortMode("Batch");
+ %this.setDefaultSpriteStride($PlanetX::TileSize, $PlanetX::TileSize);
+ %this.setDefaultSpriteSize($PlanetX::TileSize, $PlanetX::TileSize);
+
+ // Logical coords are scaled by the stride; offsetting the composite by half
+ // a tile keeps the grid span exactly on the world bounds.
+ %this.setPosition($PlanetX::TileSize / 2, $PlanetX::TileSize / 2);
+
+ %halfX = %countX / 2;
+ %halfY = %countY / 2;
+
+ for (%x = -%halfX; %x < %halfX; %x++)
+ {
+ for (%y = -%halfY; %y < %halfY; %y++)
+ {
+ %this.addSprite(%x SPC %y);
+ %this.setSpriteImage("PlanetXGame:tiles", %this.pickTileFrame());
+
+ // This tile's bottom-left grid vertex.
+ %cx = %x + %halfX;
+ %cy = %y + %halfY;
+
+ // Corner order is TL, TR, BR, BL; world +Y is up.
+ %this.setSpriteUseComplexColor(true);
+ %this.setSpriteComplexColor(
+ %corner[%cx, %cy + 1], %corner[%cx + 1, %cy + 1],
+ %corner[%cx + 1, %cy], %corner[%cx, %cy]);
+ }
+ }
+
+ %this.setBodyType("static");
+ %this.setCollisionSuppress(true);
+
+ echo("PlanetX: terrain built in" SPC getRealTime() - %startTime SPC "ms");
+}
+
+/// Map a 0..1 noise value onto the Rocket Edition palette, dark to light.
+/// Returns an "r g b 1" float color string for setSpriteComplexColor.
+function PlanetXTileMap::rocketRampColor(%this, %value)
+{
+ // Multi-octave noise clusters around 0.5; stretch for contrast.
+ %value = mClamp((%value - 0.2) / 0.6, 0, 1);
+
+ // Ramp stops: position, r, g, b (0..1 floats).
+ // #300022 -> #801946 -> #A62646 -> #C43C3E -> #F2D7DA
+ if (%value < 0.3)
+ {
+ %t = %value / 0.3;
+ %from = "0.188 0.0 0.133";
+ %to = "0.502 0.098 0.275";
+ }
+ else if (%value < 0.55)
+ {
+ %t = (%value - 0.3) / 0.25;
+ %from = "0.502 0.098 0.275";
+ %to = "0.651 0.149 0.275";
+ }
+ else if (%value < 0.8)
+ {
+ %t = (%value - 0.55) / 0.25;
+ %from = "0.651 0.149 0.275";
+ %to = "0.769 0.235 0.243";
+ }
+ else
+ {
+ %t = (%value - 0.8) / 0.2;
+ %from = "0.769 0.235 0.243";
+ %to = "0.949 0.843 0.855";
+ }
+
+ %r = getWord(%from, 0) + (getWord(%to, 0) - getWord(%from, 0)) * %t;
+ %g = getWord(%from, 1) + (getWord(%to, 1) - getWord(%from, 1)) * %t;
+ %b = getWord(%from, 2) + (getWord(%to, 2) - getWord(%from, 2)) * %t;
+
+ return %r SPC %g SPC %b SPC "1";
+}
+
+/// Weighted pick over the 16-frame tile sheet: mostly plain white with
+/// occasional speckle, cracks, pebbles, and rubble details.
+function PlanetXTileMap::pickTileFrame(%this)
+{
+ %roll = getRandom(0, 99);
+
+ if (%roll < 50)
+ return getRandom(0, 2); // plain
+ if (%roll < 75)
+ return getRandom(3, 5); // barely-there speckle
+ if (%roll < 88)
+ return getRandom(6, 9); // speckle
+ if (%roll < 93)
+ return getRandom(10, 11); // hairline cracks
+ if (%roll < 97)
+ return getRandom(12, 13); // pebbles
+ return getRandom(14, 15); // rubble
+}
diff --git a/PlanetX/PlanetXGame/scripts/upgrades.cs b/PlanetX/PlanetXGame/scripts/upgrades.cs
new file mode 100644
index 000000000..69af7f6f7
--- /dev/null
+++ b/PlanetX/PlanetXGame/scripts/upgrades.cs
@@ -0,0 +1,157 @@
+//-----------------------------------------------------------------------------
+// PlanetXUpgrades: the weapon-upgrade catalog and the playthrough's upgrade state.
+// A session singleton (built in PlanetXGame::create) that knows every upgrade - its
+// title, description, card image, whether it is co-op only, and how many times it
+// can be taken before the weapon caps out - and remembers how many times each of the
+// (up to two) players has taken each one THIS RUN.
+//
+// The counts are the whole model: a player's weapon and suit are a pure function of
+// them (applyToPlayer), recomputed from scratch each level because the player is
+// rebuilt every level. reset() wipes the counts for a fresh run; the victory screen
+// asks offer() what to show and take() to bank a choice. See TORQUE_SCRIPT.md.
+//-----------------------------------------------------------------------------
+
+function PlanetXUpgrades::onAdd(%this)
+{
+ %this.keys = "";
+
+ // define(key, title, description, coOpOnly, maxCount) - maxCount -1 = uncapped.
+ // The caps mirror the stat floors/ceilings in applyToPlayer: once a player has
+ // taken an upgrade maxCount times its stat is pinned and it stops being offered.
+ // Damage and Nanite Weave stay uncapped (always offerable in solo), and Dead
+ // Man's Payload is uncapped in co-op, so the offer pool can never run dry.
+ %this.define("damage", "Capacitor Overload", "Each shot deals +0.5 damage.", false, -1);
+ %this.define("firerate", "Cycle Accelerator", "Fire 15ms faster between shots.", false, 8);
+ %this.define("lessheat", "Cryo-Coated Barrel", "Each shot produces less heat.", false, 6);
+ %this.define("ventfast", "Coolant Vents", "Vent heat faster after firing.", false, 6);
+ %this.define("maxheat", "Reinforced Heat Sink", "Store more heat before overheating.", false, 5);
+ %this.define("split", "Fork Emitter", "Fire one extra bolt each shot.", false, 6);
+ %this.define("tighten", "Focusing Array", "Tighten the angle between bolts by 1 degree.", false, 5);
+ %this.define("selfdestruct", "Dead Man's Payload", "When you fall, a blast destroys nearby enemies.", true, -1);
+ %this.define("autorepair", "Nanite Weave", "Slowly repair your suit over time.", false, -1);
+}
+
+/// Register one upgrade in the catalog.
+function PlanetXUpgrades::define(%this, %key, %title, %desc, %coOpOnly, %maxCount)
+{
+ %this.keys = (%this.keys $= "") ? %key : (%this.keys SPC %key);
+ %this.title[%key] = %title;
+ %this.desc[%key] = %desc;
+ // Card art follows the convention gui/images/upgrade_.png -> asset id
+ // PlanetXGame:upgrade_. Each ships as a copy of the placeholder; drop real
+ // art onto the file to replace it (no code change needed).
+ %this.image[%key] = "PlanetXGame:upgrade_" @ %key;
+ %this.coOpOnly[%key] = %coOpOnly;
+ %this.maxCount[%key] = %maxCount;
+}
+
+/// Clear every player's counts - a brand-new run starts with the stock blaster.
+function PlanetXUpgrades::reset(%this)
+{
+ %n = getWordCount(%this.keys);
+ for (%i = 0; %i < %n; %i++)
+ {
+ %key = getWord(%this.keys, %i);
+ %this.count[1, %key] = 0;
+ %this.count[2, %key] = 0;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Eligibility and the end-of-level offer.
+//-----------------------------------------------------------------------------
+
+/// Can player %pi still take %key? False if it is co-op only in a solo run, if its
+/// prerequisite is unmet, or if this player has already hit its cap.
+function PlanetXUpgrades::isEligible(%this, %key, %pi)
+{
+ if (%this.coOpOnly[%key] && !$PlanetX::twoPlayer)
+ return false;
+
+ // Focusing the fan only makes sense once this player has at least two bolts, i.e.
+ // has taken the split-shot upgrade at least once.
+ if (%key $= "tighten" && %this.count[%pi, "split"] < 1)
+ return false;
+
+ %cap = %this.maxCount[%key];
+ if (%cap >= 0 && %this.count[%pi, %key] >= %cap)
+ return false;
+
+ return true;
+}
+
+/// Is %key worth offering at all - can either participating player still take it? A
+/// card offered on this basis is always claimable by someone; applyToPlayer clamps
+/// every stat, so a claim that happens to be a no-op for one player is harmless.
+function PlanetXUpgrades::eligibleForAny(%this, %key)
+{
+ if (%this.isEligible(%key, 1))
+ return true;
+ if ($PlanetX::twoPlayer && %this.isEligible(%key, 2))
+ return true;
+ return false;
+}
+
+/// Pick %needed distinct upgrade keys (2 solo, 3 co-op) to offer on the victory
+/// screen, at random from the still-takeable pool. Returns a space-separated key
+/// list; if the pool is somehow smaller than %needed the caller shows fewer cards.
+function PlanetXUpgrades::offer(%this, %needed)
+{
+ %pool = "";
+ %n = getWordCount(%this.keys);
+ for (%i = 0; %i < %n; %i++)
+ {
+ %key = getWord(%this.keys, %i);
+ if (%this.eligibleForAny(%key))
+ %pool = (%pool $= "") ? %key : (%pool SPC %key);
+ }
+
+ // Draw without replacement: pull a random word out of the pool %needed times.
+ %result = "";
+ for (%k = 0; %k < %needed && getWordCount(%pool) > 0; %k++)
+ {
+ %j = getRandom(0, getWordCount(%pool) - 1);
+ %key = getWord(%pool, %j);
+ %result = (%result $= "") ? %key : (%result SPC %key);
+ %pool = removeWord(%pool, %j);
+ }
+
+ return %result;
+}
+
+//-----------------------------------------------------------------------------
+// Banking a choice and applying the whole set to a player.
+//-----------------------------------------------------------------------------
+
+/// Record that player %pi chose %key (persists for the rest of the run).
+function PlanetXUpgrades::take(%this, %key, %pi)
+{
+ %this.count[%pi, %key] = %this.count[%pi, %key] + 1;
+}
+
+/// Re-derive %player's weapon and suit stats from its stored counts. Called from the
+/// player's onAdd every level, right after it builds its weapon. Each upgrade is a
+/// fixed step per pick, clamped to its floor/ceiling; the bullet pool is (re)built
+/// last, now that the final fire rate and bolt count are known.
+function PlanetXUpgrades::applyToPlayer(%this, %player)
+{
+ %pi = %player.playerIndex;
+ %w = %player.weapon;
+ if (!isObject(%w))
+ return;
+
+ // Weapon stats (deltas from the tuned blaster base already on the weapon).
+ %w.bulletDamage = %w.bulletDamage + 0.5 * %this.count[%pi, "damage"];
+ %w.fireCooldown = mClamp(%w.fireCooldown - 15 * %this.count[%pi, "firerate"], 80, %w.fireCooldown);
+ %w.heatPerShot = mClamp(%w.heatPerShot - 0.015 * %this.count[%pi, "lessheat"], 0.04, %w.heatPerShot);
+ %w.heatDecayPerSecond = mClamp(%w.heatDecayPerSecond + 0.08 * %this.count[%pi, "ventfast"], %w.heatDecayPerSecond, 0.80);
+ %w.maxHeat = mClamp(%w.maxHeat + 0.2 * %this.count[%pi, "maxheat"], %w.maxHeat, 2.0);
+ %w.bulletCount = mClamp(%w.bulletCount + %this.count[%pi, "split"], %w.bulletCount, 7);
+ %w.spreadAngle = mClamp(%w.spreadAngle - %this.count[%pi, "tighten"], 5, %w.spreadAngle);
+
+ // Suit upgrades live on the player, not the weapon.
+ %player.selfDestructRadius = 8 * %this.count[%pi, "selfdestruct"];
+ %player.autoRepairRate = 1.5 * %this.count[%pi, "autorepair"];
+
+ %w.buildBulletPool();
+}
diff --git a/PlanetX/PlanetXGame/scripts/weapon.cs b/PlanetX/PlanetXGame/scripts/weapon.cs
new file mode 100644
index 000000000..7dc966f19
--- /dev/null
+++ b/PlanetX/PlanetXGame/scripts/weapon.cs
@@ -0,0 +1,265 @@
+//-----------------------------------------------------------------------------
+// PlanetXWeapon: the base class for the player's weapon. It owns everything to
+// do with shooting - a pooled set of bullets, the overheat steam vent, the fire
+// cadence, and the gun-heat cooldown - so a different weapon is a drop-in swap
+// (see PlanetXBlaster). The player calls startFiring/stopFiring and never needs
+// to know which concrete weapon it holds.
+//
+// Because only the most-derived ::onAdd fires, shared setup lives in init(): a
+// concrete subclass's onAdd calls %this.init() first, then overrides stats.
+// See TORQUE_SCRIPT.md.
+//-----------------------------------------------------------------------------
+
+/// Shared setup: base stats, the steam vent, and the heat loop. A subclass
+/// overrides the stats in its onAdd (after init); playthrough upgrades then adjust
+/// them once more (PlanetXUpgrades::applyToPlayer). The BULLET POOL is built last -
+/// by applyToPlayer - because its size depends on the final fire rate and bolt
+/// count, which upgrades are still free to change.
+function PlanetXWeapon::init(%this)
+{
+ // Base stats. A subclass overrides these in its onAdd, after init().
+ %this.fireCooldown = 250;
+ %this.bulletSpeed = 40;
+ %this.bulletLife = 1200;
+ %this.heatPerShot = 0.13;
+ %this.heatDecayPerSecond = 0.32;
+ %this.heatTickMs = 100;
+ %this.heatResumeThreshold = 0.35;
+
+ // Upgrade-driven stats (see PlanetXUpgrades). These defaults are the un-upgraded
+ // blaster: one bolt per shot, one damage, a 10-degree fan once split, and a heat
+ // ceiling of 1. Upgrades add to bulletDamage/bulletCount and maxHeat, and shave
+ // fireCooldown/heatPerShot/spreadAngle down toward their floors.
+ %this.bulletDamage = 1;
+ %this.bulletCount = 1;
+ %this.spreadAngle = 10;
+ %this.maxHeat = 1.0;
+
+ %this.buildSteamVent();
+
+ %this.lastFireTime = 0;
+ %this.firing = false;
+ %this.resetHeat();
+ %this.heatTick();
+}
+
+function PlanetXWeapon::onAdd(%this)
+{
+ %this.init();
+}
+
+/// The weapon owns the fire/heat SCHEDULES, so it cancels them. The bullets and
+/// steam vent are SceneObjects: adding them to the scene handed their lifetime
+/// to the scene, which safeDeletes them when it tears down (this weapon is freed
+/// as part of that same teardown). Deleting them here would double-free them.
+function PlanetXWeapon::onRemove(%this)
+{
+ %this.stopFiring();
+ if (isEventPending(%this.heatEvent))
+ cancel(%this.heatEvent);
+}
+
+//-----------------------------------------------------------------------------
+// Pools. Bullets and the steam vent are pre-allocated so firing never allocates
+// mid-play (same pattern as TruckToy's projectile pool).
+//-----------------------------------------------------------------------------
+
+/// Pre-allocate the bullet pool, sized to the weapon's FINAL stats so a fast,
+/// many-bolt gun never runs its pool dry mid-play. The most bolts that can be alive
+/// at once is one volley (bulletCount) for each fire that is still within bulletLife
+/// of now (bulletLife / fireCooldown of them), plus a little slack. Called once, by
+/// applyToPlayer, after upgrades have settled the fire rate and bolt count.
+function PlanetXWeapon::buildBulletPool(%this)
+{
+ %volleys = mFloor(%this.bulletLife / %this.fireCooldown) + 1;
+ %this.bulletPoolSize = %volleys * %this.bulletCount + 4;
+
+ for (%i = 0; %i < %this.bulletPoolSize; %i++)
+ {
+ %bullet = new Sprite() { class = "PlanetXBullet"; };
+ PlanetXScene.add(%bullet);
+ %bullet.park();
+ %this.bullet[%i] = %bullet;
+ }
+ %this.nextBullet = 0;
+}
+
+function PlanetXWeapon::buildSteamVent(%this)
+{
+ %steam = new ParticlePlayer()
+ {
+ Particle = "PlanetXGame:steam";
+ SceneLayer = $PlanetX::EffectLayer;
+ ParticleInterpolation = true;
+ SizeScale = 1.3;
+ };
+ %steam.setBodyType("static");
+ %steam.setCollisionSuppress(true);
+ PlanetXScene.add(%steam);
+ %steam.stop();
+
+ %this.steam = %steam;
+}
+
+//-----------------------------------------------------------------------------
+// Firing. The owner's angle is already aimed at the cursor (input.cs).
+//-----------------------------------------------------------------------------
+
+function PlanetXWeapon::startFiring(%this)
+{
+ %this.firing = true;
+ %this.fireTick();
+}
+
+function PlanetXWeapon::stopFiring(%this)
+{
+ %this.firing = false;
+ if (isEventPending(%this.fireEvent))
+ cancel(%this.fireEvent);
+}
+
+/// Autofire loop while the trigger is held.
+function PlanetXWeapon::fireTick(%this)
+{
+ if (!%this.firing || $PlanetX::state !$= "playing")
+ return;
+
+ %this.fire();
+ %this.fireEvent = %this.schedule(%this.fireCooldown, "fireTick");
+}
+
+function PlanetXWeapon::fire(%this)
+{
+ if ($PlanetX::state !$= "playing" || !isObject(%this.owner))
+ return;
+
+ // An overheated gun stays locked until it cools (see heatTick).
+ if (%this.overheated)
+ return;
+
+ %now = getSimTime();
+ if (%now - %this.lastFireTime < %this.fireCooldown)
+ return;
+ %this.lastFireTime = %now;
+
+ // One volley leaves the barrel together: bulletCount bolts in a fan centered on
+ // the aim, spreadAngle degrees apart. The split-shot upgrade adds bolts; the
+ // focusing upgrade tightens the fan. A single-bolt gun is just the count == 1 case.
+ %muzzle = %this.owner.getMuzzlePosition();
+ %aim = %this.owner.aimAngle;
+ %count = %this.bulletCount;
+
+ for (%i = 0; %i < %count; %i++)
+ {
+ %angle = %aim + (%i - (%count - 1) / 2) * %this.spreadAngle;
+ %this.launchBolt(%muzzle, %angle);
+ }
+
+ Audio.PlaySound("PlanetXGame:laser");
+
+ // Heat is charged once per shot, not per bolt, so the extra split bolts are free
+ // (by design - see PlanetXUpgrades).
+ %this.addHeat(%this.heatPerShot);
+}
+
+/// Fire one pooled bolt from %muzzle along %angle, stamped with the weapon's current
+/// per-shot damage. Pulled out of fire() so a volley is just a loop of these.
+function PlanetXWeapon::launchBolt(%this, %muzzle, %angle)
+{
+ %bullet = %this.bullet[%this.nextBullet];
+ %this.nextBullet = (%this.nextBullet + 1) % %this.bulletPoolSize;
+
+ if (isEventPending(%bullet.recycleEvent))
+ cancel(%bullet.recycleEvent);
+
+ %bullet.damage = %this.bulletDamage;
+ %bullet.setPosition(%muzzle);
+ %bullet.setAngle(%angle);
+ %bullet.setActive(true);
+ %bullet.setVisible(true);
+ %bullet.setAwake(true);
+ %bullet.setLinearVelocityPolar(%angle, %this.bulletSpeed);
+ %bullet.recycleEvent = %bullet.schedule(%this.bulletLife, "recycle");
+}
+
+//-----------------------------------------------------------------------------
+// Gun heat: each shot adds heat, which bleeds off over time. Hitting full heat
+// locks the trigger until the gun cools below the resume threshold.
+//-----------------------------------------------------------------------------
+
+function PlanetXWeapon::resetHeat(%this)
+{
+ %this.gunHeat = 0;
+ %this.overheated = false;
+ %this.updateHeatBar();
+}
+
+function PlanetXWeapon::addHeat(%this, %amount)
+{
+ %this.gunHeat += %amount;
+
+ // maxHeat is the venting ceiling (1 by default; the heat-sink upgrade raises it).
+ if (%this.gunHeat >= %this.maxHeat)
+ {
+ %this.gunHeat = %this.maxHeat;
+ %this.overheated = true;
+
+ // Vent: a steam plume off the gun and a hiss.
+ if (isObject(%this.steam) && isObject(%this.owner))
+ {
+ %this.steam.setPosition(%this.owner.getMuzzlePosition());
+ %this.steam.play(true);
+ }
+ Audio.PlaySound("PlanetXGame:steamHiss");
+ }
+
+ %this.updateHeatBar();
+}
+
+/// Bleed heat off over time; an overheated gun unlocks once it has cooled below
+/// the resume threshold.
+function PlanetXWeapon::heatTick(%this)
+{
+ if ($PlanetX::state !$= "playing")
+ return;
+
+ %this.heatEvent = %this.schedule(%this.heatTickMs, "heatTick");
+
+ if (%this.gunHeat <= 0)
+ return;
+
+ %this.gunHeat -= %this.heatDecayPerSecond * %this.heatTickMs / 1000;
+ if (%this.gunHeat < 0)
+ %this.gunHeat = 0;
+
+ if (%this.overheated)
+ {
+ // The vent plume follows the gun while it cools.
+ if (isObject(%this.steam) && isObject(%this.owner))
+ %this.steam.setPosition(%this.owner.getMuzzlePosition());
+
+ // Resume is proportional to capacity, so a bigger heat sink still vents the
+ // same FRACTION of its heat before the trigger frees up.
+ if (%this.gunHeat <= %this.heatResumeThreshold * %this.maxHeat)
+ {
+ %this.overheated = false;
+
+ // Let the last puffs finish rather than vanishing.
+ if (isObject(%this.steam))
+ %this.steam.stop(true, false);
+ }
+ }
+
+ %this.updateHeatBar();
+}
+
+/// Push the current heat to the HUD, if there is one yet. (During construction
+/// the level's HUD does not exist; the HUD starts at zero on its own.) The bar is
+/// 0..1, so heat is normalized against maxHeat - a raised heat sink still reads as a
+/// full bar at the vent point.
+function PlanetXWeapon::updateHeatBar(%this)
+{
+ %level = PlanetXGame.level;
+ if (isObject(%level) && isObject(%level.hud) && isObject(%this.owner))
+ %level.hud.setHeat(%this.owner.playerIndex, %this.gunHeat / %this.maxHeat, %this.overheated, %this.heatTickMs);
+}
diff --git a/PlanetX/PlanetXGame/sound/crystalGet.audio.taml b/PlanetX/PlanetXGame/sound/crystalGet.audio.taml
new file mode 100644
index 000000000..444d138fb
--- /dev/null
+++ b/PlanetX/PlanetXGame/sound/crystalGet.audio.taml
@@ -0,0 +1,4 @@
+
diff --git a/PlanetX/PlanetXGame/sound/crystalGet.wav b/PlanetX/PlanetXGame/sound/crystalGet.wav
new file mode 100644
index 000000000..d55212bee
Binary files /dev/null and b/PlanetX/PlanetXGame/sound/crystalGet.wav differ
diff --git a/PlanetX/PlanetXGame/sound/enemyChase.audio.taml b/PlanetX/PlanetXGame/sound/enemyChase.audio.taml
new file mode 100644
index 000000000..7432d7180
--- /dev/null
+++ b/PlanetX/PlanetXGame/sound/enemyChase.audio.taml
@@ -0,0 +1,6 @@
+
diff --git a/PlanetX/PlanetXGame/sound/enemyChase.wav b/PlanetX/PlanetXGame/sound/enemyChase.wav
new file mode 100644
index 000000000..27ca5894a
Binary files /dev/null and b/PlanetX/PlanetXGame/sound/enemyChase.wav differ
diff --git a/PlanetX/PlanetXGame/sound/enemyDeath.audio.taml b/PlanetX/PlanetXGame/sound/enemyDeath.audio.taml
new file mode 100644
index 000000000..e1c5f9da8
--- /dev/null
+++ b/PlanetX/PlanetXGame/sound/enemyDeath.audio.taml
@@ -0,0 +1,6 @@
+
diff --git a/PlanetX/PlanetXGame/sound/enemyDeath.wav b/PlanetX/PlanetXGame/sound/enemyDeath.wav
new file mode 100644
index 000000000..da5422e44
Binary files /dev/null and b/PlanetX/PlanetXGame/sound/enemyDeath.wav differ
diff --git a/PlanetX/PlanetXGame/sound/enemyGiveUp.audio.taml b/PlanetX/PlanetXGame/sound/enemyGiveUp.audio.taml
new file mode 100644
index 000000000..89ac4e4fa
--- /dev/null
+++ b/PlanetX/PlanetXGame/sound/enemyGiveUp.audio.taml
@@ -0,0 +1,4 @@
+
diff --git a/PlanetX/PlanetXGame/sound/enemyGiveUp.wav b/PlanetX/PlanetXGame/sound/enemyGiveUp.wav
new file mode 100644
index 000000000..752ddf8a4
Binary files /dev/null and b/PlanetX/PlanetXGame/sound/enemyGiveUp.wav differ
diff --git a/PlanetX/PlanetXGame/sound/footstep.audio.taml b/PlanetX/PlanetXGame/sound/footstep.audio.taml
new file mode 100644
index 000000000..f2701dff5
--- /dev/null
+++ b/PlanetX/PlanetXGame/sound/footstep.audio.taml
@@ -0,0 +1,4 @@
+
diff --git a/PlanetX/PlanetXGame/sound/footstep.wav b/PlanetX/PlanetXGame/sound/footstep.wav
new file mode 100644
index 000000000..659386955
Binary files /dev/null and b/PlanetX/PlanetXGame/sound/footstep.wav differ
diff --git a/PlanetX/PlanetXGame/sound/laser.audio.taml b/PlanetX/PlanetXGame/sound/laser.audio.taml
new file mode 100644
index 000000000..178bdc381
--- /dev/null
+++ b/PlanetX/PlanetXGame/sound/laser.audio.taml
@@ -0,0 +1,4 @@
+
diff --git a/PlanetX/PlanetXGame/sound/laser.wav b/PlanetX/PlanetXGame/sound/laser.wav
new file mode 100644
index 000000000..faa999ba5
Binary files /dev/null and b/PlanetX/PlanetXGame/sound/laser.wav differ
diff --git a/PlanetX/PlanetXGame/sound/levelStart.audio.taml b/PlanetX/PlanetXGame/sound/levelStart.audio.taml
new file mode 100644
index 000000000..56b323fbd
--- /dev/null
+++ b/PlanetX/PlanetXGame/sound/levelStart.audio.taml
@@ -0,0 +1,4 @@
+
diff --git a/PlanetX/PlanetXGame/sound/levelStart.wav b/PlanetX/PlanetXGame/sound/levelStart.wav
new file mode 100644
index 000000000..dbe66d174
Binary files /dev/null and b/PlanetX/PlanetXGame/sound/levelStart.wav differ
diff --git a/PlanetX/PlanetXGame/sound/playerDeath.audio.taml b/PlanetX/PlanetXGame/sound/playerDeath.audio.taml
new file mode 100644
index 000000000..e7238a62b
--- /dev/null
+++ b/PlanetX/PlanetXGame/sound/playerDeath.audio.taml
@@ -0,0 +1,4 @@
+
diff --git a/PlanetX/PlanetXGame/sound/playerDeath.wav b/PlanetX/PlanetXGame/sound/playerDeath.wav
new file mode 100644
index 000000000..ec186192b
Binary files /dev/null and b/PlanetX/PlanetXGame/sound/playerDeath.wav differ
diff --git a/PlanetX/PlanetXGame/sound/playerHurt.audio.taml b/PlanetX/PlanetXGame/sound/playerHurt.audio.taml
new file mode 100644
index 000000000..030f96b78
--- /dev/null
+++ b/PlanetX/PlanetXGame/sound/playerHurt.audio.taml
@@ -0,0 +1,4 @@
+
diff --git a/PlanetX/PlanetXGame/sound/playerHurt.wav b/PlanetX/PlanetXGame/sound/playerHurt.wav
new file mode 100644
index 000000000..3a666393b
Binary files /dev/null and b/PlanetX/PlanetXGame/sound/playerHurt.wav differ
diff --git a/PlanetX/PlanetXGame/sound/steam.audio.taml b/PlanetX/PlanetXGame/sound/steam.audio.taml
new file mode 100644
index 000000000..87c753a0c
--- /dev/null
+++ b/PlanetX/PlanetXGame/sound/steam.audio.taml
@@ -0,0 +1,4 @@
+
diff --git a/PlanetX/PlanetXGame/sound/steam.wav b/PlanetX/PlanetXGame/sound/steam.wav
new file mode 100644
index 000000000..b9d3626b9
Binary files /dev/null and b/PlanetX/PlanetXGame/sound/steam.wav differ
diff --git a/PlanetX/PlanetXGame/sound/uiClick.audio.taml b/PlanetX/PlanetXGame/sound/uiClick.audio.taml
new file mode 100644
index 000000000..ed5bbf6e2
--- /dev/null
+++ b/PlanetX/PlanetXGame/sound/uiClick.audio.taml
@@ -0,0 +1,4 @@
+
diff --git a/PlanetX/PlanetXGame/sound/uiClick.wav b/PlanetX/PlanetXGame/sound/uiClick.wav
new file mode 100644
index 000000000..ba9cfdd07
Binary files /dev/null and b/PlanetX/PlanetXGame/sound/uiClick.wav differ
diff --git a/PlanetX/PlanetXGame/sprites/alien_brute.animation.taml b/PlanetX/PlanetXGame/sprites/alien_brute.animation.taml
new file mode 100644
index 000000000..976e4fa15
--- /dev/null
+++ b/PlanetX/PlanetXGame/sprites/alien_brute.animation.taml
@@ -0,0 +1,6 @@
+
diff --git a/PlanetX/PlanetXGame/sprites/alien_brute.image.taml b/PlanetX/PlanetXGame/sprites/alien_brute.image.taml
new file mode 100644
index 000000000..9d44b5961
--- /dev/null
+++ b/PlanetX/PlanetXGame/sprites/alien_brute.image.taml
@@ -0,0 +1,9 @@
+
diff --git a/PlanetX/PlanetXGame/sprites/alien_brute.png b/PlanetX/PlanetXGame/sprites/alien_brute.png
new file mode 100644
index 000000000..a3669fce7
Binary files /dev/null and b/PlanetX/PlanetXGame/sprites/alien_brute.png differ
diff --git a/PlanetX/PlanetXGame/sprites/alien_walk.animation.taml b/PlanetX/PlanetXGame/sprites/alien_walk.animation.taml
new file mode 100644
index 000000000..eb11dbb4e
--- /dev/null
+++ b/PlanetX/PlanetXGame/sprites/alien_walk.animation.taml
@@ -0,0 +1,6 @@
+
diff --git a/PlanetX/PlanetXGame/sprites/alien_walk.image.taml b/PlanetX/PlanetXGame/sprites/alien_walk.image.taml
new file mode 100644
index 000000000..ec911ac4b
--- /dev/null
+++ b/PlanetX/PlanetXGame/sprites/alien_walk.image.taml
@@ -0,0 +1,9 @@
+
diff --git a/PlanetX/PlanetXGame/sprites/alien_walk.png b/PlanetX/PlanetXGame/sprites/alien_walk.png
new file mode 100644
index 000000000..e6d7803fa
Binary files /dev/null and b/PlanetX/PlanetXGame/sprites/alien_walk.png differ
diff --git a/PlanetX/PlanetXGame/sprites/bolt.image.taml b/PlanetX/PlanetXGame/sprites/bolt.image.taml
new file mode 100644
index 000000000..f1d5c66ed
--- /dev/null
+++ b/PlanetX/PlanetXGame/sprites/bolt.image.taml
@@ -0,0 +1,4 @@
+
diff --git a/PlanetX/PlanetXGame/sprites/bolt.png b/PlanetX/PlanetXGame/sprites/bolt.png
new file mode 100644
index 000000000..64a2800b6
Binary files /dev/null and b/PlanetX/PlanetXGame/sprites/bolt.png differ
diff --git a/PlanetX/PlanetXGame/sprites/burst.animation.taml b/PlanetX/PlanetXGame/sprites/burst.animation.taml
new file mode 100644
index 000000000..daa6623e9
--- /dev/null
+++ b/PlanetX/PlanetXGame/sprites/burst.animation.taml
@@ -0,0 +1,6 @@
+
diff --git a/PlanetX/PlanetXGame/sprites/burst.image.taml b/PlanetX/PlanetXGame/sprites/burst.image.taml
new file mode 100644
index 000000000..0ced1fe99
--- /dev/null
+++ b/PlanetX/PlanetXGame/sprites/burst.image.taml
@@ -0,0 +1,8 @@
+
diff --git a/PlanetX/PlanetXGame/sprites/burst.png b/PlanetX/PlanetXGame/sprites/burst.png
new file mode 100644
index 000000000..d8be3910d
Binary files /dev/null and b/PlanetX/PlanetXGame/sprites/burst.png differ
diff --git a/PlanetX/PlanetXGame/sprites/crosshair.image.taml b/PlanetX/PlanetXGame/sprites/crosshair.image.taml
new file mode 100644
index 000000000..42954065d
--- /dev/null
+++ b/PlanetX/PlanetXGame/sprites/crosshair.image.taml
@@ -0,0 +1,4 @@
+
diff --git a/PlanetX/PlanetXGame/sprites/crosshair.png b/PlanetX/PlanetXGame/sprites/crosshair.png
new file mode 100644
index 000000000..e66a7297e
Binary files /dev/null and b/PlanetX/PlanetXGame/sprites/crosshair.png differ
diff --git a/PlanetX/PlanetXGame/sprites/crystal.image.taml b/PlanetX/PlanetXGame/sprites/crystal.image.taml
new file mode 100644
index 000000000..3a66db7ab
--- /dev/null
+++ b/PlanetX/PlanetXGame/sprites/crystal.image.taml
@@ -0,0 +1,4 @@
+
diff --git a/PlanetX/PlanetXGame/sprites/crystal.png b/PlanetX/PlanetXGame/sprites/crystal.png
new file mode 100644
index 000000000..1d159450a
Binary files /dev/null and b/PlanetX/PlanetXGame/sprites/crystal.png differ
diff --git a/PlanetX/PlanetXGame/sprites/gun.image.taml b/PlanetX/PlanetXGame/sprites/gun.image.taml
new file mode 100644
index 000000000..085411056
--- /dev/null
+++ b/PlanetX/PlanetXGame/sprites/gun.image.taml
@@ -0,0 +1,4 @@
+
diff --git a/PlanetX/PlanetXGame/sprites/gun.png b/PlanetX/PlanetXGame/sprites/gun.png
new file mode 100644
index 000000000..c59072575
Binary files /dev/null and b/PlanetX/PlanetXGame/sprites/gun.png differ
diff --git a/PlanetX/PlanetXGame/sprites/puff.image.taml b/PlanetX/PlanetXGame/sprites/puff.image.taml
new file mode 100644
index 000000000..108c7590d
--- /dev/null
+++ b/PlanetX/PlanetXGame/sprites/puff.image.taml
@@ -0,0 +1,3 @@
+
diff --git a/PlanetX/PlanetXGame/sprites/puff.png b/PlanetX/PlanetXGame/sprites/puff.png
new file mode 100644
index 000000000..4c50dfab5
Binary files /dev/null and b/PlanetX/PlanetXGame/sprites/puff.png differ
diff --git a/PlanetX/PlanetXGame/sprites/rock_1.image.taml b/PlanetX/PlanetXGame/sprites/rock_1.image.taml
new file mode 100644
index 000000000..6a4eec987
--- /dev/null
+++ b/PlanetX/PlanetXGame/sprites/rock_1.image.taml
@@ -0,0 +1,4 @@
+
diff --git a/PlanetX/PlanetXGame/sprites/rock_1.png b/PlanetX/PlanetXGame/sprites/rock_1.png
new file mode 100644
index 000000000..d212b4559
Binary files /dev/null and b/PlanetX/PlanetXGame/sprites/rock_1.png differ
diff --git a/PlanetX/PlanetXGame/sprites/rock_2.image.taml b/PlanetX/PlanetXGame/sprites/rock_2.image.taml
new file mode 100644
index 000000000..77754a37b
--- /dev/null
+++ b/PlanetX/PlanetXGame/sprites/rock_2.image.taml
@@ -0,0 +1,4 @@
+
diff --git a/PlanetX/PlanetXGame/sprites/rock_2.png b/PlanetX/PlanetXGame/sprites/rock_2.png
new file mode 100644
index 000000000..6d460eb40
Binary files /dev/null and b/PlanetX/PlanetXGame/sprites/rock_2.png differ
diff --git a/PlanetX/PlanetXGame/sprites/rocket.image.taml b/PlanetX/PlanetXGame/sprites/rocket.image.taml
new file mode 100644
index 000000000..a44ea03d3
--- /dev/null
+++ b/PlanetX/PlanetXGame/sprites/rocket.image.taml
@@ -0,0 +1,4 @@
+
diff --git a/PlanetX/PlanetXGame/sprites/rocket.png b/PlanetX/PlanetXGame/sprites/rocket.png
new file mode 100644
index 000000000..bfbfcb0e2
Binary files /dev/null and b/PlanetX/PlanetXGame/sprites/rocket.png differ
diff --git a/PlanetX/PlanetXGame/sprites/spaceman_idle.image.taml b/PlanetX/PlanetXGame/sprites/spaceman_idle.image.taml
new file mode 100644
index 000000000..319836e17
--- /dev/null
+++ b/PlanetX/PlanetXGame/sprites/spaceman_idle.image.taml
@@ -0,0 +1,5 @@
+
diff --git a/PlanetX/PlanetXGame/sprites/spaceman_idle.png b/PlanetX/PlanetXGame/sprites/spaceman_idle.png
new file mode 100644
index 000000000..372613902
Binary files /dev/null and b/PlanetX/PlanetXGame/sprites/spaceman_idle.png differ
diff --git a/PlanetX/PlanetXGame/sprites/spaceman_idle2.image.taml b/PlanetX/PlanetXGame/sprites/spaceman_idle2.image.taml
new file mode 100644
index 000000000..172e636e1
--- /dev/null
+++ b/PlanetX/PlanetXGame/sprites/spaceman_idle2.image.taml
@@ -0,0 +1,5 @@
+
diff --git a/PlanetX/PlanetXGame/sprites/spaceman_idle2.png b/PlanetX/PlanetXGame/sprites/spaceman_idle2.png
new file mode 100644
index 000000000..c4de43ee1
Binary files /dev/null and b/PlanetX/PlanetXGame/sprites/spaceman_idle2.png differ
diff --git a/PlanetX/PlanetXGame/sprites/spaceman_walk.animation.taml b/PlanetX/PlanetXGame/sprites/spaceman_walk.animation.taml
new file mode 100644
index 000000000..1679334dd
--- /dev/null
+++ b/PlanetX/PlanetXGame/sprites/spaceman_walk.animation.taml
@@ -0,0 +1,6 @@
+
diff --git a/PlanetX/PlanetXGame/sprites/spaceman_walk.image.taml b/PlanetX/PlanetXGame/sprites/spaceman_walk.image.taml
new file mode 100644
index 000000000..8c7050866
--- /dev/null
+++ b/PlanetX/PlanetXGame/sprites/spaceman_walk.image.taml
@@ -0,0 +1,9 @@
+
diff --git a/PlanetX/PlanetXGame/sprites/spaceman_walk.png b/PlanetX/PlanetXGame/sprites/spaceman_walk.png
new file mode 100644
index 000000000..1dd331410
Binary files /dev/null and b/PlanetX/PlanetXGame/sprites/spaceman_walk.png differ
diff --git a/PlanetX/PlanetXGame/sprites/spaceman_walk2.animation.taml b/PlanetX/PlanetXGame/sprites/spaceman_walk2.animation.taml
new file mode 100644
index 000000000..695fef789
--- /dev/null
+++ b/PlanetX/PlanetXGame/sprites/spaceman_walk2.animation.taml
@@ -0,0 +1,6 @@
+
diff --git a/PlanetX/PlanetXGame/sprites/spaceman_walk2.image.taml b/PlanetX/PlanetXGame/sprites/spaceman_walk2.image.taml
new file mode 100644
index 000000000..896a34660
--- /dev/null
+++ b/PlanetX/PlanetXGame/sprites/spaceman_walk2.image.taml
@@ -0,0 +1,9 @@
+
diff --git a/PlanetX/PlanetXGame/sprites/spaceman_walk2.png b/PlanetX/PlanetXGame/sprites/spaceman_walk2.png
new file mode 100644
index 000000000..122f6c42e
Binary files /dev/null and b/PlanetX/PlanetXGame/sprites/spaceman_walk2.png differ
diff --git a/PlanetX/PlanetXGame/sprites/tiles.image.taml b/PlanetX/PlanetXGame/sprites/tiles.image.taml
new file mode 100644
index 000000000..a65c846c9
--- /dev/null
+++ b/PlanetX/PlanetXGame/sprites/tiles.image.taml
@@ -0,0 +1,8 @@
+
diff --git a/PlanetX/PlanetXGame/sprites/tiles.png b/PlanetX/PlanetXGame/sprites/tiles.png
new file mode 100644
index 000000000..53de1bbf8
Binary files /dev/null and b/PlanetX/PlanetXGame/sprites/tiles.png differ
diff --git a/PlanetX/README.md b/PlanetX/README.md
new file mode 100644
index 000000000..f2b37db42
--- /dev/null
+++ b/PlanetX/README.md
@@ -0,0 +1,100 @@
+# PlanetX
+
+A small but complete demo game for Torque2D 4.0, built the same way the
+Project Manager scaffolds a real game project. It exists so you can read one
+codebase that goes all the way: title screen, gameplay, win/lose dialogs,
+level-to-level progression, and back to the title.
+
+
+
+## Playing
+
+Launch Torque2D and pick the **PlanetX** card in the Project Selector. Or boot
+straight into it from `main.cs` (see the commented PlanetX block there).
+
+Your rocket put you down on the wrong side of the planet. Somewhere out there
+is the crystal you came for.
+
+- **WASD / arrow keys** — move
+- **Mouse** — aim; **hold left button** — fire
+- **Escape** — abandon the mission and return to the title screen
+
+Aliens nest in clusters across the surface and swarm you on sight; the big
+dark-shelled brutes take four times the punishment. Contact hurts — your hull
+bar is top-left. Your laser builds heat as you fire (the bar under the hull
+bar); redline it and the gun vents steam and locks until it cools, so shoot
+in bursts.
+
+Touch the crystal to clear the level. Every level is generated fresh from a
+new seed and each one is harder than the last: more aliens, more brutes, and
+tougher, faster, harder-hitting versions of both. Dying restarts the current
+level with a fresh layout.
+
+## How it's put together
+
+```
+PlanetX/
+├── AppCore/1/ project bootstrap (copied from library/AppCore, retinted
+│ to the Rocket Edition palette in gui/guiProfiles.cs)
+├── Audio/1/ the standard Audio module (verbatim library copy)
+├── ScreenFade/1/ canvas fade transitions (verbatim library copy)
+└── PlanetXGame/ the game itself
+ ├── game.cs module lifecycle + the title/playing/won/lost state
+ │ machine, including level advancement
+ ├── scripts/level.cs scene, Perlin-tinted CompositeSprite terrain, seeded
+ │ random placement of the rocket and crystal
+ ├── scripts/player.cs the spaceman: a two-sprite composite (flipping body,
+ │ 360-degree rotating gun), movement, health
+ ├── scripts/input.cs WASD ActionMap + mouse aim (window-point re-projection)
+ ├── scripts/bullet.cs pooled laser bolts, impact bursts, and gun heat
+ ├── scripts/alien.cs aliens + brutes, noise-driven nest spawning, and the
+ │ per-level difficulty curve (applyDifficulty)
+ ├── scripts/crystal.cs the objective (a static sensor)
+ ├── scripts/hud.cs hull bar, heat bar, level label, objective hint
+ ├── scripts/behaviors/ ChaseBehavior + TakesDamageBehavior (adapted from DeathBallToy)
+ ├── gui/ title screen, victory + game-over dialogs (TAML)
+ ├── particles/ the overheat steam vent (ParticleAsset)
+ ├── sprites/ all game art, generated in the Rocket Edition palette
+ └── music/, sound/ the planetfall track, laser and steam effects
+```
+
+Things worth stealing:
+
+- **Project scaffold** — a top-level folder with its own AppCore is all it
+ takes to appear in the Project Selector. Only the project folder is scanned
+ at boot, so the project carries copies of every module and asset it uses.
+- **Palette retint** — the six colors in `AppCore::SetProfileColors`
+ (`AppCore/1/gui/guiProfiles.cs`) restyle every stock GUI profile at once.
+- **Perlin terrain** — the ground is one rect-layout `CompositeSprite` of
+ near-white tiles tinted per-corner (`setSpriteComplexColor`). Noise is
+ sampled once per grid *vertex* and each tile reuses the vertices it shares
+ with its neighbors, so the Gouraud interpolation is seamless across seams
+ (`level.cs::buildTileMap`).
+- **Noise is not an RNG** — one seed drives the whole level, but in two ways:
+ the `NoiseGenerator` shapes *fields* (terrain colors, alien nests) while a
+ reseeded `getRandom` picks *points* (rocket, crystal, brutes). Sampling
+ Perlin noise at a fixed coordinate clusters around 0.5 across seeds, so it
+ cannot substitute for a uniform random number (`level.cs::buildLevel`).
+- **Composite characters** — the spaceman is two batch sprites on one body:
+ the side-view body flips left/right while the gun sprite rotates to the
+ true aim angle, so aim never lags movement (`player.cs`).
+- **Angle convention** — `mAtan(Vector2Sub(target, origin))` and
+ `setLinearVelocityPolar` both use 0° = +X, counter-clockwise. All PlanetX
+ art is drawn facing +X, so no fudge offsets appear anywhere.
+- **Pooled projectiles** — `bullet.cs` pre-builds its bolts and bursts
+ (TruckToy's pattern) so firing never allocates mid-play. Note the
+ `setFixedAngle(true)` on the bolts: collisions impart angular velocity that
+ survives pooling, and without it recycled bullets come back spinning.
+- **Behaviors** — the alien AI is two small `BehaviorTemplate`s composed onto
+ a plain Sprite; `chaseBehavior.cs` shows the self-scheduled tick pattern
+ with a game-state guard.
+- **Difficulty in one place** — `alien.cs::applyDifficulty` maps the level
+ number onto a handful of `$PlanetX::Cur*` globals that every spawn reads,
+ so the whole curve is tunable from a single function.
+- **Teardown** — `PlanetXGame::teardownLevel` deletes the scene + root GUI
+ and rebuilds from scratch for every retry and level change; three loops in
+ a row leak nothing (check `PlanetXScene.getCount()` stays constant).
+
+All sprite art was generated for this demo in the Torque2D Rocket Edition
+palette (#EA4848 / #A62646 / #801946 / #300022 / #21BF84) and is MIT-licensed
+with the engine, like the rest of the project.
diff --git a/PlanetX/ScreenFade/1/gui/background.image.taml b/PlanetX/ScreenFade/1/gui/background.image.taml
new file mode 100644
index 000000000..a48a5858f
--- /dev/null
+++ b/PlanetX/ScreenFade/1/gui/background.image.taml
@@ -0,0 +1,3 @@
+
diff --git a/PlanetX/ScreenFade/1/gui/background.png b/PlanetX/ScreenFade/1/gui/background.png
new file mode 100644
index 000000000..169e6f7c5
Binary files /dev/null and b/PlanetX/ScreenFade/1/gui/background.png differ
diff --git a/PlanetX/ScreenFade/1/module.taml b/PlanetX/ScreenFade/1/module.taml
new file mode 100644
index 000000000..8c7f3c0d4
--- /dev/null
+++ b/PlanetX/ScreenFade/1/module.taml
@@ -0,0 +1,15 @@
+
+
+
diff --git a/PlanetX/ScreenFade/1/screenFade.cs b/PlanetX/ScreenFade/1/screenFade.cs
new file mode 100644
index 000000000..04995e0c8
--- /dev/null
+++ b/PlanetX/ScreenFade/1/screenFade.cs
@@ -0,0 +1,72 @@
+function ScreenFade::create(%this)
+{
+ exec("./scripts/ScreenFadeBackground.cs");
+
+ %this.background = new GuiSpriteCtrl() {
+ class = "ScreenFadeBackground";
+ profile = "GuiDefaultProfile";
+ HorizSizing = "relative";
+ VertSizing = "relative";
+ Position = "0 0";
+ Image = "ScreenFade:background";
+ FullSize = 1;
+ ConstrainProportions = 0;
+ ImageColor = "255 255 255 0";
+ Owner = %this;
+ };
+}
+
+function ScreenFade::destroy(%this)
+{
+ if(isObject(%this.background))
+ {
+ %this.background.delete();
+ }
+}
+
+//Switches the canvas by fading in to color and back out to the new content. The process takes the given time in milliseconds.
+//Color and time are optional.
+//ScreenFade will post event: onSwapComplete().
+function ScreenFade::swapCanvas(%this, %content, %color, %time)
+{
+ if(%color $= "")
+ {
+ %color = "0 0 0 0";
+ }
+
+ if(%time $= "")
+ {
+ %time = 1400;
+ }
+
+ %base = getWord(%color, 0) SPC getWord(%color, 1) SPC getWord(%color, 2);
+ %this.background.solidColor = %base SPC "255";
+ %this.background.transparentColor = %base SPC "0";
+ %this.background.swapContent = %content;
+ %this.background.swapTime = mRound((%time / 5) * 2);
+ %this.background.startSwap();
+}
+
+//Fades the screen to color and then puts the dialog on top of it.
+//Color and time are optional.
+//When your dialog closes, call %this.postEvent("dialogClose"); to inform ScreenFade where %this is the same object passed to openDialog().
+//Or call %this.postEvent("dialogSwap", %dialog); to swap the dialog for another. Then the new dialog will have to post "dialogClose" when it closes.
+//ScreenFade will post events: onOpenComplete() and onCloseComplete().
+function ScreenFade::openDialog(%this, %dialog, %color, %time)
+{
+ if(%color $= "")
+ {
+ %color = "0 0 0 230";
+ }
+
+ if(%time $= "")
+ {
+ %time = 300;
+ }
+
+ %this.background.solidColor = %color;
+ %this.background.transparentColor = getWord(%color, 0) SPC getWord(%color, 1) SPC getWord(%color, 2) SPC "0";
+ %this.background.dialog = %dialog;
+ %this.background.dialogTime = %time;
+ %this.background.openDialog();
+}
diff --git a/PlanetX/ScreenFade/1/scripts/ScreenFadeBackground.cs b/PlanetX/ScreenFade/1/scripts/ScreenFadeBackground.cs
new file mode 100644
index 000000000..c134daa0c
--- /dev/null
+++ b/PlanetX/ScreenFade/1/scripts/ScreenFadeBackground.cs
@@ -0,0 +1,94 @@
+function ScreenFadeBackground::resetColor(%this)
+{
+ if(getWord(%this.getImageColor(), 3) == 0)
+ {
+ %this.setImageColor(%this.transparentColor);
+ %extent = Canvas.getExtent();
+ %this.setExtent(getWord(%extent, 0), getWord(%extent, 1));
+ }
+}
+
+function ScreenFadeBackground::startSwap(%this)
+{
+ %this.resetColor();
+ %this.clear();
+ Canvas.pushDialog(%this);
+ %this.fadeTo(%this.solidColor, %this.swapTime, "EaseInOut");
+ %this.schedule(%this.swapTime, "doSwap");
+}
+
+function ScreenFadeBackground::doSwap(%this)
+{
+ Canvas.setContent(%this.swapContent);
+ Canvas.pushDialog(%this);
+ %this.schedule(%this.swapTime/2, "fadeSwap");
+}
+
+function ScreenFadeBackground::fadeSwap(%this)
+{
+ %this.fadeTo(%this.transparentColor, %this.swapTime, "EaseInOut");
+ %this.schedule(%this.swapTime, "finishSwap");
+}
+
+function ScreenFadeBackground::finishSwap(%this)
+{
+ Canvas.popDialog(%this);
+ %this.Owner.postEvent("SwapComplete");
+}
+
+function ScreenFadeBackground::openDialog(%this)
+{
+ %this.resetColor();
+ %this.add(%this.dialog);
+ Canvas.pushDialog(%this);
+ %this.fadeTo(%this.solidColor, %this.dialogTime, "EaseInOut");
+ %this.schedule(%this.dialogTime, "openDialogComplete");
+}
+
+function ScreenFadeBackground::openDialogComplete(%this)
+{
+ %this.Owner.postEvent("OpenComplete");
+ %this.startListening(%this.dialog);
+}
+
+function ScreenFadeBackground::onDialogClose(%this)
+{
+ if(%this.isAwake())
+ {
+ if(isEventPending(%this.hideSchedule))
+ {
+ cancel(%this.hideSchedule);
+ }
+ %this.clear();
+ %this.fadeTo(%this.transparentColor, %this.dialogTime, "EaseIn");
+ %this.hideSchedule = %this.schedule(%this.dialogTime, "onCloseComplete");
+ }
+}
+
+function ScreenFadeBackground::onDialogSwap(%this, %dialog)
+{
+ if(%this.isAwake() && isObject(%this.dialog))
+ {
+ if(isEventPending(%this.hideSchedule))
+ {
+ cancel(%this.hideSchedule);
+ }
+ %this.setImageColor(%this.solidColor);
+ %this.stopListening(%this.dialog);
+ %this.removeIfMember(%this.dialog);
+ %this.dialog = %dialog;
+ %this.add(%this.dialog);
+ %this.startListening(%this.dialog);
+ }
+}
+
+function ScreenFadeBackground::onCloseComplete(%this)
+{
+ if(isObject(%this.dialog))
+ {
+ %this.stopListening(%this.dialog);
+ %this.removeIfMember(%this.dialog);
+ }
+ Canvas.popDialog(%this);
+ %this.Owner.postEvent("CloseComplete");
+}
diff --git a/PlanetX/themes/PlanetX.taml b/PlanetX/themes/PlanetX.taml
new file mode 100644
index 000000000..6d8a06da3
--- /dev/null
+++ b/PlanetX/themes/PlanetX.taml
@@ -0,0 +1,245 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PlanetX/themes/cursors/PlanetX/NESW.png b/PlanetX/themes/cursors/PlanetX/NESW.png
new file mode 100644
index 000000000..2f73696c8
Binary files /dev/null and b/PlanetX/themes/cursors/PlanetX/NESW.png differ
diff --git a/PlanetX/themes/cursors/PlanetX/NWSE.png b/PlanetX/themes/cursors/PlanetX/NWSE.png
new file mode 100644
index 000000000..c952a3e45
Binary files /dev/null and b/PlanetX/themes/cursors/PlanetX/NWSE.png differ
diff --git a/PlanetX/themes/cursors/PlanetX/defaultCursor.png b/PlanetX/themes/cursors/PlanetX/defaultCursor.png
new file mode 100644
index 000000000..a0d1ab9de
Binary files /dev/null and b/PlanetX/themes/cursors/PlanetX/defaultCursor.png differ
diff --git a/PlanetX/themes/cursors/PlanetX/ibeam.png b/PlanetX/themes/cursors/PlanetX/ibeam.png
new file mode 100644
index 000000000..56079504c
Binary files /dev/null and b/PlanetX/themes/cursors/PlanetX/ibeam.png differ
diff --git a/PlanetX/themes/cursors/PlanetX/leftRight.png b/PlanetX/themes/cursors/PlanetX/leftRight.png
new file mode 100644
index 000000000..c29b7a9f8
Binary files /dev/null and b/PlanetX/themes/cursors/PlanetX/leftRight.png differ
diff --git a/PlanetX/themes/cursors/PlanetX/move.png b/PlanetX/themes/cursors/PlanetX/move.png
new file mode 100644
index 000000000..70f9cd540
Binary files /dev/null and b/PlanetX/themes/cursors/PlanetX/move.png differ
diff --git a/PlanetX/themes/cursors/PlanetX/upDown.png b/PlanetX/themes/cursors/PlanetX/upDown.png
new file mode 100644
index 000000000..377217897
Binary files /dev/null and b/PlanetX/themes/cursors/PlanetX/upDown.png differ
diff --git a/PlanetX/themes/fonts/share tech mono 10 (ansi).uft b/PlanetX/themes/fonts/share tech mono 10 (ansi).uft
new file mode 100644
index 000000000..cadddb953
Binary files /dev/null and b/PlanetX/themes/fonts/share tech mono 10 (ansi).uft differ
diff --git a/PlanetX/themes/fonts/share tech mono 12 (ansi).uft b/PlanetX/themes/fonts/share tech mono 12 (ansi).uft
new file mode 100644
index 000000000..c4b04d8ad
Binary files /dev/null and b/PlanetX/themes/fonts/share tech mono 12 (ansi).uft differ
diff --git a/PlanetX/themes/fonts/share tech mono 14 (ansi).uft b/PlanetX/themes/fonts/share tech mono 14 (ansi).uft
new file mode 100644
index 000000000..d06745b4c
Binary files /dev/null and b/PlanetX/themes/fonts/share tech mono 14 (ansi).uft differ
diff --git a/PlanetX/themes/fonts/share tech mono 16 (ansi).uft b/PlanetX/themes/fonts/share tech mono 16 (ansi).uft
new file mode 100644
index 000000000..57a055e5e
Binary files /dev/null and b/PlanetX/themes/fonts/share tech mono 16 (ansi).uft differ
diff --git a/PlanetX/themes/fonts/share tech mono 18 (ansi).uft b/PlanetX/themes/fonts/share tech mono 18 (ansi).uft
new file mode 100644
index 000000000..bb647a9e4
Binary files /dev/null and b/PlanetX/themes/fonts/share tech mono 18 (ansi).uft differ
diff --git a/PlanetX/themes/fonts/share tech mono 20 (ansi).uft b/PlanetX/themes/fonts/share tech mono 20 (ansi).uft
new file mode 100644
index 000000000..df286ec5c
Binary files /dev/null and b/PlanetX/themes/fonts/share tech mono 20 (ansi).uft differ
diff --git a/PlanetX/themes/fonts/share tech mono 22 (ansi).uft b/PlanetX/themes/fonts/share tech mono 22 (ansi).uft
new file mode 100644
index 000000000..ac6fd7a95
Binary files /dev/null and b/PlanetX/themes/fonts/share tech mono 22 (ansi).uft differ
diff --git a/PlanetX/themes/fonts/share tech mono 24 (ansi).uft b/PlanetX/themes/fonts/share tech mono 24 (ansi).uft
new file mode 100644
index 000000000..b94cdc102
Binary files /dev/null and b/PlanetX/themes/fonts/share tech mono 24 (ansi).uft differ
diff --git a/PlanetX/themes/fonts/share tech mono 26 (ansi).uft b/PlanetX/themes/fonts/share tech mono 26 (ansi).uft
new file mode 100644
index 000000000..3d08fcff6
Binary files /dev/null and b/PlanetX/themes/fonts/share tech mono 26 (ansi).uft differ
diff --git a/PlanetX/themes/fonts/share tech mono 27 (ansi).uft b/PlanetX/themes/fonts/share tech mono 27 (ansi).uft
new file mode 100644
index 000000000..57f197901
Binary files /dev/null and b/PlanetX/themes/fonts/share tech mono 27 (ansi).uft differ
diff --git a/PlanetX/themes/fonts/share tech mono 44 (ansi).uft b/PlanetX/themes/fonts/share tech mono 44 (ansi).uft
new file mode 100644
index 000000000..125f83046
Binary files /dev/null and b/PlanetX/themes/fonts/share tech mono 44 (ansi).uft differ
diff --git a/README.md b/README.md
index f072529c2..ce992bcc2 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@

-## Torque2D 4.0 Early Access 3
+## Torque2D 4.0 Early Access 4
MIT Licensed Open Source version of Torque2D from GarageGames. Maintained by the Torque Game Engines team and contributions from the community.
@@ -11,7 +11,13 @@ Torque2D 4.0: Rocket Edition is currently in progress. The major change with 4.0
The managers can be reached by opening the console using the console button in the Toybox or by pressing Tilde(~) + Ctrl. You will then notice tabs along that top for the various tools currently available.
-Early Access 3 introduces a **full GUI Editor** built from the ground up, replacing the previous Gui Editor Toy. The new editor includes an inspector, tree view, menus, save/load dialogs, frame set layouts, color picker, and control reordering. It provides a complete visual workflow for creating and editing GUI layouts within the engine.
+Early Access 4 builds out the **GUI Editor** that Early Access 3 introduced. You build a screen by dragging controls from an illustrated palette onto the canvas, or by clicking one to have it placed for you, and arrange them by dragging, by the arrow keys, or through the Layout menu's align and spacing commands. There is undo and redo, and cut, copy and paste work within a Gui and between them.
+
+Around the canvas sit two more panels. The **Explorer** shows the whole control tree, with a picture of each control's class, columns for hiding and locking, and drag-to-reparent. The **properties pane** shows only the fields the selected control's class actually reads — a chain never draws its own text, so it is not offered nine text fields it will ignore — and gives the common ones purpose-built editors rather than text boxes: an anchor picker for sizing, color swatches, an image picker you choose by looking at it, and editors for the things that used to be unreachable from an editor at all, such as a list box's rows and a menu bar's items.
+
+Guis are saved as either the classic `.gui` script or TAML, and the editor says which one loses what before you pick. A Gui you have changed is not thrown away without being asked about.
+
+Controls take their appearance from a **theme** rather than from profiles you wire up by hand. The **Gui Profile Editor** is where a theme is authored — profiles, borders, fonts and colors, against a live preview — and a control dropped onto the canvas arrives already wearing the right one. Set Theme re-skins an entire Gui.
The Rocket Edition also features a revamped Gui System! Until now it has been a common practice among those seriously using T2D to avoid the Gui System as much as possible. We aim to fix that with the Rocket Edition. Explanation of how to use the updated Gui System can be found in the wiki in the [Gui Guide](https://github.com/TorqueGameEngines/Torque2D/wiki/GUI-Guide).
@@ -31,16 +37,28 @@ If you do not wish to compile the source code yourself, precompiled binary files
### Building the Source
-After downloading a copy of the source code, the following project files for each platform are provided for you and can be found in the `engine/compilers` folder.
+**CMake is the single source of truth for the build.** You generate a project for your platform/toolchain from the root `CMakeLists.txt` and build it; the compiled executable is written to the repository root. Convenience generator scripts live at the repo root:
+
+* **Windows:** `generate-vs2022.bat` (or `generate-vs2026.bat`) → a Visual Studio solution
+* **macOS:** `generate-xcode.command` → an Xcode project
+* **Linux:** `build-linux.sh` (configures and builds; 32- and 64-bit)
+* **iOS:** `generate-xcode-ios.command` (simulator) or `generate-xcode-ios-device.command` (device)
+* **Android:** open `engine/compilers/android-studio` in Android Studio — its Gradle build drives CMake via the NDK
+* **Web:** `generate-emscripten.sh` → a WebAssembly build via `emcmake` (requires the Emscripten SDK)
+
+The hand-maintained per-platform project files that used to live in `engine/compilers/` have been removed — CMake replaces them. For full step-by-step build instructions on every platform, see the [Torque2D wiki](https://github.com/TorqueGameEngines/Torque2D/wiki) (the *Building from Source* guide).
+
+#### Generating a Visual Studio 2022 solution with CMake
+
+Generating a fresh, always-up-to-date Visual Studio solution from CMake takes just a few steps. You do **not** need to know anything about CMake to do this.
-* **Windows:** Visual Studio 2019 and 2022 (works with the free Community Edition)
-* **OSX:** Xcode
-* **Linux:** Make
-* **iOS:** Xcode_iOS
-* **Android:** Android Studio
-* **Web:** Emscripten/CMake
+1. **Install Visual Studio 2022** (the free Community Edition is fine). In the Visual Studio Installer, make sure the **"Desktop development with C++"** workload is checked.
+2. **Install CMake** from [cmake.org/download](https://cmake.org/download/). On the *Install Options* screen, choose **"Add CMake to the system PATH for all users"** (or for the current user). This one-time step is what lets the generator find CMake.
+3. In the root of the repository, **double-click `generate-vs2022.bat`**. It will create the solution under `build\vs2022\` and open `Torque2D.sln` in Visual Studio. (If CMake or the C++ workload is missing, the script tells you what to fix.)
+4. In Visual Studio, choose a configuration (**Debug** or **Release**) at the top, then build with **Build → Build Solution** (`Ctrl+Shift+B`).
+5. The compiled executable is written to the repository root (`Torque2D_DEBUG.exe` for Debug, `Torque2D.exe` for Release). Run it from there (press **F5** in Visual Studio, which is already set to launch from the repo root).
-Additionally, a **CMake** build system is now available as a cross-platform alternative to the platform-specific project files.
+Whenever the engine's source file list changes (for example after pulling new changes), just **re-run `generate-vs2022.bat`** to regenerate the solution.
See the [wiki](https://github.com/TorqueGameEngines/Torque2D/wiki) for available guides on platform setup and development.
diff --git a/TORQUE_SCRIPT.md b/TORQUE_SCRIPT.md
new file mode 100644
index 000000000..409fc1931
--- /dev/null
+++ b/TORQUE_SCRIPT.md
@@ -0,0 +1,376 @@
+# Writing TorqueScript for Torque2D
+
+Read this **before writing or refactoring any `.cs` game code.** TorqueScript is
+permissive — it will happily let you build a game as one giant bag of functions — so the
+discipline has to come from you. These rules keep a script codebase readable, leak-free, and
+able to grow past a demo.
+
+The examples here are deliberately generic (a `Coin`, an `Enemy`/`Goblin`/`Ogre` hierarchy, a
+`Weapon`/`Pistol`, a `HudManager`). Apply the *pattern*, not the names.
+
+---
+
+## 0. The one-sentence version
+
+**Every object is a class in its own file; the object builds itself in `onAdd` and frees
+everything it made in `onRemove`; each object owns and destroys the objects it creates.** The
+rest of this document is corollaries.
+
+---
+
+## 1. A namespace *is* a class — never build a god-namespace
+
+In TorqueScript, `function Foo::bar(%this)` defines method `bar` on class `Foo`. The engine
+calls it a "namespace," but for you it is a class. Therefore:
+
+> **Do not pile unrelated methods into one namespace.** If you find crystal logic, HUD logic,
+> bullet logic, and level generation all defined as `function MyGame::*`, you have written one
+> class doing everyone's job. That is the anti-pattern this whole document exists to prevent.
+
+The **only** legitimate top-level singleton is the module object — the `%this` passed to your
+module's create/destroy functions. It orchestrates; it does not implement everyone's behavior.
+
+---
+
+## 2. One class per file; name the file after the class
+
+Each class gets its own `.cs` file, and the file is named for the class. Every function in that
+file belongs to that class.
+
+- A shared prefix or suffix may be dropped from the filename: class `PlanetXCrystal` → file
+ `crystal.cs`; class `SpaceEnemy` → `enemy.cs`. The class name keeps the prefix; the file name
+ need not.
+- If a file contains `function A::foo` and `function B::bar` for two unrelated classes `A` and
+ `B`, split it. (A small base class and its one concrete subclass may share a file only if you
+ have a reason; prefer one file each.)
+
+---
+
+## 3. Reach for `ScriptObject` — especially for managers and systems
+
+Anything that isn't a visual/physical scene object but still has state and behavior should be a
+`ScriptObject` subclass, not a pile of functions on the module namespace. HUD managers, input
+controllers, level owners, weapons, spawners — all `ScriptObject`s.
+
+```cpp
+%hud = new ScriptObject() { class = "HudManager"; };
+
+function HudManager::onAdd(%this) { /* build the bars/labels */ }
+function HudManager::onRemove(%this){ /* delete them */ }
+function HudManager::setHealth(%this, %fraction) { /* ... */ }
+```
+
+Creating a `ScriptObject` with a `class` gives you a place to hang state (`%this.foo`) and
+methods, and — crucially — lifecycle callbacks (rule 4).
+
+---
+
+## 4. Lifecycle lives in `onAdd` / `onRemove`
+
+The engine **automatically** calls the script `onAdd` when an object is registered and
+`onRemove` when it is unregistered/deleted. This is true for **every** `SimObject` — `Sprite`,
+`SceneObject`, `ScriptObject`, `GuiControl` — as long as the object has a `class`.
+(See `engine/source/sim/simObject.cc`: `registerObject()` → `onAdd`, `unregisterObject()` →
+`onRemove`.)
+
+**The `new{}` block is the constructor.** Fields you set inside it are applied *before* `onAdd`
+runs, so `onAdd` can read them.
+
+### Who sets what: split by who controls the value
+
+- The **spawner** sets `class` (and `superclass`, rule 8) plus **any value it needs to
+ control** — position, an index, a difficulty stat, a target object. Those are the constructor
+ arguments.
+- The **object's `onAdd`** sets everything that is **always the same** for that class — its
+ size, image, collision shape, sub-objects, and any repeating schedules.
+
+```cpp
+// Spawner: sets only what IT decides — the class and where the coin goes.
+function Level::spawnCoin(%this, %position)
+{
+ %coin = new Sprite() { class = "Coin"; Position = %position; };
+ %this.scene.add(%coin);
+}
+
+// The coin looks the same everywhere, so it configures its own appearance.
+function Coin::onAdd(%this)
+{
+ %this.setSize("2 2");
+ %this.setImage("MyGame:coin");
+ %this.createCircleCollisionShape(1);
+ %this.setCollisionShapeIsSensor(0, true);
+ %this.spin(); // starts a repeating schedule
+}
+```
+
+If the coin *always* appeared at the same spot, `onAdd` would set the position too. The rule is
+about control, not about a fixed list of fields.
+
+### Passing parameters (including whole objects) into the constructor
+
+Set any field on the handle in the `new{}` block; `onAdd` reads it back. Object handles are just
+integers, so you can pass entire objects the same way.
+
+```cpp
+// Spawner makes a row of balls and tells each one its index.
+for (%i = 0; %i < 10; %i++)
+{
+ %ball = new Sprite() { class = "Ball"; i = %i; target = %player; };
+ %this.scene.add(%ball);
+}
+
+function Ball::onAdd(%this)
+{
+ // even balls red, odd balls blue — decided from the passed-in index.
+ if (%this.i % 2 == 0) %this.setBlendColor(1, 0, 0);
+ else %this.setBlendColor(0, 0, 1);
+ // %this.target is a live object handle passed straight in.
+}
+```
+
+### `onRemove` frees everything the object created
+
+Whatever an object `new`s, schedules, or listens to in `onAdd`, it must tear down in `onRemove`.
+This is the other half of the constructor/destructor pair, and it is what makes rule 5 work.
+
+> Note: `onAdd` runs *before* the spawner adds the object to a Scene/parent, so don't do work in
+> `onAdd` that requires scene membership. Setting size/image/collision/schedules is fine.
+
+---
+
+## 5. Ownership is a chain of responsibility
+
+Each object owns — and is responsible for deleting — the objects **it** created. A parent deletes
+only its *direct* children; the cascade does the rest:
+
+- `Scene.delete()` unregisters every SceneObject in it → each one's `onRemove` fires.
+- A `GuiControl` parent's `delete()` deletes its child controls.
+- A manager `ScriptObject` deletes the objects it holds references to, in its `onRemove`.
+
+Done right, deleting the top object recursively frees the entire tree, and **you can never leak
+by forgetting a cleanup line** — because cleanup lives next to creation, in the same class.
+
+```
+Game (module singleton)
+└─ level (ScriptObject) Game::onDestroy -> level.delete()
+ ├─ scene (Scene) Level::onRemove -> scene.delete() (frees all SceneObjects)
+ ├─ hud (HudManager) Level::onRemove -> hud.delete()
+ └─ player (SceneObject)
+ └─ weapon (Weapon) Player::onRemove -> weapon.delete()
+```
+
+**Adding to a container transfers lifetime ownership.** Once you `Scene.add(%obj)` or
+`%parent.add(%control)`, that container owns the object's lifetime — deleting the container frees
+it. A manager that *pools* SceneObjects (a weapon with a bullet pool, a level with an effects
+pool) therefore does **not** delete those objects in its own `onRemove`; the scene safeDeletes
+them when it tears down. The manager's `onRemove` only cancels the schedules and frees the
+non-scene objects it still owns:
+
+```cpp
+function Weapon::onRemove(%this)
+{
+ // Bullets were add()ed to the scene -> the scene frees them. We only own the
+ // fire/heat schedules, so those are all we cancel here.
+ %this.stopFiring();
+ if (isEventPending(%this.heatEvent)) cancel(%this.heatEvent);
+}
+```
+
+**Guard the deletes you *do* make across a boundary with `isObject()`.** When a class genuinely
+owns something that lives elsewhere and cascade order isn't guaranteed, check before deleting so a
+double-free can't happen:
+
+```cpp
+function Level::onRemove(%this)
+{
+ if (isObject(%this.hud)) %this.hud.delete(); // a ScriptObject we own
+ if (isObject(%this.scene)) %this.scene.delete(); // frees every SceneObject in it
+}
+```
+
+---
+
+## 6. Own by reference (`%this.child`), not by global name
+
+Store what you create on `%this` (`%this.healthBar`, `%this.weapon`), and delete it through that
+reference. A globally-named object (`new GuiProgressCtrl(HealthBar){...}`) is fine for engine
+*lookup*, but do not let a global name be the *only* thing keeping ownership straight — it makes
+teardown depend on remembering every name. Ownership must be explicit and local to the owner.
+
+---
+
+## 7. Track your schedules; cancel them in `onRemove`
+
+`schedule()` returns an event id. A self-rescheduling timer (a pulse, a tick loop) that is never
+cancelled keeps firing after its object is gone — an orphaned event. Store the id and cancel it:
+
+```cpp
+function Coin::spin(%this)
+{
+ %this.rotate();
+ %this.spinEvent = %this.schedule(600, "spin"); // keep the id
+}
+
+function Coin::onRemove(%this)
+{
+ if (isEventPending(%this.spinEvent))
+ cancel(%this.spinEvent);
+}
+```
+
+A defensive `if (!isObject(%this)) return;` at the top of a scheduled method is a band-aid, not a
+substitute for cancelling.
+
+---
+
+## 8. Inheritance: `class` + `superclass`
+
+Set both `class` and `superclass` on an object and the engine builds a **real, linked namespace
+hierarchy**. Method lookup walks `class → superclass → …` until it finds the method:
+
+```cpp
+%goblin = new Sprite() { class = "Goblin"; superclass = "Enemy"; };
+%goblin.attack(); // tries Goblin::attack, then Enemy::attack
+```
+
+Two things that surprise people:
+
+1. **The hierarchy is directional and sticky.** Once you create an object linking `Goblin →
+ Enemy`, the engine remembers it. Creating another object that links them the other way
+ (`Enemy` with `superclass="Goblin"`) is an error. Keep every class's superclass consistent
+ everywhere.
+
+2. **Only the most-derived `::onAdd` fires.** There is no automatic constructor chaining — if
+ `Goblin::onAdd` exists, `Enemy::onAdd` does **not** also run.
+
+### The `init()` convention (house style for shared setup)
+
+Because `onAdd` doesn't chain, put shared setup in an `init()` method the whole hierarchy shares,
+and have each concrete class's `onAdd` call it first:
+
+```cpp
+// Base: shared defaults live in init().
+function Enemy::init(%this)
+{
+ %this.health = 3;
+ %this.speed = 5;
+ %this.createCircleCollisionShape(0.6);
+}
+function Enemy::onAdd(%this) { %this.init(); } // a plain Enemy still gets set up
+
+// Subclass: run the shared init, then add specifics.
+function Goblin::onAdd(%this)
+{
+ %this.init(); // resolves to Enemy::init
+ %this.health = 5; // override a default
+ %this.setImage("MyGame:goblin"); // goblin-only setup
+}
+```
+
+One layer of inheritance is usually enough; this pattern extends to deeper trees if you need it.
+(The language also supports `Parent::method()` calls, but this codebase does not use them — the
+`init()` convention is the house style.)
+
+Prefer passing per-instance values in as constructor parameters (rule 4) over reading globals
+inside `init()`; e.g. the spawner sets `%enemy.health = %this.currentDifficultyHealth` and
+`init()` reads `%this.health`.
+
+---
+
+## 9. Composition: build it like an object system (has-a)
+
+Model relationships the way you would in any OO language. A player *has-a* weapon; the weapon is
+its own `ScriptObject`, held on `%this.weapon`, and called polymorphically. Swapping the concrete
+weapon class changes behavior with **no change to the caller**:
+
+```cpp
+function Player::onAdd(%this)
+{
+ %this.weapon = new ScriptObject() { class = "Pistol"; superclass = "Weapon"; };
+}
+function Player::onRemove(%this) // owner frees the owned object
+{
+ if (isObject(%this.weapon))
+ %this.weapon.delete();
+}
+function Player::fire(%this)
+{
+ %this.weapon.fire(%this.getMuzzlePosition(), %this.aimAngle); // Pistol or Shotgun — caller doesn't care
+}
+```
+
+Keep a subsystem's whole responsibility inside its object: a weapon owns its stats, its firing
+cadence, its cool-down, and its bullet pool — not scattered across the game namespace.
+
+---
+
+## 10. Keep the global surface minimal — but some things belong there
+
+A few things are legitimately global; the test is whether the engine requires it:
+
+- **`ActionMap` bind targets must be global functions.** `%map.bind("keyboard", "w", "moveUp")`
+ calls a bare `function moveUp(%val)`. That's fine — name them clearly and keep them thin,
+ delegating to an object: `function moveUp(%val) { $keyUp = %val; Player.updateVelocity(); }`.
+- **Genuine game-state singletons** live on the module object (state transitions, the current
+ level reference).
+
+Everything else — every value that belongs to one object — lives on that object (`%this.foo`),
+not in a `$Global::` variable. Fewer globals, fewer spooky couplings.
+
+---
+
+## 11. Before/after, in miniature
+
+**Before** — one god-namespace, cleanup by hand, a leaked schedule:
+
+```cpp
+function MyGame::buildCoin(%this, %pos)
+{
+ %c = new Sprite() { class = "Coin"; Position = %pos; Size = "2 2"; Image = "MyGame:coin"; };
+ %c.createCircleCollisionShape(1);
+ %this.scene.add(%c);
+ %c.schedule(600, "spin"); // never cancelled
+ %this.coin = %c;
+}
+function MyGame::teardown(%this)
+{
+ %this.coin.delete(); // and you must remember every such line
+}
+```
+
+**After** — a `Coin` class owns itself; teardown is a cascade:
+
+```cpp
+// coin.cs
+function Coin::onAdd(%this)
+{
+ %this.setSize("2 2");
+ %this.setImage("MyGame:coin");
+ %this.createCircleCollisionShape(1);
+ %this.spinEvent = %this.schedule(600, "spin");
+}
+function Coin::onRemove(%this)
+{
+ if (isEventPending(%this.spinEvent)) cancel(%this.spinEvent);
+}
+
+// level.cs — spawner sets only what it controls; deleting the scene frees the coin.
+function Level::spawnCoin(%this, %pos)
+{
+ %this.scene.add(new Sprite() { class = "Coin"; Position = %pos; });
+}
+```
+
+---
+
+## Checklist for a new or edited `.cs` file
+
+- [ ] Every `function X::y` in the file shares the same class `X` (or its base) — no god-namespace.
+- [ ] The file is named after its class (prefix/suffix may be dropped).
+- [ ] Managers/systems are `ScriptObject` subclasses, not functions on the module namespace.
+- [ ] The class configures itself in `onAdd`; the spawner sets only class + the values it controls.
+- [ ] `onRemove` deletes every object the class created and cancels every schedule it started.
+- [ ] Owned objects are stored on `%this`, and cross-boundary deletes are `isObject()`-guarded.
+- [ ] Shared setup across a `class`/`superclass` hierarchy goes through `init()` (only the
+ most-derived `onAdd` fires).
+- [ ] The only new globals are `ActionMap` bind targets or genuine game-state singletons.
diff --git a/build-linux.sh b/build-linux.sh
new file mode 100755
index 000000000..471c1b258
--- /dev/null
+++ b/build-linux.sh
@@ -0,0 +1,87 @@
+#!/usr/bin/env bash
+# ---------------------------------------------------------------------------
+# Builds Torque2D on Linux from CMake — one shot: configure + compile.
+#
+# This is the Linux counterpart to generate-vs2022.bat / generate-vs2026.bat.
+# Those open an IDE that does the build for you; on Linux there is no IDE in
+# the loop, so this script configures CMake AND runs the compile, leaving a
+# runnable executable at the repository root.
+#
+# Usage: ./build-linux.sh [Debug|Release|Shipping] (default: Debug)
+# Example: ./build-linux.sh Release
+#
+# If you only want to CONFIGURE (e.g. to build from an editor or by hand),
+# use generate-make.sh instead.
+#
+# Requires CMake, a C/C++ toolchain, and the dev packages. On Debian/Ubuntu:
+# sudo apt install build-essential cmake \
+# libsdl1.2-dev libx11-dev libxft-dev libfreetype6-dev \
+# libopenal-dev libgl1-mesa-dev
+# Note: GENUINE SDL 1.2 is required (NOT the SDL2-based sdl12-compat shim that
+# ships on Ubuntu 24.04+). For a 32-bit build see cmake/BUILD-PLATFORM-NOTES.md.
+# ---------------------------------------------------------------------------
+set -e
+
+# Run from the folder this script lives in, regardless of where it was launched.
+cd "$(dirname "$0")"
+
+BUILD_TYPE="${1:-Debug}"
+BUILD_DIR="build/make"
+
+echo ""
+echo " ==================================================="
+echo " Torque2D : building on Linux ($BUILD_TYPE)"
+echo " ==================================================="
+echo ""
+
+if [ ! -f "CMakeLists.txt" ]; then
+ echo " ERROR: CMakeLists.txt was not found next to this script."
+ echo " Please keep build-linux.sh in the root of the Torque2D repository."
+ exit 1
+fi
+
+if ! command -v cmake >/dev/null 2>&1; then
+ echo " ERROR: CMake was not found."
+ echo " Install it (e.g. 'sudo apt install cmake'), then run this script again."
+ exit 1
+fi
+
+# Bound parallelism to the core count. A bare 'make -j' (no number) launches
+# every translation unit at once — hundreds of g++ processes — which can
+# exhaust RAM and get the build OOM-killed (see the CI note in PR-builds.yml).
+JOBS="$(nproc 2>/dev/null || echo 4)"
+
+echo " Configuring into: $BUILD_DIR"
+echo ""
+cmake -S . -B "$BUILD_DIR" -G "Unix Makefiles" -DCMAKE_BUILD_TYPE="$BUILD_TYPE"
+
+echo ""
+echo " Compiling with $JOBS parallel jobs ..."
+echo ""
+cmake --build "$BUILD_DIR" --parallel "$JOBS"
+
+# The executable is dropped at the repo root: Torque2D_DEBUG (Debug) or
+# Torque2D (Release/Shipping). It MUST run from the repo root so it can find
+# main.cs and the script/asset trees.
+if [ "$BUILD_TYPE" = "Debug" ]; then
+ EXE="./Torque2D_DEBUG"
+else
+ EXE="./Torque2D"
+fi
+
+echo ""
+echo " ==================================================="
+echo " Success."
+echo " ==================================================="
+echo ""
+if [ -x "$EXE" ]; then
+ echo " Built: $EXE"
+else
+ echo " Built (executable expected at $EXE)."
+fi
+echo ""
+echo " Run it from the repository root:"
+echo " $EXE"
+echo ""
+echo " (It must run from the repo root to find main.cs and the asset trees.)"
+echo ""
diff --git a/cmake/BUILD-PLATFORM-NOTES.md b/cmake/BUILD-PLATFORM-NOTES.md
new file mode 100644
index 000000000..11356ee48
--- /dev/null
+++ b/cmake/BUILD-PLATFORM-NOTES.md
@@ -0,0 +1,794 @@
+# Build platform notes (CMake source-of-truth migration)
+
+Status board and handoff notes for finishing the per-platform CMake builds. The
+goal is to make CMake the single source of truth and generate the per-platform
+project files from it.
+
+## Status
+
+| Platform | CMake wiring | Configured | Built | Runtime verified |
+|----------|--------------|------------|-------|------------------|
+| Windows (VS2022) | ✅ | ✅ | ✅ Debug+Release | ✅ (GUI launches) |
+| Windows (VS2026) | ✅ | — | — | generator supported by CMake 4.x; needs VS2026 installed |
+| macOS (arm64) | ✅ | ✅ | ✅ Debug (.app, signed) | ✅ (single window; editor renders + animates) |
+| Linux x86_64 (Make) | ✅ | ✅ | ✅ Debug+Release | ✅ (GUI launches under WSLg) |
+| Linux x86 32-bit (Make, -m32) | ✅ | ✅ | ✅ Debug+Release | ✅ (boots+GL init under WSLg via llvmpipe) |
+| iOS (arm64 simulator) | ✅ | ✅ | ✅ Debug (.app, full bundle) | ✅ editor renders + touch works (user-confirmed) |
+| iOS (arm64 device) | ✅ | ✅ | ✅ Debug (.app, code-signed) | ✅ runs on a real iPad — perfect FPS, touch good (user-confirmed) |
+| Android (Gradle+CMake) | ✅ | ✅ (CI) | ✅ APK (CI) | ✅ editor boots, renders & runs on a real Pixel 7 Pro via Firebase Test Lab — main UI (Roboto) text renders; only un-baked decorative faces (e.g. "black ops one") stay blank. See Android round |
+| Web (Emscripten/WASM) | ✅ | ✅ | ✅ Debug (.html/.js/.wasm/.data) | ✅ editor renders in-browser — Project Manager UI with full TEXT (.uft cache + a FreeType-rasterized Roboto fallback for any uncached face/size) + sprites; toys render incl. blended/lit draws (PyramidToy light); stable, no crash |
+
+**Linux (32 & 64-bit) builds and links** (verified in WSL/Ubuntu 22.04). The
+**64-bit Debug GUI runtime is verified under WSLg** (`./build-linux.sh` →
+`./Torque2D_DEBUG`): the Project Manager window launches, OpenGL initializes via
+WSLg's D3D12/Mesa GL, and it shuts down cleanly. The **32-bit Debug runtime is also
+verified** under WSLg — same boot/GUI, but on llvmpipe (software GL), since WSLg's
+hardware-GL passthrough is 64-bit only. See the WSL caveat below.
+**macOS (arm64) is DONE and RUNTIME-VERIFIED** (Apple Silicon, Xcode 16.2). It
+builds + code-signs a `Torque2D_DEBUG.app`, launches as a single window from Xcode,
+boots, and the Project Manager / editor renders and animates correctly. Getting
+there past "it builds" took six runtime fixes — see the macOS round below. **iOS
+(arm64) is DONE and RUNTIME-VERIFIED on both the simulator AND a real iPad
+(iOS 26.2.1, Xcode 26.x).** The editor renders, point-based GUI scale is correct,
+and touch input works; on hardware the frame rate is perfect (the simulator's poor
+FPS was its GLES translation layer, not the engine). Getting from "builds" to "runs"
+took four runtime fixes (frozen clock, frame-allocator size, point-vs-pixel scale,
+touch release) — see the iOS round below. The device build is code-signed with a free
+Apple ID (7-day profile; no paid account).
+
+**Legacy projects retired.** With Windows, macOS, Linux (32 & 64-bit), and iOS all
+CMake-runtime-verified, the hand-maintained project files were deleted from
+`engine/compilers/` (the VS 2019/2022 solutions, the macOS `Xcode` project, the
+`Make-32bit`/`Make-64bit` Makefiles, and the `Xcode_iOS` project) — CMake is now their
+single source of truth. What remains under `engine/compilers/` is intentionally kept:
+`android-studio` (the Gradle shell that *drives* CMake via the NDK) and `emscripten`
+(the legacy reference recipe — now superseded since the Web target is CMake-runtime-verified
+via `emcmake` + the shared `PlatformSources.cmake`; kept for reference and a candidate for
+retirement). (`cmake-modules` is retained because `emscripten/CMakeLists.txt` includes
+`CopyFiles` from it.)
+
+## How the build is structured
+
+- `CMakeLists.txt` (root) — modern, target-based. Selects the active platform's
+ back-end source list and applies platform link libs/frameworks/defs.
+- `cmake/EngineSources.cmake` — explicit cross-platform engine sources (the
+ `platform/` abstraction is here; it compiles on every OS).
+- `cmake/PlatformSources.cmake` — OS-specific back-ends: `..._WINDOWS`,
+ `..._MACOS` (Objective-C++ `.mm`), `..._LINUX`, `..._IOS`, `..._ANDROID`,
+ `..._EMSCRIPTEN`.
+- `engine/lib/CMakeLists.txt` — third-party static libs (platform-neutral; MSVC
+ flags are guarded by `if(MSVC)`).
+- Generator scripts at repo root: `generate-vs2022.bat`, `generate-vs2026.bat`,
+ `generate-xcode.command` (macOS), `generate-xcode-ios.command` (iOS simulator),
+ `generate-xcode-ios-device.command` (iOS device, code-signed), `generate-make.sh`,
+ `generate-emscripten.sh` (Web/WASM via `emcmake`).
+
+## macOS round (run on a Mac) — DONE (builds, signs, runs; arm64)
+
+Verified on Apple Silicon (Xcode 16.2): builds with both generators (0 errors),
+code-signs, launches as a single window from Xcode, boots, and the editor renders +
+animates. Configure + build:
+- Xcode (recommended): `./generate-xcode.command` (or `cmake -S . -B build/xcode -G
+ Xcode`), then in Xcode pick the `Torque2D` scheme and Cmd-R. The Run scheme is
+ preconfigured with the repo root as its working directory.
+- Makefiles: `cmake -S . -B build/macos -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Debug`
+ then `cmake --build build/macos -j8`.
+- The product is a real `.app` bundle at the repo root (`Torque2D_DEBUG.app`). The
+ engine's `getExecutablePath()` finds `main.cs` in the bundle's PARENT dir, so no
+ asset repackaging is needed. NOTE: a `.app` must be launched via LaunchServices
+ (Xcode / `open` / double-click) — running the inner binary directly, or `open`ing
+ it from a headless/non-GUI session, won't deliver the launch event (launchd error
+ 153), so GUI runtime can only be checked from an interactive desktop.
+
+Resolved RUNTIME issues (these only appear once the app actually runs; six of them
+stood between "it builds" and "the editor works"):
+- **No window** — the bare executable installed no app delegate, so the engine never
+ booted. Fixed by bootstrapping AppKit programmatically in `platformOSX/main.mm`
+ (create NSApplication, set Regular activation policy, install AppDelegate, run) —
+ no nib required; the legacy `NSApplicationMain`/nib path is preserved for a bundle
+ that ships one.
+- **Crash on launch** — `GuiListBoxCtrl`'s `std::sort` comparators weren't a strict
+ weak ordering (`<=` for equality, negation for descending); modern libc++ aborts
+ on that. Fixed in `gui/guiListBoxCtrl.h`.
+- **Frozen UI / no scheduled events** — `getRealMilliseconds()` cast an out-of-range
+ `double` to `U32`, which SATURATES to a constant on arm64 (`fcvtzu`), so the sim
+ clock never advanced. Fixed via `U64` in `platformOSX/osxTime.mm`. (See the
+ recurring-bug note at the end of this section.)
+- **Duplicate windows (only under Xcode)** — the target was a `com.apple.product-
+ type.tool`, and Xcode relaunches a *tool* that becomes a GUI app via LaunchServices
+ → Terminal, spawning extra copies. Fixed with `MACOSX_BUNDLE` (a real `.app` has a
+ stable LaunchServices identity, launched once).
+- **CodeSign failure** — a `.app` must be signed to run on arm64; also the repo-root
+ `Torque2D_DEBUG.app` got contaminated by stale legacy + iOS build artifacts (iOS
+ writes a *flat* `.app` to the same path), which breaks codesign ("unsealed contents
+ in the bundle root"). Fixed with ad-hoc signing (`CODE_SIGN_IDENTITY="-"`, Manual
+ style) + cleaning the stale `.app`. **Delete the repo-root `.app` when switching
+ between the iOS and macOS builds in one checkout.**
+- **Editor UI hidden behind the background** — every fade-OUT was frozen.
+ `FluidColorI::processValue` did `(U8)mRound((target-start)*progress)`; for a fade
+ DOWN, `(target-start)` is negative and `(U8)(negative float)` saturates to 0 on
+ arm64, so alpha never left the start value. The Project Manager rendered fine but
+ sat under a `torqueCurtain` that never faded. Fixed in `math/mFluid.h` (round in
+ signed space, cast only the final sum to U8).
+
+**Recurring arm64 trap:** three of the above (clock, fades) are the SAME bug class —
+converting an out-of-range or negative float to an unsigned int **saturates** on
+arm64 where x86 silently **wrapped**. For any Apple-Silicon "value won't change"
+runtime bug, suspect this first; grep for `(U8)`/`(U32)` casts of float/time/`mRound`
+results. Casting a *positive in-range* value is fine (e.g. font metrics).
+
+Resolved BUILD issues (were latent in the scaffold):
+- **zlib needs ``.** `engine/lib/zlib/gz*.c` call `read/write/close/lseek`;
+ zconf.h only includes `` when `Z_HAVE_UNISTD_H` is set. Modern clang
+ errors on the otherwise-implicit declarations. Fixed by defining `HAVE_UNISTD_H`
+ on the zlib target for `UNIX` (`engine/lib/CMakeLists.txt`).
+- **Classic-Mac-OS landmines in the vendored libs (`TARGET_OS_MAC`).** Several of
+ the old vendored C libs gate code on `MACOS`/`TARGET_OS_MAC` assuming it means
+ *Classic* Mac OS. But `TARGET_OS_MAC` is 1 on ALL modern Apple platforms (macOS
+ *and* iOS), so those branches wrongly fire and reference headers/behaviour that
+ no longer exist. They are latent on older SDKs (which don't define
+ `TARGET_OS_MAC` until late) and fire on newer ones (Xcode 16.4 / macOS SDK 15.5 /
+ iPhoneOS 18.5 on the CI runners). Each surfaces only once the prior is fixed,
+ since the libs build in sequence. Fixed in place by excluding modern Apple
+ (`!defined(__APPLE__)`):
+ - `zlib/zutil.h` — `#define fdopen(fd,mode) NULL` clobbered the SDK's
+ `` `fdopen` (`HAVE_UNISTD_H` above pulls `` early, which
+ defines `TARGET_OS_MAC` before the branch).
+ - `lpng/pngpriv.h` — included the dead Classic-Mac `` instead of ``.
+ The other Apple-compiled libs are clean: `ljpeg/jconfig.h` already handles
+ `__APPLE__`; `libogg`/`libvorbis` only branch on `_WIN32`. (The `TARGET_OS_MAC`
+ hits under `engine/lib/openal/*` and `engine/lib/freetype/android/*` are for
+ Windows/Android/iPhone-framework headers NOT compiled by the Apple CMake build —
+ macOS/iOS link the system OpenAL.framework and use Cocoa/UIKit fonts.)
+- **Cocoa prefix header.** The `platformOSX` `.mm` back-end uses AppKit/Foundation
+ types at file scope (NSApplicationMain, NSEvent, NSCursor, NSAutoreleasePool,
+ NSTask, NSString, ...) and relied on the legacy Xcode prefix header. Reproduced
+ by force-including `tools/CMake/macOS-Prefix.h` (`-include`, guarded by `__OBJC__`
+ so C/C++ TUs are unaffected).
+- **One stray include.** `osxCocoaUtilities.mm` used `#import "fileDialog.h"`; fixed
+ to the canonical `"platform/nativeDialogs/fileDialog.h"` (matches every other
+ reference; the header dir was never on the search path).
+- **Architecture:** `CMAKE_OSX_ARCHITECTURES` is set to `arm64` (Apple Silicon) in
+ the `APPLE` block, overridable on the command line for Intel/universal builds. The
+ old build hard-coded `x86_64`.
+- **Deployment target:** `CMAKE_OSX_DEPLOYMENT_TARGET` is pinned to `11.0` (Big Sur,
+ the arm64 floor). Without it the binary inherited the host SDK default (14.6 here),
+ needlessly excluding older Macs. 11.0 does not block a future Metal renderer
+ (Metal/MetalKit ship since 10.11; only the Metal 3 feature set would need 13.0+).
+ The legacy Xcode project used 10.13 (an Intel-era value; predates arm64).
+
+Comparison against the legacy `engine/compilers/Xcode` project (now **retired** — see
+it in git history before the CMake migration; differences below are deliberate or
+benign, not bugs):
+- **C++17** (vs legacy C++14) and the **modern C standard** (vs legacy `gnu89`) are
+ intentional. `gnu89` is precisely what masked the zlib implicit-declaration error;
+ it's fixed properly via `HAVE_UNISTD_H` rather than by loosening the C dialect.
+- The legacy project linked `ApplicationServices` and `QD` (QuickDraw) frameworks
+ and put `QD.framework` on the header path. The CMake build compiles and links
+ without them (QuickDraw is long dead; no active code includes it). Add them back
+ only if a runtime/link symbol turns up missing.
+- Source membership: the CMake build is a SUPERSET of the legacy project's compiled
+ sources — nothing is dropped. Third-party libs are separate static-lib targets
+ (`engine/lib/`); libjpeg uses `jmemmgr.c`+`jmemnobs.c` where legacy used the
+ equivalent `jmemansi.c`. CMake additionally compiles the editorToy sources,
+ `arrayObject`, `b2ParticleAssembly`, the (arm64-inert) x86 SIMD math TUs, and the
+ GoogleTest suite.
+
+## iOS round (run on a Mac) — DONE (runtime-verified; arm64 simulator AND real device)
+
+iOS is a **separate** platform from macOS (distinct `platformiOS/` sources and a
+UIKit/OpenGL-ES framework stack), and was **never supported by the old CMake** —
+the recipe here was derived fresh from the maintained `engine/compilers/Xcode_iOS`
+project. `CMakeLists.txt` distinguishes it via `TORQUE_IOS` (since `APPLE` is true
+on iOS too). Verified building/linking a `Torque2D_DEBUG.app` against the iOS 18.2
+simulator SDK.
+
+Configure (arm64 simulator — no code-signing needed for a first pass):
+```
+cmake -S . -B build/ios -G Xcode -DCMAKE_SYSTEM_NAME=iOS \
+ -DCMAKE_OSX_SYSROOT=iphonesimulator -DCMAKE_OSX_ARCHITECTURES=arm64 \
+ -DCMAKE_OSX_DEPLOYMENT_TARGET=12.0
+cmake --build build/ios --config Debug
+```
+Use full Xcode, not just the Command Line Tools — point at it with
+`DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer` (or `sudo xcode-select -s`).
+
+**Device build (code-signed):** `./generate-xcode-ios-device.command` (configures into
+`build/ios-device` with `-DCMAKE_OSX_SYSROOT=iphoneos`). The root `CMakeLists` detects
+simulator-vs-device from the SDK: a `[Ss]imulator` sysroot keeps `CODE_SIGNING_ALLOWED=NO`;
+anything else (incl. the default empty → iphoneos) switches to `CODE_SIGN_STYLE=Automatic`.
+Supply the signing identity one of two ways: pass `-DTORQUE_IOS_TEAM=<10-char team id>`
+(the device script forwards `$TORQUE_IOS_TEAM`/`$TORQUE_IOS_BUNDLE_ID` from the env), or
+leave it empty and pick the team in Xcode's target → Signing & Capabilities. The bundle id
+(`-DTORQUE_IOS_BUNDLE_ID`, default `org.torque2d.Torque2D`) must be unique to the Apple
+account; a free Apple ID works for running on your own device (7-day profile). On the device:
+enable Developer Mode (Settings → Privacy & Security), and trust the cert after first install
+(Settings → General → VPN & Device Management). A device `.app` is a thin/arm64 bundle; the
+same POST_BUILD content copy applies, so `main.cs` + the trees ship inside it.
+
+**Verified on real hardware** (an iPad on iOS 26.2.1, Xcode 26.x) — the editor runs, the
+point-based GUI scale is correct, touch input works, and the frame rate is perfect. Toolchain
+note: an iOS 26 device needs Xcode 26.x (hence a recent macOS — this required upgrading off
+Sonoma 14.6.1). Free-account first-launch gotcha that cost real time: the device showed
+"Unable to Verify App — An Internet connection is required" / "Developer App Certificate is
+not trusted" *even while online*. With a free Personal Team, iOS must reach Apple's cert server
+(`ppq.apple.com`) on first launch, and that check fails silently on clock skew or a stuck network
+state. **What fixed it: restart the iPad, then Verify** (with Date & Time on Automatic). Toggling
+airplane mode and dropping to cellular-only did NOT help. A paid Developer Program account would
+remove the online-verify step (and the 7-day expiry) entirely, but it isn't needed for spot-testing.
+
+Resolved issues:
+- **`TORQUE_OS_IOS` must be predefined by the build.** `platform/types.gcc.h` gates
+ the iOS branch on `TORQUE_OS_IOS` but only *defines* it inside that branch — a
+ chicken-and-egg. Without it, `__APPLE__` selects the macOS/desktop-GL back-end and
+ fails on ``. The `TORQUE_IOS` block now defines `TORQUE_OS_IOS` (as
+ the legacy Xcode_iOS project did).
+- **UIKit prefix header.** Same mechanism as macOS: force-include
+ `tools/CMake/iOS-Prefix.h` (imports UIKit + Foundation, `__OBJC__`-guarded).
+- **`glDrawArraysProcPtr` collision (modern SDK).** The debug-only "outline GL"
+ feature does `#define glDrawArrays glDrawArraysProcPtr`. On the modern SDK the
+ prefix header drags GLES `gl.h` in a second time (UIKit → CoreImage), and the
+ macro rewrites the SDK's `glDrawArrays` *function* decl into `glDrawArraysProcPtr`,
+ colliding with the engine's same-named *variable*. Fixed by defining
+ `NO_REDEFINE_GL_FUNCS` for the iOS target (the engine's own escape hatch) — the
+ outline/wireframe debug draw becomes a no-op on iOS. (macOS is unaffected because
+ the Cocoa prefix header doesn't pull GL.)
+- **`graphics/bitmapPvr.cc` (PVR textures)** is required on iOS but excluded from
+ desktop builds in `EngineSources.cmake`; the `TORQUE_IOS` block adds it back.
+- **GameKit:** `"-framework GameKit"` is linked for `platformiOS/GameCenter.mm`.
+- **Output location:** like every target, the `.app` lands at the repo root
+ (`Torque2D_DEBUG.app`). A stale macOS bundle of the same name can leave a leftover
+ `Contents/` subdir inside it — harmless; delete it if it bothers you.
+
+Comparison against the legacy `engine/compilers/Xcode_iOS` project (now **retired** —
+see git history): same as macOS,
+the CMake source set is a superset (it also compiles the GoogleTest suite, which the
+legacy iOS project omitted). Deployment target is set to 12.0 to match legacy
+(`XCODE_ATTRIBUTE_IPHONEOS_DEPLOYMENT_TARGET` + `CMAKE_OSX_DEPLOYMENT_TARGET`);
+note the arm64 *simulator* slice reports minos 14.0 (simulators have their own floor)
+— a device build honors 12.0. Legacy used C++14; we use C++17 (engine-wide).
+
+Runnable bundle — DONE. The first simulator run showed the app launching but
+**nothing rendering**, because the CMake-generated `.app` was bundle-incomplete: it
+had no `CFBundleIdentifier`, no storyboard, and no game content. The iOS app's
+window is NOT created programmatically — `T2DAppDelegate` (`@synthesize window`, no
+`UIWindow alloc`) relies on a **Main storyboard** (`UIMainStoryboardFile`) to
+instantiate `T2DViewController` (a `GLKViewController`) hosting `T2DView` (the
+`GLKView` the engine renders into). The bare CMake plist had none of that, so the
+delegate launched but no window/GL view ever existed. Fixed by reproducing the
+legacy `engine/compilers/Xcode_iOS` bundle in CMake (`TORQUE_IOS` block):
+- **`tools/CMake/iOS-Info.plist.in`** (wired via `MACOSX_BUNDLE_INFO_PLIST`):
+ bundle id, `UIMainStoryboardFile`(+`~ipad`), `UILaunchStoryboardName`,
+ `LSRequiresIPhoneOS`, landscape orientations, `UIRequiresFullScreen`. Device
+ family + bundle id are ALSO set as build settings (`TARGETED_DEVICE_FAMILY`,
+ `PRODUCT_BUNDLE_IDENTIFIER`) because Xcode overrides the matching plist keys and
+ warns otherwise.
+- **Storyboards** bundled as compiled resources (ibtool): `iPhoneStoryboard` /
+ `iPadStoryboard` (copied from the legacy project into `tools/CMake` so the build
+ is self-contained — the GLKit UI) + a new minimal static **`LaunchScreen`**
+ (modern iOS needs a launch storyboard for a full-screen drawable; it may contain
+ no custom classes, so it's separate from the GL storyboards).
+- **Game content** copied into the flat `.app` root via POST_BUILD: `main.cs` +
+ `editor/`, `library/`, `toybox/`, `tools/`. An installed iOS app is sandboxed, so
+ `getExecutablePath()` resolves to the bundle and the content must live inside it
+ (the desktop build instead runs from the repo-root cwd). `main.cs` boots the
+ editor (`exec "./editor/main.cs"`), same as desktop.
+
+Verified: `xcodebuild ... -sdk iphonesimulator -arch arm64` BUILD SUCCEEDED with the
+storyboards compiled/linked and all content present in `Torque2D_DEBUG.app`. Generate
+with `./generate-xcode-ios.command`.
+
+Runtime — DONE (the editor renders; user-confirmed from Xcode on an iPad Pro 11" M4
+iOS 18.3 simulator). Three fixes stood between "launches" and "renders":
+- **Black screen #1 — frozen clock.** `platformiOS/iOSTime.mm` `getRealMilliseconds()`
+ was the **arm64 float→unsigned saturation** bug (see the macOS round), in iOS's own
+ time file that the `osxTime.mm` fix never touched: `mach_absolute_time() *
+ absolute_to_millis` (a huge double) was stored in a Carbon `Duration` (SInt32) and
+ saturated to INT_MAX on arm64 → constant time → sim clock never advanced → the
+ editor's fade-in curtain stayed opaque → black. Fixed via U64 (mirrors `osxTime.mm`).
+ `mFluid.h`/`guiListBoxCtrl.h` are shared headers, already applied to iOS.
+- **Black screen #2 — FrameAllocator assert.** `game/defaultGame.cc` initialized the iOS
+ frame allocator at 256/512KB (a 2013, ~256MB-RAM-era value), but `main.cs` boots the
+ full desktop editor; a boot-time allocation overran the buffer and tripped the fatal
+ `frameAllocator.h:102` "alloc too large" assert → halt. Bumped iOS to the desktop 3MB
+ (negligible on modern devices; Android/Emscripten keep the small budget).
+- **Half-size GUI + broken scene picking — pixel-vs-point coordinate mismatch.**
+ The engine ran in PIXEL resolution (`$pref::iOS::Width = points * scale`) while the GL
+ backing was built at POINT resolution (`createFramebuffer` runs in `viewDidLoad` and the
+ old `contentScaleFactor` set in `Platform::initWindow` ran later and only for `scale==2`).
+ The decision (user direction) is to render in **points**, not pixels: a Retina display
+ packs 2-3x the pixels into the same physical area, so a GUI laid out in raw pixels is
+ half/third size. The fix makes logical resolution, GL backing, and touch input all share
+ ONE point-space coordinate system:
+ * `iOSWindow.mm` `Platform::init`: `$pref::iOS::Width/Height` = point bounds (dropped the
+ `* screenScale`).
+ * `T2DViewController.mm`: force `view.contentScaleFactor = 1` BEFORE `createFramebuffer`
+ (a UIView otherwise defaults it to the screen scale), and `retinaEnabled = false` so
+ touch points are not scaled up. `iOSWindow.mm` `initWindow` also pins it to 1 (and no
+ longer special-cases `scale==2`).
+ Trade-off: rendering is at point resolution (UIKit upscales the layer to the physical
+ screen → slightly soft on Retina), but GUI sizing and scene picking are correct. A future
+ improvement could decouple a points logical space from a pixel backing for crisp Retina,
+ but that needs the GL viewport to use `backingWidth` while the projection stays in points.
+ (Confirmed correct on the simulator and on a real iPad.)
+- **Touch never released (press stuck) — two layers.** (1) `iOSInput.mm` `createMouseUpEvent`
+ posted `SI_BREAK` only on an EXACT coordinate match with the stored slot; the simulator's
+ mouse-up lands a pixel off → event dropped. Also it never freed the slot (leak → eventually
+ no downs). Fixed: fall back to the first occupied slot, set the cursor to the release point,
+ clear the slot. (2) The REAL stick was in `gui/guiCanvas.cc` `rootScreenTouchUp`: unlike the
+ desktop `rootMouseUp`, it ignored `mMouseCapturedControl` and only dispatched to the control
+ under the release point. A `GuiButtonCtrl` calls `mouseLock()` in `onTouchDown`, so if the
+ release didn't re-hit the button it stayed locked + depressed with no way to release. Fixed
+ by routing the up to the captured control first (mirrors `rootMouseUp`); touch-only, so
+ desktop is unaffected.
+
+All four fixes are confirmed working in the simulator AND on a real iPad. The simulator's
+**poor frame rate did NOT carry to hardware** — on the device the frame rate is perfect, so it
+was the simulator's GLES translation layer, not an engine problem. Note that on touch there is
+no hover/move without a finger down, so the editor cursor only tracks during a press — that is
+inherent to touch, not a bug.
+
+Heads-up on the shared output path: iOS and macOS both emit `Torque2D_DEBUG.app` to
+the repo root but with INCOMPATIBLE layouts (iOS = flat; macOS = `Contents/`).
+Building one then the other in the same checkout corrupts the bundle and breaks
+codesign — `rm -rf Torque2D_DEBUG.app` when switching platforms.
+
+## Linux round (run in WSL or on Linux) — DONE (builds & links, 32 & 64-bit)
+
+1. Install deps (Debian/Ubuntu):
+ `sudo apt install build-essential cmake nasm libsdl1.2-dev libx11-dev libxft-dev libfontconfig1-dev libfreetype6-dev libopenal-dev libgl1-mesa-dev`
+ For 32-bit add the multilib toolchain + `:i386` libs:
+ `sudo dpkg --add-architecture i386 && sudo apt update && sudo apt install gcc-multilib g++-multilib libsdl1.2-dev:i386 libx11-dev:i386 libxft-dev:i386 libfontconfig1-dev:i386 libfreetype6-dev:i386 libopenal-dev:i386 libgl1-mesa-dev:i386`
+ **fontconfig is a direct dependency, not just Xft's:** `x86UNIXFont.cc` calls
+ `Fc*` itself for `PlatformFont::enumeratePlatformFonts` (the installed-font list
+ the GUI tools offer), so the Linux link list carries `fontconfig` explicitly.
+ **Gotcha (per-arch `-dev`, and they do NOT coexist):** `libsdl1.2-dev:amd64`
+ and `libsdl1.2-dev:i386` conflict (shared files like `sdl-config`), so only one
+ can be installed at a time — installing one removes the other. A box prepped for
+ 32-bit has only `libsdl1.2-dev:i386` (which still provides `sdl-config`, masking
+ the problem), so a default 64-bit configure fails `find_library(SDL12_LIBRARY)`
+ with "SDL 1.2 not found"; install `libsdl1.2-dev` (`:amd64`) to build 64-bit.
+ The reverse bites the 32-bit build: with the amd64 `-dev` installed, the i386
+ dev symlink `/usr/lib/i386-linux-gnu/libSDL.so` is gone (only the runtime
+ `libSDL-1.2.so.0` from `libsdl1.2debian:i386` remains), so `find_library` can't
+ find it. The headers are arch-independent (shared), so the fix is to point CMake
+ at the i386 runtime directly: `-DSDL12_LIBRARY=/usr/lib/i386-linux-gnu/libSDL-1.2.so.0`
+ (no sudo; leaves the 64-bit setup intact). Alternatively recreate the symlink
+ (`sudo ln -s libSDL-1.2.so.0 /usr/lib/i386-linux-gnu/libSDL.so`) or swap dev
+ packages per build.
+2. 64-bit one-shot: `./build-linux.sh [Debug|Release|Shipping]` (configures **and**
+ compiles, bounding `--parallel` to `nproc`, leaving the exe at the repo root).
+ Configure-only: `./generate-make.sh Debug` then `cmake --build build/make -j$(nproc)`.
+ 32-bit (verified building, linking, and running): configure with
+ `-DCMAKE_C_FLAGS=-m32 -DCMAKE_CXX_FLAGS=-m32 -DCMAKE_EXE_LINKER_FLAGS=-m32`
+ (and `PKG_CONFIG_PATH=/usr/lib/i386-linux-gnu/pkgconfig`, plus the SDL override
+ above when only the amd64 `-dev` is present), then build. `-m32` makes CMake
+ auto-detect `CMAKE_LIBRARY_ARCHITECTURE=i386-linux-gnu`, so OpenGL/FreeType/etc.
+ resolve to `/usr/lib/i386-linux-gnu`; the root picks the bitness code path from
+ `CMAKE_SIZEOF_VOID_P`. Note both builds emit `Torque2D_DEBUG` at the repo root,
+ so they overwrite each other — use separate build dirs (`build/make`, `build/make32`)
+ and rebuild whichever bitness you want at the root.
+3. Resolved issues (the original scaffold's wrong assumptions):
+ - **SDL 1.2 is REQUIRED, not optional.** The back-end calls 1.2-only APIs
+ (`SDL_GetVideoSurface`, `SDL_WM_*`, `SDL_*GammaRamp`, `SDL_GL_SwapBuffers`)
+ and pulls `X11_KeyToUnicode` out of `libSDL`. This is **NOT SDL2** — and NOT
+ the SDL2-based `sdl12-compat` shim, which lacks `X11_KeyToUnicode` (so CI is
+ pinned to ubuntu-22.04, which still ships genuine SDL 1.2.15).
+ - **`detectX86CPUInfo`** comes from `platform/platformCPUInfo.asm`, 32-bit-only
+ NASM (does not assemble for elf64). It's referenced only `#ifndef TORQUE_64`,
+ so 64-bit defines `TORQUE_64` (asm unneeded); 32-bit assembles it via NASM.
+ - **Bitness macros:** 64-bit defines `TORQUE_64` (`__amd64__` is auto); 32-bit
+ defines `i386` (bare `i386` isn't predefined under standard C++, and
+ `types.gcc.h`'s CPU detection keys off it).
+ - **OpenGL/FreeType** are resolved via `find_package`; SDL via
+ `find_library`/`find_path` (the latter so `#include ` resolves).
+4. **WSL runtime — VERIFIED under WSLg (64-bit Debug).** On a WSL2 box with WSLg
+ up (`DISPLAY=:0`, `WAYLAND_DISPLAY=wayland-0`, `/mnt/wslg/.X11-unix/X0`),
+ `./Torque2D_DEBUG` launches the Project Manager GUI: OpenGL initializes through
+ WSLg's GL stack (`Renderer: D3D12 (...) Mesa`), screen mode sets, editor modules
+ load, and it exits 0 on close. The `X11_KeyToUnicode()` warning at startup is
+ expected (the genuine-SDL-1.2 symbol, see above) and harmless. Without WSLg/an X
+ server the build/link still verifies but the window won't appear.
+ **32-bit also boots under WSLg**, but falls back to **llvmpipe (software GL)** —
+ `Renderer: llvmpipe (...)` rather than the 64-bit `D3D12 (NVIDIA ...)`, because
+ WSLg's hardware-GL passthrough (the d3d12 Mesa driver) is 64-bit only. It still
+ renders; on real 32-bit hardware it would use the native GL driver.
+
+## Android round (build via CI or Android Studio + NDK)
+
+Android builds a **shared library** (`libtorque2d.so`) via Gradle → `externalNativeBuild { cmake }`
+(the root `CMakeLists.txt`), loaded by a NativeActivity; the script/asset tree is copied into the
+APK assets by the `copyGame` Gradle task. The old `Android.mk` (stale, referenced deleted files)
+and its `.cxx` cache were removed; the Gradle project was modernized to AGP 8.6 / Gradle 8.7 /
+`compileSdk 34` / `namespace`. Target is **arm64-v8a only** (the only ABI with prebuilt
+freetype/openal). Build it with the CI job (`./gradlew assembleDebug`) or open
+`engine/compilers/android-studio` in Android Studio.
+
+**DONE — the Android CI job builds a working APK (arm64-v8a).** Getting there took
+(all are legit cross-platform correctness fixes):
+- `settings.gradle`: `plugins{}` must follow `pluginManagement{}` (before `dependencyResolutionManagement`).
+- Committed the vendored prebuilt arm64 `libfreetype.a` / `libopenal.so` (the global `*.a`/`*.so`
+ ignore was hiding them, so CI had nothing to link).
+- `types.gcc.h`: detect `__aarch64__` (NDK/Linux) as 64-bit ARM, not just Apple's `__arm64__`
+ (fixed CPU/endian + `TORQUE_CPU_X64`).
+- `-Wno-register` for non-MSVC (the engine uses the C++17-removed `register` in ~35 files; clang errors).
+- `mMathSSE.cc`: gate the x86 SSE inline asm on `TORQUE_CPU_X86_64`, not `TORQUE_CPU_X64`
+ (which is now also set for arm64).
+- Added `platformAndroid` to the Android include path (`T2DActivity.h` includes the vendored
+ ``).
+
+**Runtime — DONE (verified on a real Pixel 7 Pro via Firebase Test Lab).** With no local
+arm64 device, the APK is run on a **real Pixel 7 / 7 Pro** through Firebase Test Lab (Robo
+test, free Spark tier). The local x86_64 emulator *can* run the arm64 APK via NDK ARM
+translation, but a crash there lands in an anonymous translated-code region that can't be
+symbolicated — so use a real device. Debug loop: build → FTL Robo run → download the logcat →
+`ndk-stack -sym -dump `
+(the unstripped lib keeps full symbols + line numbers). The logcat tag for the engine's own
+`Con::printf` output is **`Torque2D`** (filter on it to read the boot narrative).
+
+What works: the editor **boots, renders, and runs**. The logcat shows the full boot —
+EGL + GLESv1/v2 + OpenAL init, GL up on **Mali-G710 / OpenGL ES-CM 1.1** (screen mode
+2232×1080×32, Max Texture Size 16383), **all five editor modules register** (EditorCore,
+EditorConsole, ProjectManager, AssetAdmin, GuiEditor), Android logs
+`Displayed …MyNativeActivity: +297ms` (first frame drawn), the app runs ~35 s and Robo
+crawls the live window — **zero native crashes**. Main UI text (Roboto) renders.
+
+Fixed to get there (each crash surfaced the next, the same crash-by-crash bring-up the
+other platforms went through):
+- **Empty main.cs dir (the module-registration crash).** Android's process cwd is `/`, so
+ `defaultGame.cc` resolved `main.cs` to `/main.cs`, then chopped the filename at the
+ *leading* slash, leaving an **empty** main.cs dir. That empty string became the `cwd`
+ for every `Platform::makeFullPathName`, so `endptr = buffer + strlen("")-1 = buffer-1`
+ (before the buffer) → out-of-bounds path math → SIGSEGV in `catPath`. Fixed in
+ `defaultGame.cc`: when the script is at the filesystem root, keep `/` as the directory
+ rather than truncating to `""`. Also hardened `platformFileIO.cc` `makeFullPathName` /
+ `catPath` (signed remaining-length via `getMax(...,0)` + a `len<3` guard) so a
+ degenerate cwd can't overrun the buffer again — a latent cross-platform bug.
+- **Font init crash #1 (`AndroidFont::getCharInfo`, null deref, fault 0x98).** Two layers,
+ both fixed earlier: the editor's font failed to load (`FT_New_Face` errored), AND
+ `AndroidFont::create()` returned `true` even on failure so `getCharInfo` dereferenced an
+ invalid `FT_Face`. Now `create()` propagates the real result (→ `createPlatformFont()`
+ returns NULL, which `GFont::create`/`GuiControlProfile::getFont` already tolerate) and
+ `getCharInfo` guards `!fontFaceCreated || face == NULL`. This stopped the crash *inside*
+ AndroidFont but exposed the next one — the font still wasn't loading.
+- **Font init crash #2 — the font never resolved (`GFont::isValidChar` null-`this`, fault
+ 0x1e0 ← `GuiMenuBarCtrl::calculateMenus`).** Root cause was the **Java** side:
+ `FontManager.TTFAnalyzer.getTtfFontName()` only read Macintosh (`platformID == 1`) name
+ records, but the bundled `Roboto-Regular.ttf` *and* the Pixel's own system fonts ship
+ **only** Windows (`platformID == 3`, UTF-16BE) records. So the enumerated font map came up
+ empty, `getFont("Roboto")` returned null, `AndroidFont` failed, `GFont::create` returned
+ NULL, and `GuiMenuBarCtrl` (one of ~30 GUI sites that deref `getFont()` unguarded)
+ crashed. Fixed by accepting `platformID` 1/3/0 and decoding UTF-16BE for 3/0
+ (`FontManager.java`). Roboto now resolves → the main UI renders text. (We chose the
+ root-cause fix over hardening all ~30 `getFont()->` deref sites; those stay reliant on the
+ font loading, which now holds. The unguarded sites remain a latent cross-platform
+ robustness gap if a font is ever genuinely missing.)
+- **Frame-allocator overflow (`FrameAllocator::alloc` SEGV ← `GFont::read` of a `.uft` ←
+ `GuiListBoxCtrl::updateSize`).** Once fonts actually loaded, reading a cached `.uft` glyph
+ table allocates a `FrameTemp` larger than Android's **512 KB** frame allocator and
+ overran it. This is the SAME class already fixed for iOS/Emscripten (the desktop-class
+ editor boot needs 3 MB); Android was the last platform left on the small budget. Collapsed
+ `defaultGame.cc` to give **every** platform the 3 MB buffer (negligible on any modern
+ device). This was the last boot crash.
+
+**Checked, NOT a bug on Android: the arm64 float→unsigned saturation class** (frozen clock /
+frozen fades; fixed on macOS/iOS in `osxTime.mm` / `iOSTime.mm` / `mFluid.h`). `mFluid.h` is
+shared (already applied), and `AndroidTime::getRealMilliseconds` is **safe** — unlike the
+mac/iOS code that cast a huge raw time, it subtracts a startup baseline (`android_StartupTime()`
+at boot, `T2DActivity.cpp:1194`), so the value stays in U32 range. No frozen clock.
+
+Cosmetic follow-up (NOT a crash, DEFERRED): un-baked **decorative faces with no `.uft`
+cache** (e.g. the editor title font **`black ops one`** 21/28) render blank — `getFont` for
+them falls back to `Helvetica`, which doesn't exist on Android, so `createSafePlatformFont`
+"utterly fails". This is the exact gap the web build had before its per-face FreeType
+fallback. The intended fix is a per-face → bundled-Roboto fallback (so any unresolved face
+rasterizes from Roboto instead of disappearing), mirroring the Emscripten `EmscriptenFont`
+approach.
+
+**Two fallback attempts were made and BOTH reverted — deferred pending better runtime
+evidence.** (1) In `AndroidFont::create` (C++): on a failed face, call `getFontPath("Roboto")`
+and retry — this adds a SECOND `getFontPath` JNI call (extra `AttachCurrentThread`/`Detach`
+on the engine thread). (2) In `FontManager.getFont` (Java): return the Roboto path instead of
+null when no face matches (no new JNI). **Both** FTL runs showed the engine thread going
+**silent right after the `FileWalker` "time in dir java" log** — no crash, no further GL/frame
+activity, and **zero `Torque2D`-tagged lines for the whole run** (even ones that precede
+`FileWalker`, like `Input Init`, which DID appear in the good `ewrz` run). No `logd` "chatty"
+drop markers tie to the app, so it's **ambiguous**: either FTL's logcat capture silently lost
+the high-volume `Torque2D` tag (plausible — `ewrz` captured it, these didn't) OR boot really
+hangs there. The two attempts use entirely different mechanisms (C++ JNI vs pure-Java map
+lookup), and code review finds **no plausible deterministic hang** in the Java path, which
+points at FTL flakiness — but it couldn't be proven from lossy n=1 FTL logs. Since this is
+purely cosmetic and the verified-working state (`ewrz`: full boot, all modules, no crash) is
+already shipped, the fallback was reverted. **Re-attempt on a local arm64 device with reliable
+`adb logcat`** (not FTL) to disambiguate before committing; if it's confirmed flaky, the Java
+`getFont` → Roboto fallback is the cleaner of the two (single JNI call).
+
+Remaining iteration notes:
+- **Engine source set under GLES/NDK:** the unified `EngineSources.cmake` will surface files that
+ need `TORQUE_OS_ANDROID`/GLES guards (the old `Android.mk` compiled a smaller, divergent subset).
+ Fix by guarding the code, not by forking the list.
+- **OpenAL `.so` packaging:** shipped via `jniLibs.srcDirs = ['../../../lib/openal/Android']`; confirm
+ it lands in the APK and loads.
+- **Java glue** (`MyNativeActivity`, helpers) may need minor AndroidX/API updates under AGP 8.
+- **Gradle wrapper jar** is old (5.4.1-era) but should bootstrap 8.7; regenerate with
+ `gradle wrapper --gradle-version 8.7` if it doesn't.
+- Other ABIs (armeabi-v7a/x86/x86_64) are a later round — need freetype/openal built from source.
+
+## Emscripten / Web round (build with emsdk) — DONE: editor + toys render in-browser
+
+Emscripten builds the engine to **WebAssembly** (`emcc`), emitting
+`Torque2D_DEBUG.{html,js,wasm,data}`. The browser owns the event loop, so
+`platformEmscripten/main.cpp` drives the engine with
+`emscripten_set_main_loop(_EmscriptenGameInnerLoop, 60, false)` → `Game->mainLoop()`
+once per animation frame — the SAME callback model as iOS/Android (no blocking
+`while`). The back-end (`engine/source/platformEmscripten/*`) existed but was years
+bit-rotted and had never been CMake-built; the legacy `engine/compilers/emscripten`
+recipe was a hand-maintained flat source list (it still compiled the dead `spine/*`
+tree). This round wires Emscripten into the **shared** `EngineSources.cmake` (the
+Android pattern: one list + guards), NOT the legacy list.
+
+### Build (Windows host; works from any host with emsdk)
+1. Install the Emscripten SDK once and activate it:
+ `git clone https://github.com/emscripten-core/emsdk && cd emsdk && ./emsdk install latest && ./emsdk activate latest`,
+ then `source ./emsdk_env.sh` (or `emsdk_env.bat`). Verified with **emsdk 6.0.1**.
+ - **Windows/Git-Bash gotcha:** `emsdk_env.sh` shells out to `python`, which on
+ Windows hits the Microsoft-Store `python` stub and silently fails to export
+ `EMSDK`/PATH. Work around it by driving the tools directly:
+ `export EM_CONFIG=/c/Users//emsdk/.emscripten` and prepend
+ `.../emsdk/upstream/emscripten` and `.../emsdk/node//bin` to `PATH`. Then
+ `emcc`/`emcmake` work.
+2. Configure + build (a `make` program must be on PATH — `mingw32-make`, MSYS make,
+ or the chocolatey `make` all work):
+ `./generate-emscripten.sh` (= `emcmake cmake -S . -B build/emscripten -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Debug`)
+ then `cmake --build build/emscripten -j`. `emcmake` injects the Emscripten
+ toolchain and sets the CMake `EMSCRIPTEN` variable, which selects the
+ `platformEmscripten` back-end.
+3. **Run:** serve over HTTP (NOT `file://`) and open the page:
+ `cd build/emscripten && python -m http.server 8000` → `http://localhost:8000/Torque2D_DEBUG.html`.
+
+### How it's wired (root `CMakeLists.txt` + `PlatformSources.cmake`)
+- `TORQUE_PLATFORM_SOURCES_EMSCRIPTEN` lists the `platformEmscripten/*.cpp` back-end
+ (incl. `EmscriptenGL2ES.cpp`, the fixed-function→GLES immediate-mode shim, which
+ the legacy list wrongly omitted).
+- The `EMSCRIPTEN` branch is matched **before** `UNIX` in the platform dispatch (the
+ Emscripten toolchain sets `UNIX=1`, exactly like Android).
+- **`EMSCRIPTEN=1` is defined GLOBALLY** (before `add_subdirectory(engine/lib)`) — emcc
+ only predefines `__EMSCRIPTEN__`, but the engine (`types.gcc.h`, which then defines
+ `TORQUE_OS_EMSCRIPTEN`) AND vendored libs (`ljpeg/jconfig.h`) key off bare
+ `EMSCRIPTEN`. Without it ljpeg fails with "No jconfig.h was included". The legacy
+ recipe did the same via a global `ADD_DEFINITIONS`.
+- **Net swap:** the engine list filters out `platformNet.cpp`/`platformNetAsync.cpp`
+ and adds `platform/platformNet_Emscripten.cpp` (all stubs — browsers can't open raw
+ sockets). `platformNet_ScriptBinding.cc` (the script API) is shared and stays.
+- **emcc link flags:** `-sUSE_SDL=1` (SDL 1.2 input/video via emscripten's bundled
+ port), `-sLEGACY_GL_EMULATION=1` (fixed-function GL over WebGL 1.0 — works on emsdk
+ 6.0.1; this was the flagged "at-risk" flag and it's fine), `-sINITIAL_MEMORY=128MB`
+ `-sALLOW_MEMORY_GROWTH=1` `-sEXIT_RUNTIME=0` `-sFORCE_FILESYSTEM=1`,
+ `--js-library platformEmscripten/platform.js`.
+- **Assets** are packaged into MEMFS with `--preload-file SRC@/DST` (no host FS to
+ lazy-load from in a browser → a `.data` sidecar). `main.cs` + `editor/`, `library/`,
+ `toybox/` are bundled at the VFS root (engine cwd is `/`). **`tools/` is deliberately
+ NOT bundled** — it's build-time-only (TexturePacker + ~90 MB of generated doxygen
+ HTML under `tools/doxygen/output`), which cut the `.data` from 272 MB to ~186 MB.
+ (Heads-up: the iOS bundle still ships `tools/` and carries that same dead ~90 MB —
+ worth trimming there too.)
+- No gtest / no `testing/*` on Emscripten (like Android). zlib is still built from
+ source (compiles fine to wasm). Output suffix is `.html` (drives emcc to emit the
+ HTML shell); the bundle is kept in `build/emscripten`, NOT the repo root (it's a web
+ bundle, not run from cwd).
+
+### Compile/link fixes (the back-end had drifted from the engine interfaces)
+All were bit-rot in `platformEmscripten/*`, surfaced once it compiled against current
+headers:
+- **ljpeg** "No jconfig.h" — the global `EMSCRIPTEN=1` (above).
+- `EmscriptenStrings.cpp` — `dStrlen`/`dStrspn`/`dStrcspn` returned `dsize_t`, but
+ `platformString.h` (and every other platform) declares them `U32`; on wasm32
+ `dsize_t != U32` → "functions differ only in return type". Changed to `U32`.
+- `EmscriptenGL.cpp` — dropped a stale `AssertFatal(platState.engine, ...)`
+ (`EmscriptenPlatState` has no `engine` member; macOS-derived leftover).
+- `EmscriptenOutlineGL.cpp` — fixed include typo `platformEmscriptenplatformGL.h` →
+ `platformEmscripten/platformGL.h`.
+- `EmscriptenOGLVideo.{h,cpp}` — added the missing `getVerticalSync()` override
+ (`DisplayDevice` declares it pure-virtual → the class was abstract → `new
+ OpenGLDevice()` failed). Stub mirrors x86UNIX.
+- `platformGL.h` — `#include "platform/types.h"` so the `gluProject`/`gluUnProject`
+ prototypes' `F64` resolves regardless of include order.
+- `platform/platformNet_Emscripten.cpp` — the whole file was guarded
+ `#if defined(TORQUE_OS_EMSCRIPTEN)` but `#include "platformNet.h"` (which *defines*
+ that macro) was the NEXT line, INSIDE the guard → the file compiled to nothing and
+ every `Net::` symbol was undefined at link. Guard now keys on bare `EMSCRIPTEN`
+ (a build-level define available before any include). Same chicken-and-egg class as
+ the iOS `TORQUE_OS_IOS` predefine.
+- `EmscriptenOutlineGL.cpp` — `glArrayElement` (legacy immediate-mode indexed draw)
+ is undefined: emscripten's `LEGACY_GL_EMULATION` provides `glBegin/glEnd/glDrawArrays`
+ but NOT `glArrayElement`. Added an `extern "C"` no-op (this debug-only wireframe
+ overlay is off by default — same graceful-degradation choice as iOS's
+ `NO_REDEFINE_GL_FUNCS`).
+
+### Runtime fixes (browser-driven via Playwright + `python -m http.server`)
+Debugged headless by driving Chromium at the page, reading the JS console, and
+screenshotting. Boot reaches: all subsystems init → **WebGL 1.0 context up**
+(`Renderer: WebKit WebGL`, extensions, `Max Texture Size 16384`, screen mode
+1024×768) → **EditorCore module loads** → main loop runs. Fixes:
+- **Directory scan dropped the first char of each entry** — module registration
+ reported scanning `/editor/ssetAdmin` (should be `AssetAdmin`), so editor modules
+ never loaded. `EmscriptenFileio.cpp recurseDumpDirectories()` built the child
+ `subPath` WITHOUT a leading slash when `basePath` ended in `/`, but the path-assembly
+ unconditionally did `&subPath[1]` (assuming a leading slash to strip) → dropped a
+ real filename char. Fixed to join with exactly one `/`, only stripping `subPath`'s
+ leading slash when it actually has one. (Same FAMILY as the Android leading-slash
+ path off-by-one — Emscripten runs from cwd `/` too, but has its own Fileio.)
+- **`platform.js` showed pointer addresses, not messages** — `js_AlertOK` etc. did
+ `alert(message)` where `message` is the raw wasm heap POINTER (e.g. "363333"), not a
+ JS string. Decode with `UTF8ToString()`. Also routed informational `AlertOK` to
+ `console.error` instead of a blocking native `alert()`: the engine's assert handler
+ calls a Platform alert per failed assert, and a blocking dialog wedges the browser
+ tab in an un-dismissable storm (a web game must never block on `alert()`). Decision
+ dialogs (OKCancel/Retry/YesNo) keep a real `confirm()` since the engine needs the
+ boolean.
+- **`GFont::create` hard-crashed on a missing font** — `gFont.cc` did
+ `AssertFatal(platFont, ...)` then dereferenced the null `platFont` anyway
+ (`getFontHeight()`), which TRAPS the wasm runtime ("memory access out of bounds").
+ Its sole caller `GuiControlProfile::addFont()` ALREADY null-checks the return, so
+ `GFont::create` now returns a null `Resource` on a missing font instead of
+ crashing. Cross-platform robustness fix; directly analogous to the open Android
+ font-robustness bug (`AndroidFont::create` returning true on failure + unguarded
+ deref).
+
+### Fonts round — DONE: the editor renders TEXT on the web (cache-first, no FreeType)
+The web build has **no font backend** (`EmscriptenFont::createPlatformFont()` is stubbed),
+so it can't synthesize glyphs at runtime. But the engine's `.uft` files are **fully
+self-contained** (glyph bitmaps + metrics + texture sheets via `GFont::read`) and load
+with no platform font — and the editor already ships ~125 MB of them. So fonts were wired
+**cache-first** (the `.uft` already in the preload), no FreeType, no extra download. Result:
+the Project Manager UI renders with real text ("TORQUE2D", version, project-tile labels)
+plus sprites, stable, no crash. This was also the first end-to-end exercise of the GL
+**draw** path on web (text quads + sprite batches under `LEGACY_GL_EMULATION`) — it works.
+
+Fixes (most are cross-platform robustness; the asset-path ones are web-specific):
+- **Blocking assert dialogs wedged the tab.** `PlatformAssert::process` showed a native
+ `AlertRetry`/`AlertOKCancel` (→ blocking `confirm()`) for every non-Warning assert and
+ `forceShutdown(1)` on Cancel — fatal inside the rAF main loop, and a per-frame assert
+ produced an un-dismissable dialog storm. On `TORQUE_OS_EMSCRIPTEN` the assert is now
+ logged-and-continued (no modal, no shutdown) — the only sane web behavior. This is what
+ unblocked boot past the (still-present, non-fatal) `Con::init should only be called once`
+ double-init assert.
+- **Frame allocator too small.** Emscripten was grouped with Android at 512 KB, but it
+ boots the SAME desktop-class editor iOS needed 3 MB for — moved Emscripten to the 3 MB
+ branch (`defaultGame.cc`); only Android keeps the small budget.
+- **`GuiControlProfile::getFont()` hard-asserted on a missing font** (`guiTypes.cc:768`),
+ and text-render sites deref the result. Now it falls back to another loaded size in the
+ profile and returns NULL only if the profile has no usable font (cross-platform).
+- **Web font selection + cache dir.** `AppCore`/`EditorCore` `SetProfileFont` picked
+ "monaco" on web (`$platform=="x86UNIX"`, no `.uft`, no system font). Added a web branch
+ (`$platformUnixType=="emscripten"`) → "share tech mono". The base `GuiDefaultProfile`
+ also hardcoded a **non-existent** `^EditorCore/gui/fonts` dir (desktop only survived via
+ `createPlatformFont`); pointed it at an **expanded** real dir under the registered
+ `^EditorCore` expando that actually ships the face — `^EditorCore/Themes/LabCoat/fonts`
+ (the resource manager does NOT resolve the `^Module` expando for cache lookups, and
+ `^AppCore` isn't even loaded at editor boot, so the path must be pre-`expandPath`'d).
+- **`GFont::getTextureHandle(index)` did an unguarded `mTextureSheets[index]`** — an
+ out-of-range sheet index returned a garbage `TextureHandle` whose non-NULL `object` was
+ then dereferenced by `lock()` → fatal wasm "memory access out of bounds" in `dglDrawText`.
+ Bounds-checked to return a NULL handle (lock-safe), so an unrenderable glyph is skipped
+ (cross-platform robustness; gFont.h).
+
+**SCRIPT-CHANGE GOTCHA (build):** the `--preload-file` asset trees are NOT tracked as CMake
+dependencies (same as `--js-library platform.js`), so editing a `.cs`/asset does NOT trigger
+a repackage. After a script edit, force it: `rm build/emscripten/Torque2D_DEBUG.{html,js,wasm,data}`
+then rebuild. (TODO: add a CMake custom-command dependency on the preload trees, or a clean
+target, so script edits repackage automatically.)
+
+Residual after the cache-first round (since RESOLVED by the FreeType round below):
+- The un-baked editor title sizes (`black ops one` 21/28) that logged a non-fatal per-frame
+ `Vector out of bounds` now rasterize via FreeType — gone.
+- **`Con::init should only be called once`** still fires at boot (console double-init); now
+ harmless (logged-and-continued). Root cause not yet chased.
+
+### FreeType round — DONE: a real rasterizer (web behaves like the desktop)
+The cache-first round rendered only the pre-baked `.uft` sizes; anything else degraded to a
+near size or nothing (Windows' GDI is a universal backstop — the web had none). This round
+restores that backstop: **FreeType (the vendored 2.4.12) is compiled to wasm** and
+`EmscriptenFont` rasterizes a bundled **Roboto** `.ttf` for any face/size not in the `.uft`
+cache. Result: the editor renders ALL its text (the previously-blank "New Project" heading /
+body now draw), the per-frame OOB is gone, and web matches the desktop font behavior. `.uft`
+is kept (designed faces still use it at baked sizes; FreeType only fills gaps).
+
+- **FreeType static lib** (`engine/lib/CMakeLists.txt`, gated `if(EMSCRIPTEN)` — desktop uses
+ system fonts / `find_package(Freetype)`, Android the prebuilt `.a`): the vendored
+ `freetype/android/freetype-2.4.12/` tree built from its per-module **aggregator** `.c`
+ (`ftbase/ftinit/ftsystem/ftdebug`, `sfnt`, `truetype`, `smooth`, `raster`, `autofit`,
+ `psnames/psaux/pshinter`), `FT2_BUILD_LIBRARY` defined, `torque_thirdparty_lib`. Linked in
+ the root EMSCRIPTEN block (its PUBLIC include propagates ``). +~1 MB wasm.
+- **Trimmed `ftmodule.h`** — `ftinit.c` registers every driver listed there, so the default
+ full list produced undefined-symbol link errors (`bdf/pcf/t42/winfnt/type1/cff/cid/pfr`).
+ Trimmed to the TrueType path we compile (the file's whole purpose is to list built-in
+ modules). Edited in the vendored tree — safe, since that source is ONLY consumed by this
+ from-source Emscripten build (Android links the prebuilt `.a`).
+- **`EmscriptenFont`** (`platformEmscripten/EmscriptenFont.{h,cpp}`) — mirrors `AndroidFont`:
+ `FT_Init_FreeType`; `create` resolves the `.ttf` from `$pref::Web::fallbackFont`,
+ `FT_New_Face(path)` (the path is a preloaded MEMFS file — FreeType's ANSI stdio reads it),
+ `FT_Set_Pixel_Sizes`, metrics from `face->size->metrics`; `getCharInfo` does
+ `FT_Load_Char(FT_LOAD_RENDER)` and copies the 8-bit alpha bitmap. Uses the rendered
+ **bitmap** dims (`slot->bitmap.width/rows`, stride `bitmap.pitch`) for the CharInfo — keeps
+ alloc/copy/size consistent (metrics width can be a pixel narrower → would overrun).
+- **Fallback resolution + app/editor separation.** `EmscriptenFont` only gets the face NAME,
+ so the `.ttf` path comes from `$pref::Web::fallbackFont`, which each core registers to its
+ OWN copy: `library/AppCore/scripts/defaultPreferences.cs` →
+ `^AppCore/fonts/Roboto-Regular.ttf` (ships with games; self-contained), and
+ `editor/EditorCore/scripts/defaultPreferences.cs` → `^EditorCore/gui/fonts/Roboto-Regular.ttf`
+ (overrides while the editor is loaded). Removing `editor/` falls back to AppCore's — the
+ editor never reaches into the app. `Roboto-Regular.ttf` (SIL OFL) is bundled in both dirs
+ (preloaded into the web `.data`) and in `android-studio/.../assets/fonts/`.
+- **Android (wired, NOT tested this round).** `guiProfiles.cs` (both cores) request `"Roboto"`
+ instead of the long-gone `"Droid"`, and `Roboto-Regular.ttf` is in `assets/fonts/` so
+ `FontManager` resolves it → `AndroidFont` (already FreeType, failure now propagated)
+ rasterizes it. No new Android code; APK run deferred.
+
+Residual (minor): the `Con::init` double-init still logs (harmless); a single non-fatal OOB
+remains at boot (down from thousands). FOLLOW-UPS: per-face `.ttf` resolution
+(`/.ttf` before the generic fallback) for exact typography without baking `.uft`;
+then drop most `.uft` for a much smaller web download; and run the Android APK to confirm text.
+
+### Interaction round — DONE: navigate the editor, open toys, type in the console
+With text rendering, the next step was making the UI actually usable (find/open a project,
+keyboard input). Four fixes:
+- **Project selector found no projects** (only the "New Project" placeholder). `Platform::
+ dumpDirectories` started recursion at `currentDepth 0`, but the child-recursion guard is
+ `currentDepth < recurseDepth`, so the common `getDirectoryList()` (depth 0) call evaluated
+ `0 < 0` == false and descended into NO children → empty list. The editor enumerates
+ `getMainDotCsDir()` to find project folders, so the Toy Box (the only default project) never
+ listed. Fixed by starting recursion at `-1`, matching Win32/x86UNIX (the SAME fix was made
+ to x86UNIX in the Linux round but never propagated here). `EmscriptenFileio.cpp`.
+- **Typing crashed the tab (hard wasm trap).** `_StringTable::hashString/hashStringn` indexed
+ the 256-entry hash table with a signed `char`; any byte ≥ 0x80 → negative index → read
+ before the array. Harmless wrong-hash on desktop, a hard "memory access out of bounds" on
+ wasm. Reproduced by pressing Ctrl with a text field focused (the bogus high ascii got
+ inserted, then hashed on `StringTable::insert`). Cast the index to `(U8)` in both hashers
+ (`string/stringTable.cc`) — a latent CROSS-PLATFORM bug; fixes all high-bit/accented input.
+- **Phantom glyphs from modifier keys.** `EmscriptenInputManager::MapKey` assigned the raw SDL
+ keysym as each key's `ascii`, so modifiers/function/arrow/keypad keys all carried a bogus
+ non-zero ascii and got inserted as (unrenderable) characters. Desktop x86UNIX avoids this via
+ `X11_KeyToUnicode()` (returns 0 for non-character keys), but emscripten's SDL1 port has no
+ working `X11_KeyToUnicode`. Filtered the default assignment: only printable ASCII (0x20-0x7E)
+ carries a character ascii; SDL specials (≥0x100), 0x7F-0xFF, and control keys (<0x20) map to
+ 0 and stay handled by keycode (`EmscriptenInputManager.cpp`).
+- **Event-list re-entrancy OOB.** `ProcessMessages()` cached the size of the shared
+ `gPlatState.eventList` then indexed it in the loop, but handling `SDL_USEREVENT`
+ (SETVIDEOMODE) → `SetAppState` → `Input::reactivate` re-enters `ProcessMessages` and
+ clears+refills that same list → the outer loop read past the now-smaller vector (the two red
+ `vector.h:578` fatals on every toybox load). Iterate a LOCAL copy of the frame's events so
+ re-entrancy can't corrupt iteration (`EmscriptenWindow.cpp`). Root-caused with a temporary
+ `emscripten_log(EM_LOG_C_STACK)` in the assert path.
+
+### Blending / immediate-mode round — DONE: toys render correctly (e.g. PyramidToy light)
+First exercise of the toys' raw `glBegin`/`glEnd` draw path on the web (the editor uses the
+batched array path; toys reach further into legacy GL). The PyramidToy `LightObject` rendered
+as an opaque dark "umbrella" fading to BLACK instead of a soft light fading OUT.
+
+- **Root cause — GL state changed *inside* glBegin/glEnd.** `LightObject::sceneRender`
+ (`2d/sceneobject/LightObject.cc`) called `glDisable(GL_BLEND)` before `glEnd()`. On desktop
+ GL that call is illegal between glBegin/glEnd (`GL_INVALID_OPERATION`) and is silently
+ IGNORED, so the fan still draws with the additive blend it set up — fades out correctly.
+ But the web's immediate-mode shim (`EmscriptenGL2ES.cpp`, compiled by `PlatformSources.cmake`
+ — its `glBegin`/`glEnd` override emscripten's `LEGACY_GL_EMULATION` ones) only BUFFERS the
+ vertices and defers the real `glDrawArrays` to `glEnd()`. So the `glDisable(GL_BLEND)` runs
+ immediately and the deferred draw happens with blending OFF → opaque fan whose per-vertex
+ colors fade to black. Fixed by moving the disable AFTER `glEnd()`.
+- **General rule (web immediate mode):** only emit vertex/color/texcoord between glBegin/glEnd;
+ do every enable/disable/blendfunc OUTSIDE the block. A sweep of `engine/source/2d` found
+ `LightObject` was the only offender — `DebugDraw.cc` already disables blend after `glEnd()`,
+ and BatchRender/SceneWindow/SceneObject use direct vertex arrays (no deferral). Watch for
+ this when porting any other legacy-GL toy/sample to the web.
+
+## Cross-cutting notes
+
+- Single-config generators (Make/Ninja) use `-DCMAKE_BUILD_TYPE=`; multi-config
+ (VS/Xcode) use `--config`. The root handles both, incl. the `Shipping` config
+ (Release flags + `TORQUE_SHIPPING`) and per-config exe names (`Torque2D_DEBUG`).
+- `bitmapPvr.cc` (PVR/mobile) is excluded on all desktop platforms — correct.
+- Windows-only details (static `/MT`, `/Zc:wchar_t-`, `_HAS_STD_BYTE=0`) are all
+ guarded by `if(MSVC)` and won't affect mac/linux.
+- **Coordination:** the root `CMakeLists.txt`, `PlatformSources.cmake`, and
+ `engine/lib/CMakeLists.txt` are shared. Do macOS and Linux on **separate
+ branches off `cmake-do-over`** (or one at a time) to avoid merge conflicts.
diff --git a/cmake/EngineSources.cmake b/cmake/EngineSources.cmake
new file mode 100644
index 000000000..d011fe1f9
--- /dev/null
+++ b/cmake/EngineSources.cmake
@@ -0,0 +1,443 @@
+# -----------------------------------------------------------------------------
+# EngineSources.cmake
+#
+# Authoritative, explicit list of the cross-platform Torque2D engine translation
+# units (the build's source of truth). When you add or remove an engine source
+# file, edit THIS list, then re-generate the per-platform project files.
+#
+# Platform-specific sources live in PlatformSources.cmake.
+# This list was bootstrapped from the on-disk source tree and reconciled against
+# the VS2022 project; maintain it by hand going forward.
+# -----------------------------------------------------------------------------
+
+set(TORQUE_ENGINE_SOURCES
+ # ---- 2d/assets ----
+ ${TORQUE_SRC}/2d/assets/AnimationAsset.cc
+ ${TORQUE_SRC}/2d/assets/FontAsset.cc
+ ${TORQUE_SRC}/2d/assets/ImageAsset.cc
+ ${TORQUE_SRC}/2d/assets/ParticleAsset.cc
+ ${TORQUE_SRC}/2d/assets/ParticleAssetEmitter.cc
+ ${TORQUE_SRC}/2d/assets/ParticleAssetField.cc
+ ${TORQUE_SRC}/2d/assets/ParticleAssetFieldCollection.cc
+ # ---- 2d/controllers ----
+ ${TORQUE_SRC}/2d/controllers/AmbientForceController.cc
+ ${TORQUE_SRC}/2d/controllers/BuoyancyController.cc
+ ${TORQUE_SRC}/2d/controllers/PointForceController.cc
+ # ---- 2d/controllers/core ----
+ ${TORQUE_SRC}/2d/controllers/core/GroupedSceneController.cc
+ ${TORQUE_SRC}/2d/controllers/core/PickingSceneController.cc
+ # ---- 2d/core ----
+ ${TORQUE_SRC}/2d/core/BatchRender.cc
+ ${TORQUE_SRC}/2d/core/CoreMath.cc
+ ${TORQUE_SRC}/2d/core/ImageFrameProvider.cc
+ ${TORQUE_SRC}/2d/core/ImageFrameProviderCore.cc
+ ${TORQUE_SRC}/2d/core/ParticleSystem.cc
+ ${TORQUE_SRC}/2d/core/RenderProxy.cc
+ ${TORQUE_SRC}/2d/core/SpriteBase.cc
+ ${TORQUE_SRC}/2d/core/SpriteBatch.cc
+ ${TORQUE_SRC}/2d/core/SpriteBatchItem.cc
+ ${TORQUE_SRC}/2d/core/SpriteBatchQuery.cc
+ ${TORQUE_SRC}/2d/core/Utility.cc
+ ${TORQUE_SRC}/2d/core/Vector2.cc
+ # ---- 2d/editorToy ----
+ ${TORQUE_SRC}/2d/editorToy/EditorToySceneWindow.cc
+ ${TORQUE_SRC}/2d/editorToy/EditorToyTool.cc
+ # ---- 2d/experimental/composites ----
+ ${TORQUE_SRC}/2d/experimental/composites/WaveComposite.cc
+ # ---- 2d/gui ----
+ ${TORQUE_SRC}/2d/gui/SceneWindow.cc
+ ${TORQUE_SRC}/2d/gui/guiSceneObjectCtrl.cc
+ ${TORQUE_SRC}/2d/gui/guiSpriteCtrl.cc
+ # ---- 2d/scene ----
+ ${TORQUE_SRC}/2d/scene/ContactFilter.cc
+ ${TORQUE_SRC}/2d/scene/DebugDraw.cc
+ ${TORQUE_SRC}/2d/scene/Scene.cc
+ ${TORQUE_SRC}/2d/scene/SceneRenderFactories.cpp
+ ${TORQUE_SRC}/2d/scene/SceneRenderQueue.cpp
+ ${TORQUE_SRC}/2d/scene/WorldQuery.cc
+ # ---- 2d/sceneobject ----
+ ${TORQUE_SRC}/2d/sceneobject/CompositeSprite.cc
+ ${TORQUE_SRC}/2d/sceneobject/LightObject.cc
+ ${TORQUE_SRC}/2d/sceneobject/ParticlePlayer.cc
+ ${TORQUE_SRC}/2d/sceneobject/Path.cc
+ ${TORQUE_SRC}/2d/sceneobject/SceneObject.cc
+ ${TORQUE_SRC}/2d/sceneobject/SceneObjectList.cc
+ ${TORQUE_SRC}/2d/sceneobject/SceneObjectSet.cc
+ ${TORQUE_SRC}/2d/sceneobject/Scroller.cc
+ ${TORQUE_SRC}/2d/sceneobject/ShapeVector.cc
+ ${TORQUE_SRC}/2d/sceneobject/Sprite.cc
+ ${TORQUE_SRC}/2d/sceneobject/TextSprite.cc
+ ${TORQUE_SRC}/2d/sceneobject/Trigger.cc
+ # ---- algorithm ----
+ ${TORQUE_SRC}/algorithm/Perlin.cc
+ ${TORQUE_SRC}/algorithm/crc.cc
+ ${TORQUE_SRC}/algorithm/hashFunction.cc
+ ${TORQUE_SRC}/algorithm/pcg_basic.c
+ # ---- assets ----
+ ${TORQUE_SRC}/assets/assetBase.cc
+ ${TORQUE_SRC}/assets/assetFieldTypes.cc
+ ${TORQUE_SRC}/assets/assetManager.cc
+ ${TORQUE_SRC}/assets/assetQuery.cc
+ ${TORQUE_SRC}/assets/assetTagsManifest.cc
+ ${TORQUE_SRC}/assets/declaredAssets.cc
+ ${TORQUE_SRC}/assets/referencedAssets.cc
+ # ---- audio ----
+ ${TORQUE_SRC}/audio/AudioAsset.cc
+ ${TORQUE_SRC}/audio/audio.cc
+ ${TORQUE_SRC}/audio/audioBuffer.cc
+ ${TORQUE_SRC}/audio/audioDataBlock.cc
+ ${TORQUE_SRC}/audio/audioDescriptions.cc
+ ${TORQUE_SRC}/audio/audioStreamSourceFactory.cc
+ ${TORQUE_SRC}/audio/audio_ScriptBinding.cc
+ ${TORQUE_SRC}/audio/vorbisStreamSource.cc
+ ${TORQUE_SRC}/audio/wavStreamSource.cc
+ # ---- bitmapFont ----
+ ${TORQUE_SRC}/bitmapFont/BitmapFont.cc
+ ${TORQUE_SRC}/bitmapFont/BitmapFontCharacter.cc
+ # ---- collection ----
+ ${TORQUE_SRC}/collection/bitTables.cc
+ ${TORQUE_SRC}/collection/hashTable.cc
+ ${TORQUE_SRC}/collection/nameTags.cpp
+ ${TORQUE_SRC}/collection/undo.cc
+ ${TORQUE_SRC}/collection/vector.cc
+ # ---- component ----
+ ${TORQUE_SRC}/component/dynamicConsoleMethodComponent.cpp
+ ${TORQUE_SRC}/component/simComponent.cpp
+ # ---- component/behaviors ----
+ ${TORQUE_SRC}/component/behaviors/behaviorComponent.cpp
+ ${TORQUE_SRC}/component/behaviors/behaviorInstance.cpp
+ ${TORQUE_SRC}/component/behaviors/behaviorTemplate.cpp
+ # ---- console ----
+ ${TORQUE_SRC}/console/CMDscan.cc
+ ${TORQUE_SRC}/console/ConsoleTypeValidators.cc
+ ${TORQUE_SRC}/console/Package.cc
+ ${TORQUE_SRC}/console/arrayObject.cpp
+ ${TORQUE_SRC}/console/astAlloc.cc
+ ${TORQUE_SRC}/console/astNodes.cc
+ ${TORQUE_SRC}/console/cmdgram.cc
+ ${TORQUE_SRC}/console/codeBlock.cc
+ ${TORQUE_SRC}/console/compiledEval.cc
+ ${TORQUE_SRC}/console/compiler.cc
+ ${TORQUE_SRC}/console/console.cc
+ ${TORQUE_SRC}/console/consoleBaseType.cc
+ ${TORQUE_SRC}/console/consoleDictionary.cc
+ ${TORQUE_SRC}/console/consoleDoc.cc
+ ${TORQUE_SRC}/console/consoleExprEvalState.cc
+ ${TORQUE_SRC}/console/consoleFunctions.cc
+ ${TORQUE_SRC}/console/consoleLogger.cc
+ ${TORQUE_SRC}/console/consoleNamespace.cc
+ ${TORQUE_SRC}/console/consoleObject.cc
+ ${TORQUE_SRC}/console/consoleParser.cc
+ ${TORQUE_SRC}/console/consoleTypes.cc
+ ${TORQUE_SRC}/console/metaScripting_ScriptBinding.cc
+ # ---- debug ----
+ ${TORQUE_SRC}/debug/profiler.cc
+ ${TORQUE_SRC}/debug/telnetDebugger.cc
+ # ---- debug/remote ----
+ ${TORQUE_SRC}/debug/remote/RemoteDebugger1.cc
+ ${TORQUE_SRC}/debug/remote/RemoteDebuggerBase.cc
+ ${TORQUE_SRC}/debug/remote/RemoteDebuggerBridge.cc
+ # ---- delegates ----
+ ${TORQUE_SRC}/delegates/delegateSignal.cpp
+ # ---- game ----
+ ${TORQUE_SRC}/game/defaultGame.cc
+ ${TORQUE_SRC}/game/gameConnection.cc
+ ${TORQUE_SRC}/game/gameInterface.cc
+ ${TORQUE_SRC}/game/version.cc
+ # ---- graphics ----
+ ${TORQUE_SRC}/graphics/DynamicTexture.cc
+ ${TORQUE_SRC}/graphics/PNGImage.cpp
+ ${TORQUE_SRC}/graphics/TextureDictionary.cc
+ ${TORQUE_SRC}/graphics/TextureHandle.cc
+ ${TORQUE_SRC}/graphics/TextureManager.cc
+ ${TORQUE_SRC}/graphics/bitmapBmp.cc
+ ${TORQUE_SRC}/graphics/bitmapJpeg.cc
+ ${TORQUE_SRC}/graphics/bitmapPng.cc
+ # excluded (mobile/other-platform): graphics/bitmapPvr.cc
+ ${TORQUE_SRC}/graphics/dgl.cc
+ ${TORQUE_SRC}/graphics/dglMatrix.cc
+ ${TORQUE_SRC}/graphics/gBitmap.cc
+ ${TORQUE_SRC}/graphics/gColor.cc
+ ${TORQUE_SRC}/graphics/gFont.cc
+ ${TORQUE_SRC}/graphics/gPalette.cc
+ ${TORQUE_SRC}/graphics/splineUtil.cc
+ # ---- gui ----
+ ${TORQUE_SRC}/gui/guiArrayCtrl.cc
+ ${TORQUE_SRC}/gui/guiCanvas.cc
+ ${TORQUE_SRC}/gui/guiColorPickerCtrl.cc
+ ${TORQUE_SRC}/gui/guiColorPopupCtrl.cc
+ ${TORQUE_SRC}/gui/guiConsole.cc
+ ${TORQUE_SRC}/gui/guiConsoleEditCtrl.cc
+ ${TORQUE_SRC}/gui/guiControl.cc
+ ${TORQUE_SRC}/gui/guiDefaultControlRender.cc
+ ${TORQUE_SRC}/gui/guiInputCtrl.cc
+ ${TORQUE_SRC}/gui/guiListBoxCtrl.cc
+ ${TORQUE_SRC}/gui/guiMessageVectorCtrl.cc
+ ${TORQUE_SRC}/gui/guiProfileTheme.cc
+ ${TORQUE_SRC}/gui/guiProgressCtrl.cc
+ ${TORQUE_SRC}/gui/guiSliderCtrl.cc
+ ${TORQUE_SRC}/gui/guiTextEditCtrl.cc
+ ${TORQUE_SRC}/gui/guiTextEditSliderCtrl.cc
+ ${TORQUE_SRC}/gui/guiTreeViewCtrl.cc
+ ${TORQUE_SRC}/gui/guiTypes.cc
+ ${TORQUE_SRC}/gui/messageVector.cc
+ # ---- gui/buttons ----
+ ${TORQUE_SRC}/gui/buttons/guiButtonCtrl.cc
+ ${TORQUE_SRC}/gui/buttons/guiCheckBoxCtrl.cc
+ ${TORQUE_SRC}/gui/buttons/guiDropDownCtrl.cc
+ ${TORQUE_SRC}/gui/buttons/guiRadioCtrl.cc
+ # ---- gui/containers ----
+ ${TORQUE_SRC}/gui/containers/guiChainCtrl.cc
+ ${TORQUE_SRC}/gui/containers/guiDragAndDropCtrl.cc
+ ${TORQUE_SRC}/gui/containers/guiExpandCtrl.cc
+ ${TORQUE_SRC}/gui/containers/guiFrameSetCtrl.cc
+ ${TORQUE_SRC}/gui/containers/guiGridCtrl.cc
+ ${TORQUE_SRC}/gui/containers/guiPanelCtrl.cc
+ ${TORQUE_SRC}/gui/containers/guiSceneScrollCtrl.cc
+ ${TORQUE_SRC}/gui/containers/guiScrollCtrl.cc
+ ${TORQUE_SRC}/gui/containers/guiTabBookCtrl.cc
+ ${TORQUE_SRC}/gui/containers/guiTabPageCtrl.cc
+ ${TORQUE_SRC}/gui/containers/guiWindowCtrl.cc
+ # ---- gui/editor ----
+ ${TORQUE_SRC}/gui/editor/guiEditorCursorCtrl.cc
+ ${TORQUE_SRC}/gui/editor/guiDebugger.cc
+ ${TORQUE_SRC}/gui/editor/guiEditCtrl.cc
+ ${TORQUE_SRC}/gui/editor/guiEditFramePaletteCtrl.cc
+ ${TORQUE_SRC}/gui/editor/guiEditFrameStripCtrl.cc
+ ${TORQUE_SRC}/gui/editor/guiEditFrameTimelineCtrl.cc
+ ${TORQUE_SRC}/gui/editor/guiEditParticleColorGraph.cc
+ ${TORQUE_SRC}/gui/editor/guiEditorExplorerTree.cc
+ ${TORQUE_SRC}/gui/editor/guiGraphCtrl.cc
+ ${TORQUE_SRC}/gui/editor/guiInspector.cc
+ ${TORQUE_SRC}/gui/editor/guiInspectorTypes.cc
+ ${TORQUE_SRC}/gui/editor/guiMenuBarCtrl.cc
+ ${TORQUE_SRC}/gui/editor/guiParticleGraphInspector.cc
+ # ---- gui/language ----
+ ${TORQUE_SRC}/gui/language/lang.cc
+ # ---- input ----
+ ${TORQUE_SRC}/input/actionMap.cc
+ # ---- io ----
+ ${TORQUE_SRC}/io/bitStream.cc
+ ${TORQUE_SRC}/io/bufferStream.cc
+ ${TORQUE_SRC}/io/byteBuffer.cpp
+ ${TORQUE_SRC}/io/fileObject.cc
+ ${TORQUE_SRC}/io/fileStream.cc
+ ${TORQUE_SRC}/io/fileStreamObject.cc
+ ${TORQUE_SRC}/io/fileSystem_ScriptBinding.cc
+ ${TORQUE_SRC}/io/filterStream.cc
+ ${TORQUE_SRC}/io/memStream.cc
+ ${TORQUE_SRC}/io/nStream.cc
+ ${TORQUE_SRC}/io/resizeStream.cc
+ ${TORQUE_SRC}/io/streamObject.cc
+ # ---- io/resource ----
+ ${TORQUE_SRC}/io/resource/resourceDictionary.cc
+ ${TORQUE_SRC}/io/resource/resourceManager.cc
+ # ---- io/zip ----
+ ${TORQUE_SRC}/io/zip/centralDir.cc
+ ${TORQUE_SRC}/io/zip/compressor.cc
+ ${TORQUE_SRC}/io/zip/deflate.cc
+ ${TORQUE_SRC}/io/zip/extraField.cc
+ ${TORQUE_SRC}/io/zip/fileHeader.cc
+ ${TORQUE_SRC}/io/zip/stored.cc
+ ${TORQUE_SRC}/io/zip/zipArchive.cc
+ ${TORQUE_SRC}/io/zip/zipCryptStream.cc
+ ${TORQUE_SRC}/io/zip/zipObject.cc
+ ${TORQUE_SRC}/io/zip/zipSubStream.cc
+ ${TORQUE_SRC}/io/zip/zipTempStream.cc
+ # ---- math ----
+ ${TORQUE_SRC}/math/mBox.cc
+ ${TORQUE_SRC}/math/mFluid.cpp
+ ${TORQUE_SRC}/math/mMathAMD.cc
+ ${TORQUE_SRC}/math/mMathAltivec.cc
+ ${TORQUE_SRC}/math/mMathFn.cc
+ ${TORQUE_SRC}/math/mMathSSE.cc
+ ${TORQUE_SRC}/math/mMath_C.cc
+ ${TORQUE_SRC}/math/mMatrix.cc
+ ${TORQUE_SRC}/math/mPlaneTransformer.cc
+ ${TORQUE_SRC}/math/mPoint.cpp
+ ${TORQUE_SRC}/math/mQuadPatch.cc
+ ${TORQUE_SRC}/math/mQuat.cc
+ ${TORQUE_SRC}/math/mRandom.cc
+ ${TORQUE_SRC}/math/mSolver.cc
+ ${TORQUE_SRC}/math/mSplinePatch.cc
+ ${TORQUE_SRC}/math/mathTypes.cc
+ ${TORQUE_SRC}/math/mathUtils.cc
+ ${TORQUE_SRC}/math/math_ScriptBinding.cc
+ ${TORQUE_SRC}/math/rectClipper.cpp
+ # ---- math/noise ----
+ ${TORQUE_SRC}/math/noise/NoiseGenerator.cc
+ ${TORQUE_SRC}/math/noise/RandomNumberGenerator.cc
+ # ---- memory ----
+ ${TORQUE_SRC}/memory/dataChunker.cc
+ ${TORQUE_SRC}/memory/frameAllocator_ScriptBinding.cc
+ # ---- messaging ----
+ ${TORQUE_SRC}/messaging/dispatcher.cc
+ ${TORQUE_SRC}/messaging/eventManager.cc
+ ${TORQUE_SRC}/messaging/message.cc
+ ${TORQUE_SRC}/messaging/messageForwarder.cc
+ ${TORQUE_SRC}/messaging/scriptMsgListener.cc
+ # ---- module ----
+ ${TORQUE_SRC}/module/moduleDefinition.cc
+ ${TORQUE_SRC}/module/moduleManager.cc
+ ${TORQUE_SRC}/module/moduleMergeDefinition.cc
+ # ---- network ----
+ ${TORQUE_SRC}/network/RemoteCommandEvent.cc
+ ${TORQUE_SRC}/network/connectionProtocol.cc
+ ${TORQUE_SRC}/network/connectionStringTable.cc
+ ${TORQUE_SRC}/network/httpObject.cc
+ ${TORQUE_SRC}/network/netConnection.cc
+ ${TORQUE_SRC}/network/netDownload.cc
+ ${TORQUE_SRC}/network/netEvent.cc
+ ${TORQUE_SRC}/network/netGhost.cc
+ ${TORQUE_SRC}/network/netInterface.cc
+ ${TORQUE_SRC}/network/netObject.cc
+ ${TORQUE_SRC}/network/netStringTable.cc
+ ${TORQUE_SRC}/network/netTest.cc
+ ${TORQUE_SRC}/network/networkProcessList.cc
+ ${TORQUE_SRC}/network/serverQuery.cc
+ ${TORQUE_SRC}/network/tcpObject.cc
+ ${TORQUE_SRC}/network/telnetConsole.cc
+ # ---- persistence ----
+ ${TORQUE_SRC}/persistence/SimXMLDocument.cpp
+ # ---- persistence/taml ----
+ ${TORQUE_SRC}/persistence/taml/taml.cc
+ ${TORQUE_SRC}/persistence/taml/tamlCustom.cc
+ ${TORQUE_SRC}/persistence/taml/tamlWriteNode.cc
+ # ---- persistence/taml/binary ----
+ ${TORQUE_SRC}/persistence/taml/binary/tamlBinaryReader.cc
+ ${TORQUE_SRC}/persistence/taml/binary/tamlBinaryWriter.cc
+ # ---- persistence/taml/json ----
+ ${TORQUE_SRC}/persistence/taml/json/tamlJSONParser.cc
+ ${TORQUE_SRC}/persistence/taml/json/tamlJSONReader.cc
+ ${TORQUE_SRC}/persistence/taml/json/tamlJSONWriter.cc
+ # ---- persistence/taml/xml ----
+ ${TORQUE_SRC}/persistence/taml/xml/tamlXmlParser.cc
+ ${TORQUE_SRC}/persistence/taml/xml/tamlXmlReader.cc
+ ${TORQUE_SRC}/persistence/taml/xml/tamlXmlWriter.cc
+ # ---- persistence/tinyXML ----
+ ${TORQUE_SRC}/persistence/tinyXML/tinystr.cpp
+ ${TORQUE_SRC}/persistence/tinyXML/tinyxml.cpp
+ ${TORQUE_SRC}/persistence/tinyXML/tinyxmlerror.cpp
+ ${TORQUE_SRC}/persistence/tinyXML/tinyxmlparser.cpp
+ # ---- sim ----
+ ${TORQUE_SRC}/sim/SimObjectList.cc
+ ${TORQUE_SRC}/sim/scriptGroup.cc
+ ${TORQUE_SRC}/sim/scriptObject.cc
+ ${TORQUE_SRC}/sim/simBase.cc
+ ${TORQUE_SRC}/sim/simConsoleEvent.cc
+ ${TORQUE_SRC}/sim/simConsoleThreadExecEvent.cc
+ ${TORQUE_SRC}/sim/simDatablock.cc
+ ${TORQUE_SRC}/sim/simDictionary.cc
+ ${TORQUE_SRC}/sim/simFieldDictionary.cc
+ ${TORQUE_SRC}/sim/simManager.cc
+ ${TORQUE_SRC}/sim/simObject.cc
+ ${TORQUE_SRC}/sim/simSerialize.cpp
+ ${TORQUE_SRC}/sim/simSet.cc
+ # ---- string ----
+ ${TORQUE_SRC}/string/findMatch.cc
+ ${TORQUE_SRC}/string/stringBuffer.cc
+ ${TORQUE_SRC}/string/stringStack.cc
+ ${TORQUE_SRC}/string/stringTable.cc
+ ${TORQUE_SRC}/string/stringUnit.cpp
+ ${TORQUE_SRC}/string/unicode.cc
+ # ---- testing ----
+ ${TORQUE_SRC}/testing/unitTesting.cc
+ # ---- testing/tests ----
+ ${TORQUE_SRC}/testing/tests/animationFrameConversionTests.cc
+ ${TORQUE_SRC}/testing/tests/assetStateCopyTests.cc
+ ${TORQUE_SRC}/testing/tests/bitmapFontParseTests.cc
+ ${TORQUE_SRC}/testing/tests/declaredPathCaseTests.cc
+ ${TORQUE_SRC}/testing/tests/directoryScanCaseTests.cc
+ ${TORQUE_SRC}/testing/tests/guiControlReparentTests.cc
+ ${TORQUE_SRC}/testing/tests/guiCursorHotSpotTests.cc
+ ${TORQUE_SRC}/testing/tests/guiFrameStripLayoutTests.cc
+ ${TORQUE_SRC}/testing/tests/guiHitTestTests.cc
+ ${TORQUE_SRC}/testing/tests/guiParticleColorGraphTests.cc
+ ${TORQUE_SRC}/testing/tests/guiProfileThemeTests.cc
+ ${TORQUE_SRC}/testing/tests/guiScrollLayoutTests.cc
+ ${TORQUE_SRC}/testing/tests/guiTextEditTests.cc
+ ${TORQUE_SRC}/testing/tests/guiTextWrapTests.cc
+ ${TORQUE_SRC}/testing/tests/guiTreeRowLayoutTests.cc
+ ${TORQUE_SRC}/testing/tests/imageAssetCellNameTests.cc
+ ${TORQUE_SRC}/testing/tests/namespaceLinkTests.cc
+ ${TORQUE_SRC}/testing/tests/platformFileIoTests.cc
+ ${TORQUE_SRC}/testing/tests/platformMemoryTests.cc
+ ${TORQUE_SRC}/testing/tests/platformStringTests.cc
+ ${TORQUE_SRC}/testing/tests/simObjectCloneTests.cc
+ ${TORQUE_SRC}/testing/tests/stringTableCaseTests.cc
+ # ---- platform ----
+ ${TORQUE_SRC}/platform/CursorManager.cc
+ ${TORQUE_SRC}/platform/Tickable.cc
+ ${TORQUE_SRC}/platform/platform.cc
+ ${TORQUE_SRC}/platform/platformAssert.cc
+ ${TORQUE_SRC}/platform/platformCPU.cc
+ ${TORQUE_SRC}/platform/platformFileIO.cc
+ ${TORQUE_SRC}/platform/platformFont.cc
+ ${TORQUE_SRC}/platform/platformMemory.cc
+ ${TORQUE_SRC}/platform/platformNet.cpp
+ ${TORQUE_SRC}/platform/platformNetAsync.cpp
+ # excluded (mobile/other-platform): platform/platformNet_Emscripten.cpp
+ ${TORQUE_SRC}/platform/platformNet_ScriptBinding.cc
+ ${TORQUE_SRC}/platform/platformString.cc
+ ${TORQUE_SRC}/platform/platformVideo.cc
+ # ---- platform/menus ----
+ ${TORQUE_SRC}/platform/menus/popupMenu.cc
+ # ---- platform/nativeDialogs ----
+ ${TORQUE_SRC}/platform/nativeDialogs/fileDialog.cc
+ ${TORQUE_SRC}/platform/nativeDialogs/msgBox.cpp
+ # ---- Box2D (third-party physics, full tree) ----
+ ${TORQUE_SRC}/Box2D/Collision/Shapes/b2ChainShape.cpp
+ ${TORQUE_SRC}/Box2D/Collision/Shapes/b2CircleShape.cpp
+ ${TORQUE_SRC}/Box2D/Collision/Shapes/b2EdgeShape.cpp
+ ${TORQUE_SRC}/Box2D/Collision/Shapes/b2PolygonShape.cpp
+ ${TORQUE_SRC}/Box2D/Collision/b2BroadPhase.cpp
+ ${TORQUE_SRC}/Box2D/Collision/b2CollideCircle.cpp
+ ${TORQUE_SRC}/Box2D/Collision/b2CollideEdge.cpp
+ ${TORQUE_SRC}/Box2D/Collision/b2CollidePolygon.cpp
+ ${TORQUE_SRC}/Box2D/Collision/b2Collision.cpp
+ ${TORQUE_SRC}/Box2D/Collision/b2Distance.cpp
+ ${TORQUE_SRC}/Box2D/Collision/b2DynamicTree.cpp
+ ${TORQUE_SRC}/Box2D/Collision/b2TimeOfImpact.cpp
+ ${TORQUE_SRC}/Box2D/Common/b2BlockAllocator.cpp
+ ${TORQUE_SRC}/Box2D/Common/b2Draw.cpp
+ ${TORQUE_SRC}/Box2D/Common/b2FreeList.cpp
+ ${TORQUE_SRC}/Box2D/Common/b2Math.cpp
+ ${TORQUE_SRC}/Box2D/Common/b2Settings.cpp
+ ${TORQUE_SRC}/Box2D/Common/b2StackAllocator.cpp
+ ${TORQUE_SRC}/Box2D/Common/b2Stat.cpp
+ ${TORQUE_SRC}/Box2D/Common/b2Timer.cpp
+ ${TORQUE_SRC}/Box2D/Common/b2TrackedBlock.cpp
+ ${TORQUE_SRC}/Box2D/Dynamics/Contacts/b2ChainAndCircleContact.cpp
+ ${TORQUE_SRC}/Box2D/Dynamics/Contacts/b2ChainAndPolygonContact.cpp
+ ${TORQUE_SRC}/Box2D/Dynamics/Contacts/b2CircleContact.cpp
+ ${TORQUE_SRC}/Box2D/Dynamics/Contacts/b2Contact.cpp
+ ${TORQUE_SRC}/Box2D/Dynamics/Contacts/b2ContactSolver.cpp
+ ${TORQUE_SRC}/Box2D/Dynamics/Contacts/b2EdgeAndCircleContact.cpp
+ ${TORQUE_SRC}/Box2D/Dynamics/Contacts/b2EdgeAndPolygonContact.cpp
+ ${TORQUE_SRC}/Box2D/Dynamics/Contacts/b2PolygonAndCircleContact.cpp
+ ${TORQUE_SRC}/Box2D/Dynamics/Contacts/b2PolygonContact.cpp
+ ${TORQUE_SRC}/Box2D/Dynamics/Joints/b2DistanceJoint.cpp
+ ${TORQUE_SRC}/Box2D/Dynamics/Joints/b2FrictionJoint.cpp
+ ${TORQUE_SRC}/Box2D/Dynamics/Joints/b2GearJoint.cpp
+ ${TORQUE_SRC}/Box2D/Dynamics/Joints/b2Joint.cpp
+ ${TORQUE_SRC}/Box2D/Dynamics/Joints/b2MotorJoint.cpp
+ ${TORQUE_SRC}/Box2D/Dynamics/Joints/b2MouseJoint.cpp
+ ${TORQUE_SRC}/Box2D/Dynamics/Joints/b2PrismaticJoint.cpp
+ ${TORQUE_SRC}/Box2D/Dynamics/Joints/b2PulleyJoint.cpp
+ ${TORQUE_SRC}/Box2D/Dynamics/Joints/b2RevoluteJoint.cpp
+ ${TORQUE_SRC}/Box2D/Dynamics/Joints/b2RopeJoint.cpp
+ ${TORQUE_SRC}/Box2D/Dynamics/Joints/b2WeldJoint.cpp
+ ${TORQUE_SRC}/Box2D/Dynamics/Joints/b2WheelJoint.cpp
+ ${TORQUE_SRC}/Box2D/Dynamics/b2Body.cpp
+ ${TORQUE_SRC}/Box2D/Dynamics/b2ContactManager.cpp
+ ${TORQUE_SRC}/Box2D/Dynamics/b2Fixture.cpp
+ ${TORQUE_SRC}/Box2D/Dynamics/b2Island.cpp
+ ${TORQUE_SRC}/Box2D/Dynamics/b2World.cpp
+ ${TORQUE_SRC}/Box2D/Dynamics/b2WorldCallbacks.cpp
+ ${TORQUE_SRC}/Box2D/Particle/b2Particle.cpp
+ ${TORQUE_SRC}/Box2D/Particle/b2ParticleAssembly.cpp
+ ${TORQUE_SRC}/Box2D/Particle/b2ParticleGroup.cpp
+ ${TORQUE_SRC}/Box2D/Particle/b2ParticleSystem.cpp
+ ${TORQUE_SRC}/Box2D/Particle/b2VoronoiDiagram.cpp
+ ${TORQUE_SRC}/Box2D/Rope/b2Rope.cpp
+)
diff --git a/cmake/PlatformSources.cmake b/cmake/PlatformSources.cmake
new file mode 100644
index 000000000..41328bb1a
--- /dev/null
+++ b/cmake/PlatformSources.cmake
@@ -0,0 +1,245 @@
+# -----------------------------------------------------------------------------
+# PlatformSources.cmake
+#
+# Per-platform translation units. The root CMakeLists selects the active
+# platform's list and adds it to the Torque2D target. Windows, macOS, Linux, iOS,
+# Android and Emscripten (Web/WASM) are all populated.
+#
+# The cross-platform engine sources live in EngineSources.cmake. The generic
+# `platform/` abstraction (compiled on every platform) is part of that list;
+# only the OS-specific back-ends live here.
+# -----------------------------------------------------------------------------
+
+# === Windows (platformWin32) =================================================
+set(TORQUE_PLATFORM_SOURCES_WINDOWS
+ # ---- platformWin32 ----
+ ${TORQUE_SRC}/platformWin32/cardProfile.cpp
+ ${TORQUE_SRC}/platformWin32/winAsmBlit.cc
+ ${TORQUE_SRC}/platformWin32/winCPUInfo.cc
+ ${TORQUE_SRC}/platformWin32/winConsole.cc
+ ${TORQUE_SRC}/platformWin32/winDInputDevice.cc
+ ${TORQUE_SRC}/platformWin32/winDirectInput.cc
+ ${TORQUE_SRC}/platformWin32/winExec.cc
+ ${TORQUE_SRC}/platformWin32/winFileio.cc
+ ${TORQUE_SRC}/platformWin32/winFont.cc
+ ${TORQUE_SRC}/platformWin32/winGL.cc
+ ${TORQUE_SRC}/platformWin32/winGLSpecial.cc
+ ${TORQUE_SRC}/platformWin32/winInput.cc
+ ${TORQUE_SRC}/platformWin32/winMath.cc
+ ${TORQUE_SRC}/platformWin32/winMath_ASM.cc
+ ${TORQUE_SRC}/platformWin32/winMemory.cc
+ ${TORQUE_SRC}/platformWin32/winOGLVideo.cc
+ ${TORQUE_SRC}/platformWin32/winOpenAL.cc
+ ${TORQUE_SRC}/platformWin32/winProcessControl.cc
+ ${TORQUE_SRC}/platformWin32/winSemaphore.cc
+ ${TORQUE_SRC}/platformWin32/winStrings.cc
+ ${TORQUE_SRC}/platformWin32/winTLS.cc
+ ${TORQUE_SRC}/platformWin32/winTime.cc
+ ${TORQUE_SRC}/platformWin32/winUser.cc
+ ${TORQUE_SRC}/platformWin32/winVFS.cc
+ ${TORQUE_SRC}/platformWin32/winVideo.cc
+ ${TORQUE_SRC}/platformWin32/winWindow.cc
+ # ---- platformWin32/menus ----
+ ${TORQUE_SRC}/platformWin32/menus/popupMenuWin32.cc
+ # ---- platformWin32/nativeDialogs ----
+ ${TORQUE_SRC}/platformWin32/nativeDialogs/win32DirectoryResolver.cpp
+ ${TORQUE_SRC}/platformWin32/nativeDialogs/win32FileDialog.cc
+ ${TORQUE_SRC}/platformWin32/nativeDialogs/win32MsgBox.cpp
+ # ---- platformWin32/threads ----
+ ${TORQUE_SRC}/platformWin32/threads/mutex.cc
+ ${TORQUE_SRC}/platformWin32/threads/thread.cc
+)
+
+# === macOS (platformOSX) =====================================================
+# Objective-C++ (.mm) back-end. Builds & links on Apple Silicon (arm64) with both
+# the Makefiles and Xcode generators; the root CMakeLists force-includes
+# tools/CMake/macOS-Prefix.h (Cocoa) and pins
+# CMAKE_OSX_ARCHITECTURES=arm64. The exe builds as a plain binary that runs from
+# the repo root (so it finds main.cs) — no MACOSX_BUNDLE on desktop.
+set(TORQUE_PLATFORM_SOURCES_MACOS
+ # ---- platformOSX ----
+ ${TORQUE_SRC}/platformOSX/AppDelegate.mm
+ ${TORQUE_SRC}/platformOSX/main.mm
+ ${TORQUE_SRC}/platformOSX/osxAudio.mm
+ ${TORQUE_SRC}/platformOSX/osxCPU.mm
+ ${TORQUE_SRC}/platformOSX/osxCocoaUtilities.mm
+ ${TORQUE_SRC}/platformOSX/osxEvents.mm
+ ${TORQUE_SRC}/platformOSX/osxFileDialogs.mm
+ ${TORQUE_SRC}/platformOSX/osxFileIO.mm
+ ${TORQUE_SRC}/platformOSX/osxFont.mm
+ ${TORQUE_SRC}/platformOSX/osxGL.mm
+ ${TORQUE_SRC}/platformOSX/osxInput.mm
+ ${TORQUE_SRC}/platformOSX/osxInputManager.mm
+ ${TORQUE_SRC}/platformOSX/osxMath.mm
+ ${TORQUE_SRC}/platformOSX/osxMemory.mm
+ ${TORQUE_SRC}/platformOSX/osxMutex.mm
+ ${TORQUE_SRC}/platformOSX/osxOpenGLDevice.mm
+ ${TORQUE_SRC}/platformOSX/osxOutlineGL.cc
+ ${TORQUE_SRC}/platformOSX/osxPopupMenu.mm
+ ${TORQUE_SRC}/platformOSX/osxSemaphore.mm
+ ${TORQUE_SRC}/platformOSX/osxString.mm
+ ${TORQUE_SRC}/platformOSX/osxThread.mm
+ ${TORQUE_SRC}/platformOSX/osxTime.mm
+ ${TORQUE_SRC}/platformOSX/osxTorqueView.mm
+ ${TORQUE_SRC}/platformOSX/osxVideo.mm
+ ${TORQUE_SRC}/platformOSX/osxWindow.mm
+ ${TORQUE_SRC}/platformOSX/platformOSX.mm
+)
+
+# === Linux / X11 (platformX86UNIX) ===========================================
+set(TORQUE_PLATFORM_SOURCES_LINUX
+ # ---- platformX86UNIX ----
+ ${TORQUE_SRC}/platformX86UNIX/x86UNIXAsmBlit.cc
+ ${TORQUE_SRC}/platformX86UNIX/x86UNIXCPUInfo.cc
+ ${TORQUE_SRC}/platformX86UNIX/x86UNIXConsole.cc
+ ${TORQUE_SRC}/platformX86UNIX/x86UNIXDedicatedStub.cc
+ ${TORQUE_SRC}/platformX86UNIX/x86UNIXDialogs.cc
+ ${TORQUE_SRC}/platformX86UNIX/x86UNIXFileio.cc
+ ${TORQUE_SRC}/platformX86UNIX/x86UNIXFont.cc
+ ${TORQUE_SRC}/platformX86UNIX/x86UNIXGL.cc
+ ${TORQUE_SRC}/platformX86UNIX/x86UNIXIO.cc
+ ${TORQUE_SRC}/platformX86UNIX/x86UNIXInput.cc
+ ${TORQUE_SRC}/platformX86UNIX/x86UNIXInputManager.cc
+ ${TORQUE_SRC}/platformX86UNIX/x86UNIXMath.cc
+ ${TORQUE_SRC}/platformX86UNIX/x86UNIXMath_ASM.cc
+ ${TORQUE_SRC}/platformX86UNIX/x86UNIXMemory.cc
+ ${TORQUE_SRC}/platformX86UNIX/x86UNIXMessageBox.cc
+ ${TORQUE_SRC}/platformX86UNIX/x86UNIXMutex.cc
+ ${TORQUE_SRC}/platformX86UNIX/x86UNIXOGLVideo.cc
+ ${TORQUE_SRC}/platformX86UNIX/x86UNIXOpenAL.cc
+ ${TORQUE_SRC}/platformX86UNIX/x86UNIXPopupMenu.cc
+ ${TORQUE_SRC}/platformX86UNIX/x86UNIXProcessControl.cc
+ ${TORQUE_SRC}/platformX86UNIX/x86UNIXSemaphore.cc
+ ${TORQUE_SRC}/platformX86UNIX/x86UNIXStrings.cc
+ ${TORQUE_SRC}/platformX86UNIX/x86UNIXThread.cc
+ ${TORQUE_SRC}/platformX86UNIX/x86UNIXTime.cc
+ ${TORQUE_SRC}/platformX86UNIX/x86UNIXUtils.cc
+ ${TORQUE_SRC}/platformX86UNIX/x86UNIXWindow.cc
+)
+
+# === iOS (platformiOS) =======================================================
+# UIKit/OpenGL-ES back-end. SEPARATE from macOS (distinct sources + frameworks).
+# Was never supported by the old CMake; recipe derived from the Xcode_iOS project.
+# Builds & links for the arm64 simulator (iOS 18.2 SDK) -> Torque2D_DEBUG.app.
+# Requires full Xcode + `-DCMAKE_SYSTEM_NAME=iOS`; the root CMakeLists' TORQUE_IOS
+# block adds bitmapPvr.cc, defines TORQUE_OS_IOS + NO_REDEFINE_GL_FUNCS, and
+# force-includes tools/CMake/iOS-Prefix.h. See cmake/BUILD-PLATFORM-NOTES.md.
+set(TORQUE_PLATFORM_SOURCES_IOS
+ # ---- platformiOS ----
+ ${TORQUE_SRC}/platformiOS/GameCenter.mm
+ ${TORQUE_SRC}/platformiOS/SoundEngine.mm
+ ${TORQUE_SRC}/platformiOS/T2DAppDelegate.mm
+ ${TORQUE_SRC}/platformiOS/T2DView.mm
+ ${TORQUE_SRC}/platformiOS/T2DViewController.mm
+ ${TORQUE_SRC}/platformiOS/iOSAlerts.mm
+ ${TORQUE_SRC}/platformiOS/iOSAudio.mm
+ ${TORQUE_SRC}/platformiOS/iOSCPUInfo.mm
+ ${TORQUE_SRC}/platformiOS/iOSConsole.mm
+ ${TORQUE_SRC}/platformiOS/iOSDialogs.mm
+ ${TORQUE_SRC}/platformiOS/iOSEvents.mm
+ ${TORQUE_SRC}/platformiOS/iOSFileio.mm
+ ${TORQUE_SRC}/platformiOS/iOSFont.mm
+ ${TORQUE_SRC}/platformiOS/iOSGL.mm
+ ${TORQUE_SRC}/platformiOS/iOSGL2ES.mm
+ ${TORQUE_SRC}/platformiOS/iOSInput.mm
+ ${TORQUE_SRC}/platformiOS/iOSMath.mm
+ ${TORQUE_SRC}/platformiOS/iOSMemory.mm
+ ${TORQUE_SRC}/platformiOS/iOSMotionManager.mm
+ ${TORQUE_SRC}/platformiOS/iOSMoviePlayback.mm
+ ${TORQUE_SRC}/platformiOS/iOSMutex.mm
+ ${TORQUE_SRC}/platformiOS/iOSOGLVideo.mm
+ ${TORQUE_SRC}/platformiOS/iOSOutlineGL.mm
+ ${TORQUE_SRC}/platformiOS/iOSPlatform.mm
+ ${TORQUE_SRC}/platformiOS/iOSProcessControl.mm
+ ${TORQUE_SRC}/platformiOS/iOSProfiler.mm
+ ${TORQUE_SRC}/platformiOS/iOSSemaphore.mm
+ ${TORQUE_SRC}/platformiOS/iOSStreamSource.cc
+ ${TORQUE_SRC}/platformiOS/iOSStrings.mm
+ ${TORQUE_SRC}/platformiOS/iOSThread.mm
+ ${TORQUE_SRC}/platformiOS/iOSTime.mm
+ ${TORQUE_SRC}/platformiOS/iOSUserMusicLibrary.mm
+ ${TORQUE_SRC}/platformiOS/iOSUtil.mm
+ ${TORQUE_SRC}/platformiOS/iOSWindow.mm
+ ${TORQUE_SRC}/platformiOS/main.mm
+ # ---- platformiOS/menus ----
+ ${TORQUE_SRC}/platformiOS/menus/popupMenu.mm
+)
+
+# === Android (platformAndroid) ===============================================
+# NativeActivity / OpenGL-ES back-end built into libtorque2d.so by the NDK.
+# Recipe derived from the (now-deleted) ndk-build Android.mk; arm64-v8a target.
+set(TORQUE_PLATFORM_SOURCES_ANDROID
+ # ---- platformAndroid ----
+ ${TORQUE_SRC}/platformAndroid/AndroidAlerts.cpp
+ ${TORQUE_SRC}/platformAndroid/AndroidAudio.cpp
+ ${TORQUE_SRC}/platformAndroid/AndroidCPUInfo.cpp
+ ${TORQUE_SRC}/platformAndroid/AndroidConsole.cpp
+ ${TORQUE_SRC}/platformAndroid/AndroidDialogs.cpp
+ ${TORQUE_SRC}/platformAndroid/AndroidEvents.cpp
+ ${TORQUE_SRC}/platformAndroid/AndroidFileio.cpp
+ ${TORQUE_SRC}/platformAndroid/AndroidFont.cpp
+ ${TORQUE_SRC}/platformAndroid/AndroidGL.cpp
+ ${TORQUE_SRC}/platformAndroid/AndroidGL2ES.cpp
+ ${TORQUE_SRC}/platformAndroid/AndroidInput.cpp
+ ${TORQUE_SRC}/platformAndroid/AndroidMath.cpp
+ ${TORQUE_SRC}/platformAndroid/AndroidMemory.cpp
+ ${TORQUE_SRC}/platformAndroid/AndroidMutex.cpp
+ ${TORQUE_SRC}/platformAndroid/AndroidOGLVideo.cpp
+ ${TORQUE_SRC}/platformAndroid/AndroidOutlineGL.cpp
+ ${TORQUE_SRC}/platformAndroid/AndroidPlatform.cpp
+ ${TORQUE_SRC}/platformAndroid/AndroidProcessControl.cpp
+ ${TORQUE_SRC}/platformAndroid/AndroidProfiler.cpp
+ ${TORQUE_SRC}/platformAndroid/AndroidSemaphore.cpp
+ ${TORQUE_SRC}/platformAndroid/AndroidStreamSource.cc
+ ${TORQUE_SRC}/platformAndroid/AndroidStrings.cpp
+ ${TORQUE_SRC}/platformAndroid/AndroidThread.cpp
+ ${TORQUE_SRC}/platformAndroid/AndroidTime.cpp
+ ${TORQUE_SRC}/platformAndroid/AndroidUtil.cpp
+ ${TORQUE_SRC}/platformAndroid/AndroidWindow.cpp
+ ${TORQUE_SRC}/platformAndroid/T2DActivity.cpp
+ ${TORQUE_SRC}/platformAndroid/android_native_app_glue.c
+ ${TORQUE_SRC}/platformAndroid/main.cpp
+ # ---- platformAndroid/menus ----
+ ${TORQUE_SRC}/platformAndroid/menus/popupMenu.cpp
+)
+
+# === Emscripten / Web (platformEmscripten) ==================================
+# WebAssembly back-end built by emcc (configure via `emcmake cmake`). The browser
+# owns the event loop, so main.cpp drives the engine through
+# emscripten_set_main_loop(_EmscriptenGameInnerLoop, ...) -> Game->mainLoop() once
+# per animation frame (same callback model as iOS/Android). GL is GLES via the
+# EmscriptenGL2ES fixed-function shim over WebGL. The root CMakeLists' EMSCRIPTEN
+# block defines EMSCRIPTEN=1 (the engine's types.gcc.h keys TORQUE_OS_EMSCRIPTEN
+# off it), swaps in platformNet_Emscripten.cpp, and sets the emcc link flags.
+# Networking back-end (platformNet_Emscripten.cpp) is swapped in from the engine
+# list in the root CMakeLists, not listed here. See cmake/BUILD-PLATFORM-NOTES.md.
+set(TORQUE_PLATFORM_SOURCES_EMSCRIPTEN
+ # ---- platformEmscripten ----
+ ${TORQUE_SRC}/platformEmscripten/EmscriptenAlerts.cpp
+ ${TORQUE_SRC}/platformEmscripten/EmscriptenAudio.cpp
+ ${TORQUE_SRC}/platformEmscripten/EmscriptenConsole.cpp
+ ${TORQUE_SRC}/platformEmscripten/EmscriptenCPUInfo.cpp
+ ${TORQUE_SRC}/platformEmscripten/EmscriptenDialogs.cpp
+ ${TORQUE_SRC}/platformEmscripten/EmscriptenEvents.cpp
+ ${TORQUE_SRC}/platformEmscripten/EmscriptenFileio.cpp
+ ${TORQUE_SRC}/platformEmscripten/EmscriptenFont.cpp
+ ${TORQUE_SRC}/platformEmscripten/EmscriptenGL.cpp
+ ${TORQUE_SRC}/platformEmscripten/EmscriptenGL2ES.cpp
+ ${TORQUE_SRC}/platformEmscripten/EmscriptenInput.cpp
+ ${TORQUE_SRC}/platformEmscripten/EmscriptenInputManager.cpp
+ ${TORQUE_SRC}/platformEmscripten/EmscriptenMath.cpp
+ ${TORQUE_SRC}/platformEmscripten/EmscriptenMemory.cpp
+ ${TORQUE_SRC}/platformEmscripten/EmscriptenMutex.cpp
+ ${TORQUE_SRC}/platformEmscripten/EmscriptenOGLVideo.cpp
+ ${TORQUE_SRC}/platformEmscripten/EmscriptenOutlineGL.cpp
+ ${TORQUE_SRC}/platformEmscripten/EmscriptenPlatform.cpp
+ ${TORQUE_SRC}/platformEmscripten/EmscriptenProcessControl.cpp
+ ${TORQUE_SRC}/platformEmscripten/EmscriptenSemaphore.cpp
+ ${TORQUE_SRC}/platformEmscripten/EmscriptenStrings.cpp
+ ${TORQUE_SRC}/platformEmscripten/EmscriptenThread.cpp
+ ${TORQUE_SRC}/platformEmscripten/EmscriptenTime.cpp
+ ${TORQUE_SRC}/platformEmscripten/EmscriptenWindow.cpp
+ ${TORQUE_SRC}/platformEmscripten/main.cpp
+ # ---- platformEmscripten/menus ----
+ ${TORQUE_SRC}/platformEmscripten/menus/popupMenu.cpp
+)
diff --git a/editor/AssetAdmin/Animation/AssetAnimationFrameRange.cs b/editor/AssetAdmin/Animation/AssetAnimationFrameRange.cs
new file mode 100644
index 000000000..1e2101578
--- /dev/null
+++ b/editor/AssetAdmin/Animation/AssetAnimationFrameRange.cs
@@ -0,0 +1,164 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2013 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+
+//-----------------------------------------------------------------------------
+// Turning "the death animation is frames 28 to 32" into a list of frames.
+//
+// Kept apart from the dialog that asks for the numbers, because it is the part
+// worth checking: the dialog shows the answer back to the user before they
+// commit to it, and the test asks for the same answer without opening anything.
+//
+// Nothing here touches an asset or a control.
+//-----------------------------------------------------------------------------
+
+// A guard against a fat-fingered hold. AnimationAsset::getAnimationFrames formats
+// into a fixed 4096-byte buffer, so the ENGINE quietly truncates somewhere near a
+// thousand frames; this stops well short of that.
+$AssetAnimationFrameRange::maxFrames = 512;
+
+//-----------------------------------------------------------------------------
+// Three stages, and the order of them is the whole design.
+//
+// 1. the stepped run from start to end, counting down when end is the smaller
+// 2. if ping-pong, the reverse of that MINUS both shared end frames
+// 3. then every element repeated %hold times
+//
+// Hold goes last so that "shared end frames" means one frame rather than N slots.
+// Dropping those two is what stops the turn at each end lasting twice as long as
+// every other frame, which reads as a stutter.
+//-----------------------------------------------------------------------------
+
+function AssetAnimationFrameRange::build(%this, %start, %end, %step, %hold, %pingPong)
+{
+ %start = mFloor(%start);
+ %end = mFloor(%end);
+ %step = mGetMax(1, mFloor(%step));
+ %hold = mGetMax(1, mFloor(%hold));
+
+ %forward = "";
+ %direction = (%end >= %start) ? %step : -%step;
+
+ for(%frame = %start; (%direction > 0) ? (%frame <= %end) : (%frame >= %end); %frame += %direction)
+ {
+ %forward = (%forward $= "") ? %frame : (%forward SPC %frame);
+ }
+
+ %list = %forward;
+
+ if(%pingPong)
+ {
+ // From the second-to-last back to the second: both ends are already in the
+ // run and playing them twice is what makes a ping-pong stutter.
+ %count = getWordCount(%forward);
+ for(%i = %count - 2; %i >= 1; %i--)
+ {
+ %list = %list SPC getWord(%forward, %i);
+ }
+ }
+
+ if(%hold > 1)
+ {
+ %held = "";
+ %count = getWordCount(%list);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %frame = getWord(%list, %i);
+ for(%r = 0; %r < %hold; %r++)
+ {
+ %held = (%held $= "") ? %frame : (%held SPC %frame);
+ }
+ }
+ %list = %held;
+ }
+
+ return %list;
+}
+
+// Why a given set of numbers cannot be used, or "" when it can.
+//
+// Takes the image's frame count so the message can name it: counting from one is
+// the mistake people actually make, and "0 to 99" says more than "out of range".
+function AssetAnimationFrameRange::problemWith(%this, %start, %end, %step, %hold, %pingPong, %imageFrameCount)
+{
+ if(%start $= "" || %end $= "")
+ {
+ return "Give a first and last frame.";
+ }
+
+ if(%start < 0 || %end < 0)
+ {
+ return "Frames start at 0.";
+ }
+
+ if(%imageFrameCount > 0 && (%start >= %imageFrameCount || %end >= %imageFrameCount))
+ {
+ return "This image has" SPC %imageFrameCount SPC "frames, numbered 0 to" SPC (%imageFrameCount - 1) @ ".";
+ }
+
+ if(%step < 1)
+ {
+ return "A step of less than 1 would never get there.";
+ }
+
+ if(%hold < 1)
+ {
+ return "Every frame has to be held at least once.";
+ }
+
+ %count = getWordCount(%this.build(%start, %end, %step, %hold, %pingPong));
+ if(%count > $AssetAnimationFrameRange::maxFrames)
+ {
+ return "That would make" SPC %count SPC "frames, and" SPC
+ $AssetAnimationFrameRange::maxFrames SPC "is the most an animation can hold here.";
+ }
+
+ return "";
+}
+
+// What the user is about to get, in words. The strongest argument for keeping the
+// builder callable without a dialog: this line is the builder's own answer read
+// back, not a second description of it that could drift.
+function AssetAnimationFrameRange::describe(%this, %frames, %mode, %existingCount)
+{
+ %count = getWordCount(%frames);
+ if(%count == 0)
+ {
+ return "";
+ }
+
+ %shown = %frames;
+ if(%count > 12)
+ {
+ %shown = "";
+ for(%i = 0; %i < 10; %i++)
+ {
+ %shown = (%shown $= "") ? getWord(%frames, %i) : (%shown SPC getWord(%frames, %i));
+ }
+ %shown = %shown SPC "..." SPC getWord(%frames, %count - 1);
+ }
+
+ %tail = (%mode $= "append")
+ ? ("appended to the" SPC %existingCount SPC ((%existingCount == 1) ? "already there" : "already there"))
+ : "replacing what is there";
+
+ return %shown SPC "-" SPC %count SPC ((%count == 1) ? "frame," : "frames,") SPC %tail @ ".";
+}
diff --git a/editor/AssetAdmin/Animation/AssetAnimationPalettePane.cs b/editor/AssetAdmin/Animation/AssetAnimationPalettePane.cs
new file mode 100644
index 000000000..32710226b
--- /dev/null
+++ b/editor/AssetAdmin/Animation/AssetAnimationPalettePane.cs
@@ -0,0 +1,128 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2013 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+
+//-----------------------------------------------------------------------------
+// The right-hand pane of the animation editor: every frame the animation's image
+// has to offer, scrolled vertically, to click or drag into the timeline.
+//
+// The pane is the scroller and the caption; the grid inside it is C++, because
+// script cannot ask an image where one of its frames is.
+//-----------------------------------------------------------------------------
+
+$AssetAnimationPalettePane::captionHeight = 20;
+
+function AssetAnimationPalettePane::onAdd(%this)
+{
+ ThemeManager.setProfile(%this, "emptyProfile");
+
+ %this.caption = new GuiControl()
+ {
+ HorizSizing = "width";
+ VertSizing = "bottom";
+ Position = "0 0";
+ Extent = "100" SPC $AssetAnimationPalettePane::captionHeight;
+ Text = "Frames";
+ };
+ // panelProfile, which is what the Asset Inspector's own title bar wears: its
+ // font is the theme's color5, legible on the panel fill behind it. labelProfile
+ // is meant for a caption on the window background and comes out near-black on
+ // dark blue under Lab Coat.
+ ThemeManager.setProfile(%this.caption, "panelProfile");
+ %this.add(%this.caption);
+
+ // "height", not "fill": fill means "be the whole of the parent", which here
+ // would put the scroller over the caption. Anchoring both edges keeps the
+ // caption's height at the top and takes everything below it.
+ %this.scroller = new GuiScrollCtrl()
+ {
+ HorizSizing = "width";
+ VertSizing = "height";
+ Position = "0" SPC $AssetAnimationPalettePane::captionHeight;
+ Extent = "100 80";
+ hScrollBar = "alwaysOff";
+
+ // Always on, not dynamic. A sheet worth opening the palette for has more
+ // frames than fit, so the bar is all but permanent anyway -- and a
+ // dynamic bar has to be decided from the strip's height, which the strip
+ // works out during the very layout pass that would have to notice it.
+ vScrollBar = "alwaysOn";
+ constantThumbHeight = false;
+ scrollBarThickness = 14;
+ showArrowButtons = true;
+ };
+ ThemeManager.setProfile(%this.scroller, "scrollingPanelProfile");
+ ThemeManager.setProfile(%this.scroller, "scrollingPanelThumbProfile", "ThumbProfile");
+ ThemeManager.setProfile(%this.scroller, "scrollingPanelTrackProfile", "TrackProfile");
+ ThemeManager.setProfile(%this.scroller, "scrollingPanelArrowProfile", "ArrowProfile");
+ %this.add(%this.scroller);
+
+ // No class= on the grid. The C++ class owns that namespace, and setting class
+ // to the same name makes Namespace::classLinkTo complain every time the
+ // editor opens. The owner is passed as a plain field instead.
+ // "fill" across, and it is legal precisely because the horizontal bar is
+ // alwaysOff: GuiScrollCtrl only refuses fill on an axis it can scroll, where
+ // filling would clamp the content to what is already visible. Across, there
+ // is nothing to scroll and fill is how the grid asks for the real width --
+ // which it must have before it can work out how many columns fit.
+ %this.strip = new GuiEditFramePaletteCtrl()
+ {
+ pane = %this;
+ HorizSizing = "fill";
+ VertSizing = "bottom";
+ Position = "0 0";
+ Extent = "100 100";
+ CellSize = 48;
+ CellPad = 4;
+ ShowFrameNumbers = true;
+ };
+ // The same profile the timeline wears, so hover and frame numbers look the
+ // same in both grids -- which matters when a frame is being dragged from one
+ // to the other.
+ ThemeManager.setProfile(%this.strip, "frameGridProfile");
+ %this.scroller.add(%this.strip);
+}
+
+function AssetAnimationPalettePane::load(%this, %imageAssetId)
+{
+ %this.strip.setImageAsset(%imageAssetId);
+ %this.refreshCaption();
+}
+
+function AssetAnimationPalettePane::reload(%this)
+{
+ // The image may have been re-cut, so the frame count has moved. Setting the
+ // same id again is what makes the grid ask it afresh.
+ %this.strip.setImageAsset(%this.strip.getImageAsset());
+ %this.refreshCaption();
+}
+
+function AssetAnimationPalettePane::refreshCaption(%this)
+{
+ %count = %this.strip.getImageFrameCount();
+ if(%count == 1)
+ {
+ %this.caption.setText("1 frame");
+ return;
+ }
+
+ %this.caption.setText(%count SPC "frames");
+}
diff --git a/editor/AssetAdmin/Animation/AssetAnimationRangeDialog.cs b/editor/AssetAdmin/Animation/AssetAnimationRangeDialog.cs
new file mode 100644
index 000000000..65c2f2f51
--- /dev/null
+++ b/editor/AssetAdmin/Animation/AssetAnimationRangeDialog.cs
@@ -0,0 +1,218 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2013 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+
+//-----------------------------------------------------------------------------
+// "The death animation is frames 28 to 32", asked for in a dialog.
+//
+// Dragging twenty-five frames one at a time is the thing this exists to spare
+// people. The arithmetic lives in AssetAnimationFrameRange, which knows nothing
+// about controls; this asks for the numbers and shows the answer back before
+// anything is written.
+//
+// That feedback line is the whole point of the dialog rather than a nicety --
+// step, hold and ping-pong interact in ways nobody should have to predict, and
+// reading the actual frames out is quicker than explaining the rules.
+//-----------------------------------------------------------------------------
+
+function AssetAnimationRangeDialog::init(%this, %width, %height)
+{
+ %window = %this.getObject(0);
+ %content = %window.getObject(0);
+
+ %form = new GuiGridCtrl()
+ {
+ class = "EditorForm";
+ extent = %width SPC %height;
+ cellSizeX = %width / 2;
+ cellSizeY = 50;
+ cellModeX = "fixed";
+ cellModeY = "fixed";
+ maxColCount = 2;
+ };
+ %form.addListener(%this);
+
+ %half = %width / 2;
+
+ %item = %form.addFormItem("First Frame", %half SPC 30);
+ %this.startBox = %form.createTextEditItem(%item);
+
+ %item = %form.addFormItem("Last Frame", %half SPC 30);
+ %this.endBox = %form.createTextEditItem(%item);
+
+ %item = %form.addFormItem("Step", %half SPC 30);
+ %this.stepBox = %form.createTextEditItem(%item);
+
+ %item = %form.addFormItem("Hold Each Frame", %half SPC 30);
+ %this.holdBox = %form.createTextEditItem(%item);
+
+ %item = %form.addFormItem("Ping-pong", %half SPC 30);
+ %this.pingPongBox = %form.createCheckboxItem(%item);
+
+ %item = %form.addFormItem("Mode", %half SPC 30);
+ // addItem, not add. GuiControl::add is what was being called here -- it takes a
+ // CONTROL and puts it inside this one -- so the list stayed empty, the box
+ // read "none", and the second choice could not be picked at all.
+ %this.modeDropDown = %form.createDropDownItem(%item);
+ %this.modeDropDown.addItem("Append to the timeline");
+ %this.modeDropDown.addItem("Replace the timeline");
+
+ %content.add(%form);
+
+ // Every box re-asks the same question on every keystroke, so Apply is only
+ // ever live when the numbers make sense and the line below always describes
+ // what is about to happen.
+ %command = %this.getId() @ ".validate();";
+ %this.startBox.Command = %command;
+ %this.endBox.Command = %command;
+ %this.stepBox.Command = %command;
+ %this.holdBox.Command = %command;
+ %this.pingPongBox.Command = %command;
+ %this.modeDropDown.Command = %command;
+
+ // Below whatever the form actually came out as, rather than below a number
+ // written here: the grid decides its own height from how many rows six items
+ // make, and a seventh field would silently land underneath this.
+ %formBottom = getWord(%form.getPosition(), 1) + getWord(%form.getExtent(), 1);
+
+ // The answer line. textExtend grows it downward for a long answer, which is
+ // why the buttons sit well clear of where it starts rather than just under it.
+ %this.feedback = new GuiControl()
+ {
+ HorizSizing = "width";
+ VertSizing = "anchorTop";
+ Position = "12" SPC (%formBottom + 8);
+ Extent = (%width - 24) SPC 90;
+ text = "";
+ textWrap = true;
+ textExtend = true;
+ };
+ ThemeManager.setProfile(%this.feedback, "infoProfile");
+ %content.add(%this.feedback);
+
+ // Measured from the room the content actually has, not from the dialog's own
+ // height -- the title bar and border take 34 of it, and buttons placed
+ // without allowing for that fall off the bottom.
+ %bottom = %this.contentHeight() - 12;
+
+ %this.cancelButton = new GuiButtonCtrl()
+ {
+ HorizSizing = "anchorRight";
+ VertSizing = "anchorBottom";
+ Position = (%width - 222) SPC (%bottom - 32);
+ Extent = "100 30";
+ Text = "Cancel";
+ Command = %this.getID() @ ".onClose();";
+ };
+ ThemeManager.setProfile(%this.cancelButton, "buttonProfile");
+ %content.add(%this.cancelButton);
+
+ %this.applyButton = new GuiButtonCtrl()
+ {
+ HorizSizing = "anchorRight";
+ VertSizing = "anchorBottom";
+ Position = (%width - 112) SPC (%bottom - 34);
+ Extent = "100 34";
+ Text = "Apply";
+ Command = %this.getID() @ ".onApply();";
+ };
+ ThemeManager.setProfile(%this.applyButton, "primaryButtonProfile");
+ %content.add(%this.applyButton);
+
+ // Down here with the rest of the starting values, and not beside the add()s
+ // that fill the list, because a drop down that has not been added to anything
+ // yet is not awake -- and the selection made on it then does not stick. The
+ // list showed "none" until the user opened it.
+ %this.modeDropDown.setSelected(0);
+
+ %this.startBox.setText(0);
+ %this.endBox.setText(mGetMax(0, %this.imageFrameCount() - 1));
+ %this.stepBox.setText(1);
+ %this.holdBox.setText(1);
+
+ %this.validate();
+}
+
+function AssetAnimationRangeDialog::imageFrameCount(%this)
+{
+ if(!isObject(%this.stage) || !isObject(%this.stage.imageAsset))
+ {
+ return 0;
+ }
+
+ return %this.stage.imageAsset.getFrameCount();
+}
+
+// getSelectedItem, not getSelected: the latter is not a method on a drop down at
+// all, so this always answered "append" and Replace was unreachable.
+function AssetAnimationRangeDialog::mode(%this)
+{
+ return (%this.modeDropDown.getSelectedItem() == 1) ? "replace" : "append";
+}
+
+function AssetAnimationRangeDialog::validate(%this)
+{
+ %range = AssetAdmin.frameRange;
+
+ %start = %this.startBox.getText();
+ %end = %this.endBox.getText();
+ %step = %this.stepBox.getText();
+ %hold = %this.holdBox.getText();
+ %pingPong = %this.pingPongBox.getValue();
+
+ %problem = %range.problemWith(%start, %end, %step, %hold, %pingPong, %this.imageFrameCount());
+ if(%problem !$= "")
+ {
+ %this.applyButton.setActive(false);
+ %this.feedback.setText(%problem);
+ return false;
+ }
+
+ %frames = %range.build(%start, %end, %step, %hold, %pingPong);
+
+ %this.applyButton.setActive(true);
+ %this.feedback.setText(%range.describe(%frames, %this.mode(),
+ %this.stage.timelinePane.strip.getCellCount()));
+
+ return true;
+}
+
+function AssetAnimationRangeDialog::onApply(%this)
+{
+ if(!%this.validate())
+ {
+ return;
+ }
+
+ %frames = AssetAdmin.frameRange.build(%this.startBox.getText(), %this.endBox.getText(),
+ %this.stepBox.getText(), %this.holdBox.getText(), %this.pingPongBox.getValue());
+
+ if(%this.mode() $= "replace")
+ {
+ %this.stage.setFrames(%frames);
+ }
+ else
+ {
+ %this.stage.appendFrames(%frames);
+ }
+
+ %this.onClose();
+}
diff --git a/editor/AssetAdmin/Animation/AssetAnimationStage.cs b/editor/AssetAdmin/Animation/AssetAnimationStage.cs
new file mode 100644
index 000000000..d82f15146
--- /dev/null
+++ b/editor/AssetAdmin/Animation/AssetAnimationStage.cs
@@ -0,0 +1,778 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2013 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+
+//-----------------------------------------------------------------------------
+// Turns the Asset Manager's preview into an animation editor while an animation
+// asset is selected, and puts it back the moment anything else is.
+//
+// A manager rather than a control: it owns two panes and the shape of the frame
+// set they live in, and it is the one place that knows an animation is on show.
+// Everything that used to ask "is this an animation?" now asks this instead.
+//
+// The split is built on demand and collapsed on the way out. It has to be, not
+// merely for tidiness: the other five asset kinds want the whole preview area,
+// and a frame set with three frames cannot give it to them.
+//-----------------------------------------------------------------------------
+
+$AssetAnimationStage::defaultPaletteWidth = 220;
+$AssetAnimationStage::defaultTimelineHeight = 150;
+
+// A frame set hands out ids from 1, and the root is the first. Named because
+// createVerticalSplit(1) reads as a magic number otherwise.
+$AssetAnimationStage::rootFrameId = 1;
+
+function AssetAnimationStage::onAdd(%this)
+{
+ %this.built = false;
+ %this.assetId = "";
+ %this.playing = false;
+
+ // -1 rather than unset: an unset field reads as an empty string, which is not
+ // less than zero, so every "have I got one?" test below would pass with it.
+ %this.resumeSlot = -1;
+}
+
+function AssetAnimationStage::onRemove(%this)
+{
+ %this.teardown();
+}
+
+//-----------------------------------------------------------------------------
+// Selection.
+//
+// retainFor is called for EVERY asset, whatever its kind, from the one place a
+// selection is made. Saying "keep the split if it is for this asset, otherwise
+// take it down" in one method is what keeps the five branches that have nothing
+// to do with animation completely untouched.
+//-----------------------------------------------------------------------------
+
+function AssetAnimationStage::retainFor(%this, %animationAssetId)
+{
+ if(%animationAssetId $= "" || !AssetDatabase.isDeclaredAsset(%animationAssetId))
+ {
+ %this.teardown();
+ return false;
+ }
+
+ return true;
+}
+
+// Whether the animation on show addresses its frames by name.
+//
+// Asked of the asset, which asks the image: an image in explicit mode cuts itself
+// into named cells, so an animation on it lists names. Nothing here sets it, and
+// there is no flag to set -- changing the image, or that image's explicit mode,
+// is what changes the answer.
+//
+// The whole editor stays in INDEX space either way. The palette shows cell N, the
+// timeline holds cell N, a drag carries cell N; only loading and committing know
+// about names at all. What that buys is that every gesture, the range dialog, the
+// caret arithmetic and the hold detection are written once.
+function AssetAnimationStage::namedMode(%this)
+{
+ return isObject(%this.animationAsset) && %this.animationAsset.getNamedCellsMode();
+}
+
+// Point the timeline at the asset's frames, in whichever space they are kept.
+//
+// One method because there were three call sites that each did it slightly
+// differently -- selection, a refresh, and an inspector commit -- and a fourth
+// spelling of it was how the named case would have been missed.
+function AssetAnimationStage::loadTimeline(%this)
+{
+ if(!isObject(%this.timelinePane) || !isObject(%this.animationAsset))
+ {
+ return;
+ }
+
+ if(%this.namedMode())
+ {
+ %this.timelinePane.loadNamed(%this.imageAssetId, trim(%this.animationAsset.getNamedAnimationFrames()));
+ }
+ else
+ {
+ %this.timelinePane.load(%this.imageAssetId, trim(%this.animationAsset.getAnimationFrames()));
+ }
+}
+
+function AssetAnimationStage::select(%this, %imageAsset, %animationAsset, %assetId)
+{
+ if(%this.busy || !%this.retainFor(%assetId))
+ {
+ return;
+ }
+
+ %this.build();
+
+ // A selection that arrives with no panes to put anything in. absorbResize now
+ // closes the route this was written for -- a resize mid-teardown re-clicking
+ // the selected tile -- but build() can also decline, so nothing below may
+ // assume a pane is there.
+ if(!isObject(%this.palettePane) || !isObject(%this.timelinePane))
+ {
+ return;
+ }
+
+ // Borrowed, not acquired. AssetDictionaryButton::loadAnimationAsset already
+ // holds both of these and releases them in its onRemove, and this stage never
+ // outlives a selection -- a second acquire here would be a second release to
+ // remember somewhere else.
+ %this.animationAsset = %animationAsset;
+ %this.imageAsset = %imageAsset;
+ %this.assetId = %assetId;
+ %this.imageAssetId = %animationAsset.getImage();
+
+ %this.palettePane.load(%this.imageAssetId);
+ %this.loadTimeline();
+
+ %this.admin.transportBarContainer.setVisible(true);
+
+ // Adopt the sprite the preview has already made. displayAnimationAsset builds
+ // it and announces it BEFORE this runs -- the tile displays first and selects
+ // second -- so on a first selection that announcement arrives while there is
+ // no stage to hear it. Asking here covers both orders.
+ //
+ // Before the bar is refreshed, because adopting the sprite is what settles
+ // whether the animation is playing.
+ %this.onPreviewRebuilt(%this.admin.previewSprite);
+
+ // The sprite was measured against the whole preview area, because it was made
+ // before the split existed -- the tile displays first and selects second. The
+ // split has since taken a palette and a timeline out of that area, and the only
+ // thing that answers a resize while it is being built is the guard that stops
+ // the preview being rebuilt. So the sprite is put right here, once, at the end.
+ %this.resizePreview();
+
+ %this.admin.transportBar.refresh();
+}
+
+//-----------------------------------------------------------------------------
+// Building and collapsing the split.
+//-----------------------------------------------------------------------------
+
+// Building and collapsing both move the dividers, which resizes the SceneWindow,
+// whose onExtentChange answers a resize by re-clicking the selected tile -- which
+// lands back here. So both are shut for the duration.
+//
+// Without it, deleting the first pane re-entered select() while built was still
+// true, and the second call reached for a pane that was already half gone.
+function AssetAnimationStage::build(%this)
+{
+ if(%this.built || %this.busy)
+ {
+ return;
+ }
+
+ %this.busy = true;
+ %frames = %this.admin.previewFrames;
+
+ // One split at a time, and the pane it makes room for added straight after.
+ // GuiFrameSetCtrl::assignChildToFrame puts a child in the empty frame its
+ // bounds fall inside, or failing that the first empty frame it walks to --
+ // neither of which is worth relying on. Adding while exactly one frame is
+ // empty makes the answer certain.
+ //
+ // splitFrame leaves the existing control -- the preview background -- in
+ // child1 and anchors it, so the art stays top left through both splits and is
+ // never reparented.
+ %ids = %frames.createVerticalSplit($AssetAnimationStage::rootFrameId);
+ %topId = getWord(%ids, 0);
+ %this.timelineFrameId = getWord(%ids, 1);
+ %frames.anchorFrame(%this.timelineFrameId);
+
+ %this.timelinePane = new GuiControl()
+ {
+ class = "AssetAnimationTimelinePane";
+ stage = %this;
+ HorizSizing = "width";
+ VertSizing = "height";
+ Position = "0 0";
+ Extent = "100 100";
+ };
+ %frames.add(%this.timelinePane);
+
+ %ids = %frames.createHorizontalSplit(%topId);
+ %this.paletteFrameId = getWord(%ids, 1);
+ %frames.anchorFrame(%this.paletteFrameId);
+
+ %this.palettePane = new GuiControl()
+ {
+ class = "AssetAnimationPalettePane";
+ stage = %this;
+ HorizSizing = "width";
+ VertSizing = "height";
+ Position = "0 0";
+ Extent = "100 100";
+ };
+ %frames.add(%this.palettePane);
+
+ // Both sizes LAST, and that ordering is the whole of it. setFrameSize is the
+ // only thing here that lays the tree out -- it ends in a resize of the whole
+ // frame set -- and a layout can only size the controls that are already in
+ // their frames. Sized before the panes were added, the split came out with
+ // the right frames and a palette still at the 100 x 100 it was built with,
+ // sitting behind the preview where nothing could be seen of it.
+ %frames.setFrameSize(%this.timelineFrameId,
+ EditorPreferences.get("assetAnimationTimelineHeight", $AssetAnimationStage::defaultTimelineHeight));
+ %frames.setFrameSize(%this.paletteFrameId,
+ EditorPreferences.get("assetAnimationPaletteWidth", $AssetAnimationStage::defaultPaletteWidth));
+
+ %this.built = true;
+ %this.busy = false;
+}
+
+function AssetAnimationStage::teardown(%this)
+{
+ if(!%this.built || %this.busy)
+ {
+ return;
+ }
+
+ %this.busy = true;
+ %this.rememberSizes();
+ %this.admin.transportBarContainer.setVisible(false);
+
+ // Deleting each pane collapses the frame that held it and hoists its twin's
+ // subtree, so two deletes take the tree back to one frame holding the preview
+ // background -- which was never moved and does not have to be put back.
+ if(isObject(%this.palettePane))
+ {
+ %this.palettePane.delete();
+ }
+ if(isObject(%this.timelinePane))
+ {
+ %this.timelinePane.delete();
+ }
+
+ // Forgotten, not merely deleted. Sim ids are handed out again once they are
+ // free, so a field still holding a deleted pane's id will happily answer
+ // isObject() -- about whatever object took that id next. The symptom is a
+ // call into a live object that has never heard of the method, which reads
+ // like a namespace problem and is nothing of the sort.
+ %this.palettePane = "";
+ %this.timelinePane = "";
+ %this.previewSprite = "";
+
+ %this.built = false;
+ %this.assetId = "";
+ %this.animationAsset = "";
+ %this.imageAsset = "";
+ %this.playing = false;
+ %this.busy = false;
+}
+
+// A frame set has no divider-moved callback -- there is no Con::executef
+// anywhere in guiFrameSetCtrl.cc -- so the sizes are read at the two moments the
+// split is going away, which is the last chance to see where the user left them.
+function AssetAnimationStage::rememberSizes(%this)
+{
+ if(!%this.built)
+ {
+ return;
+ }
+
+ %layout = %this.admin.previewFrames.getFrameLayout();
+
+ %paletteWidth = %this.frameSizeFrom(%layout, %this.paletteFrameId, 0);
+ %timelineHeight = %this.frameSizeFrom(%layout, %this.timelineFrameId, 1);
+
+ if(%paletteWidth > 0)
+ {
+ EditorPreferences.set("assetAnimationPaletteWidth", %paletteWidth);
+ }
+ if(%timelineHeight > 0)
+ {
+ EditorPreferences.set("assetAnimationTimelineHeight", %timelineHeight);
+ }
+}
+
+// getFrameLayout is eight words per frame:
+// id child1 child2 isVertical extentX extentY isAnchored controlID
+function AssetAnimationStage::frameSizeFrom(%this, %layout, %frameId, %axis)
+{
+ %count = getWordCount(%layout);
+ for(%i = 0; %i < %count; %i += 8)
+ {
+ if(getWord(%layout, %i) == %frameId)
+ {
+ return getWord(%layout, %i + 4 + %axis);
+ }
+ }
+
+ return 0;
+}
+
+//-----------------------------------------------------------------------------
+// The preview.
+//-----------------------------------------------------------------------------
+
+// The preview scene is cleared and rebuilt from scratch by the display path, so
+// the sprite the timeline follows is a different object every time. Said here
+// rather than found by the timeline, because the timeline should not have to
+// know how the preview is made.
+function AssetAnimationStage::onPreviewRebuilt(%this, %sprite)
+{
+ if(!%this.built || %this.busy || !isObject(%sprite) || !isObject(%this.timelinePane))
+ {
+ return;
+ }
+
+ %this.previewSprite = %sprite;
+ %this.timelinePane.setPreviewSprite(%sprite);
+
+ // A sprite built with an Animation on it is ALREADY RUNNING -- nothing had to
+ // press play -- so the editor's idea of the playing state has to be read off
+ // the sprite rather than assumed. It was initialised false, so selecting an
+ // animation put a Play button over a preview that was busy playing.
+ %this.playing = !%sprite.getIsAnimationFinished();
+ %this.refreshTransport();
+}
+
+// Put a finished animation back in a state where it can be moved.
+//
+// The single answer to a trap that otherwise looks like a dead control:
+// ImageFrameProviderCore::updateAnimation returns immediately once
+// mAnimationFinished is set, and setAnimationFrame goes through it -- so a
+// non-cycling preview that has run to the end cannot be scrubbed at all.
+// playAnimation is the only way back, and it clears the pause on its way, so the
+// pause has to be put back afterwards.
+function AssetAnimationStage::armPreview(%this)
+{
+ if(!isObject(%this.previewSprite))
+ {
+ return false;
+ }
+
+ if(%this.previewSprite.getIsAnimationFinished())
+ {
+ %this.previewSprite.playAnimation(%this.assetId);
+ %this.previewSprite.pauseAnimation(!%this.playing);
+ }
+
+ return true;
+}
+
+// The one place a slot is ever set.
+function AssetAnimationStage::scrubTo(%this, %slot)
+{
+ if(!%this.armPreview())
+ {
+ return;
+ }
+
+ %count = %this.timelinePane.strip.getCellCount();
+ if(%count < 1)
+ {
+ return;
+ }
+
+ %this.previewSprite.setAnimationFrame(mClamp(%slot, 0, %count - 1));
+}
+
+// Clicking a slot stops first, deliberately: scrubbing a running preview shows a
+// frame for a thirtieth of a second and then moves on, which reads as the click
+// having done nothing.
+function AssetAnimationStage::onSlotSelected(%this, %slot, %frame)
+{
+ %this.stop();
+ %this.scrubTo(%slot);
+}
+
+// Pausing, not stopping. SpriteBase::stopAnimation sets the finished flag, which
+// is what kills setAnimationFrame -- so a preview stopped that way could never be
+// scrubbed again. Pausing halts it just as visibly and leaves every other gesture
+// alive.
+function AssetAnimationStage::stop(%this)
+{
+ if(!isObject(%this.previewSprite))
+ {
+ return;
+ }
+
+ %this.playing = false;
+ %this.previewSprite.pauseAnimation(true);
+ %this.refreshTransport();
+}
+
+// Every path that changes the playing state ends here, and there are more of
+// them than the two buttons: clicking a slot stops in order to scrub, dragging a
+// frame off the timeline stops, and a one-shot animation stops itself by reaching
+// the end. Each of those left a Stop button showing over a stopped preview until
+// the bar was told.
+function AssetAnimationStage::refreshTransport(%this)
+{
+ if(!%this.built || !isObject(%this.admin.transportBar))
+ {
+ return;
+ }
+
+ %this.admin.transportBar.refresh();
+}
+
+function AssetAnimationStage::play(%this)
+{
+ if(!%this.armPreview())
+ {
+ return;
+ }
+
+ %this.playing = true;
+ %this.previewSprite.pauseAnimation(false);
+ %this.refreshTransport();
+}
+
+// A one-shot animation reached its end on its own. Nothing to do to the preview
+// -- the engine has already parked it on the last frame -- but the button still
+// says Stop, over something that has already stopped.
+function AssetAnimationStage::onPreviewFinished(%this)
+{
+ %this.playing = false;
+ %this.refreshTransport();
+}
+
+//-----------------------------------------------------------------------------
+// Absorbing a refresh instead of being rebuilt by one.
+//
+// Every asset setter ends in refreshAsset, which rewrites the file and fires
+// onRefresh -- and the editor's answer to onRefresh has always been to re-click
+// the selected tile, which clears the preview scene and builds a new sprite. For
+// an image that is exactly right. For an animation being edited it means the
+// preview restarts from frame one every time a frame is dragged.
+//
+// So the stage says "I've got this" for the asset it is showing, and the old
+// path is untouched for everything else.
+//-----------------------------------------------------------------------------
+
+function AssetAnimationStage::absorbRefresh(%this, %asset)
+{
+ if(!%this.built || %this.busy || !isObject(%asset))
+ {
+ return false;
+ }
+
+ %isAnimation = (%asset == %this.animationAsset);
+ %isImage = (%asset == %this.imageAsset);
+
+ if(!%isAnimation && !%isImage)
+ {
+ return false;
+ }
+
+ // Unless the strip is what produced this value in the first place. Reloading
+ // it from the asset mid-commit would throw away the selection and the caret
+ // for a list it already holds. The same guard shape AssetInspectorPane uses.
+ if(%isAnimation && !%this.committing)
+ {
+ %this.loadTimeline();
+ }
+
+ // The image may have been re-cut, so the palette's frame count has moved --
+ // and so has what the animation's frames mean.
+ //
+ // It may also have changed explicit mode, which moves the animation between
+ // name space and index space entirely. The asset has already converted its own
+ // list by the time this runs -- that is what AnimationAsset::onAssetRefresh
+ // does -- so the timeline is reloaded here as well, from whichever list is now
+ // the live one.
+ if(%isImage)
+ {
+ %this.palettePane.reload();
+ %this.loadTimeline();
+ }
+
+ %this.resyncPreview();
+ return true;
+}
+
+// Put the preview back the way it was before the write disturbed it.
+//
+// Three cases, because ImageFrameProviderCore::onAssetRefreshed has already had
+// its say by the time this runs: it calls playAnimation on a RUNNING preview,
+// restarting it from slot zero, and does nothing at all to a finished one. And
+// playAnimation opens by clearing the pause, so a paused preview comes back
+// playing.
+function AssetAnimationStage::resyncPreview(%this)
+{
+ if(!isObject(%this.previewSprite))
+ {
+ return;
+ }
+
+ %this.armPreview();
+ %this.previewSprite.pauseAnimation(!%this.playing);
+
+ // Only when a commit of ours put a slot aside. One write raises more than one
+ // refresh -- the asset manager fans them out as it re-reads the file it just
+ // wrote -- and the later ones arrive with nothing remembered.
+ //
+ // The obvious fallback, asking the strip where its marker was drawn, is a
+ // trap: that marker is a cached value updated once a frame in onPreRender, so
+ // mid-script it is whatever the last rendered frame said. Restoring from it
+ // undid the correct restore the first refresh had just made, which is how the
+ // playhead ended up back at zero after every edit.
+ //
+ // And by SLOT, not by image frame: a slot's meaning shifts when something is
+ // inserted before it, so the preview can appear to skip -- but tracking the
+ // image frame breaks the moment a frame appears twice, which is what a hold is.
+ if(%this.resumeSlot >= 0)
+ {
+ %this.scrubTo(%this.resumeSlot);
+ }
+}
+
+// A divider moved, or the editor was resized, and the window is asking whether it
+// should answer that by rebuilding the preview from the selected tile. Two
+// separate reasons it must not, and only one of them is about size.
+//
+// The first is the split being built or collapsed. Both move dividers, so both
+// come back through here -- and the tile the window would re-click is the
+// PREVIOUSLY selected one, because AssetDictionaryButton::onClick does not record
+// its own tile until every branch below it has run. Selecting an animation while
+// anything else was selected therefore repainted the preview with the asset the
+// user had just navigated away from, on top of the animation sprite that had been
+// made a moment earlier -- and cleared that sprite out from under the stage. There
+// is nothing for the window to do in either case: the selection that started the
+// rebuild paints the preview itself, before or after, and this is only a divider
+// moving in the middle of it.
+//
+// The second is an animation already on show, which is the case below.
+function AssetAnimationStage::absorbResize(%this)
+{
+ if(%this.busy)
+ {
+ return true;
+ }
+
+ return %this.resizePreview();
+}
+
+// A divider moved, or the editor was resized. The sprite is already there and
+// only its size is wrong, so there is nothing to rebuild.
+function AssetAnimationStage::resizePreview(%this)
+{
+ if(!%this.built || %this.busy || !isObject(%this.previewSprite) || !isObject(%this.imageAsset))
+ {
+ return false;
+ }
+
+ %this.previewSprite.setSize(%this.admin.assetWindow.getWorldSize(%this.imageAsset.getFrameSize(0)));
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+// Editing. Every path that changes the list ends here, and this is the only
+// place in the editor that writes the animation's frames.
+//-----------------------------------------------------------------------------
+
+function AssetAnimationStage::commitFrames(%this)
+{
+ if(!%this.built || !isObject(%this.animationAsset) || !isObject(%this.timelinePane))
+ {
+ return;
+ }
+
+ %named = %this.namedMode();
+
+ // How many frames there were, asked of the ASSET rather than of the strip:
+ // the strip already holds the edited list by the time it reports, so it can
+ // no longer say what the animation used to be.
+ //
+ // getFrameCount rather than getAnimationFrameCount, because that one refuses
+ // to answer in named mode and returns -1 -- which reads as "fewer than one"
+ // to keepFrameRate below, and would have silently switched that feature off
+ // for every named animation.
+ %before = %this.animationAsset.getFrameCount();
+
+ // Where the preview is, captured BEFORE the write, because the engine
+ // restarts playback in the middle of it: AssetManager::refreshAsset notifies
+ // every AssetPtr pointing at the asset -- which for a sprite means
+ // playAnimation, from slot zero -- and only then fires the script onRefresh
+ // this editor listens on. By the time we are asked to put things back, the
+ // sprite has already forgotten where it was.
+ %this.resumeSlot = isObject(%this.previewSprite) ? %this.previewSprite.getAnimationFrame() : -1;
+
+ // One transaction around both writes. Keep Frame Rate makes this path change
+ // the asset twice for a single thing the user did, and two undo steps for one
+ // dropped frame is two presses of undo to put it back.
+ AssetAdmin.undoRecorder.begin("Set Frames");
+
+ // Guarded because the change comes straight back: every asset setter ends in
+ // refreshAsset, which announces the change and fires onRefresh synchronously,
+ // inside this call.
+ //
+ // The list is asked of the strip in whichever space the asset keeps it. Both
+ // are always available -- the strip fills its index list and its name list
+ // together, whichever one it was given -- so this is a choice of which to hand
+ // over, not a conversion.
+ %this.committing = true;
+ if(%named)
+ {
+ %this.animationAsset.setNamedAnimationFrames(%this.timelinePane.strip.getNamedFrames());
+ }
+ else
+ {
+ %this.animationAsset.setAnimationFrames(%this.timelinePane.strip.getFrames());
+ }
+ %this.committing = false;
+
+ // Before the slot is forgotten, because this changes the asset a second time
+ // and every change restarts playback. Cleared only once BOTH are done, so the
+ // one remembered slot covers the pair -- forgetting it in between left the
+ // second refresh with nothing to restore, and the preview back at frame zero
+ // after every edit.
+ %this.keepFrameRate(%before);
+
+ AssetAdmin.undoRecorder.end();
+
+ %this.resumeSlot = -1;
+}
+
+// Hold the per-frame rate steady across an edit, when the user has asked for it.
+//
+// Uniform timing means AnimationTime is shared out over however many frames there
+// are, so adding one makes every frame play faster and the animation no longer
+// lasts as long. Which of those two a person wants is genuinely a matter of what
+// they are doing -- lengthening a walk cycle, or dropping in a hold without
+// speeding everything up -- so it is a switch, off by default, and the info line
+// in the inspector always shows both numbers either way.
+function AssetAnimationStage::keepFrameRate(%this, %beforeCount)
+{
+ %afterCount = %this.timelinePane.strip.getCellCount();
+
+ if(%beforeCount < 1 || %afterCount == %beforeCount)
+ {
+ return;
+ }
+
+ if(!EditorPreferences.get("assetAnimationKeepFrameRate", false))
+ {
+ return;
+ }
+
+ %time = %this.animationAsset.getAnimationTime() * (%afterCount / %beforeCount);
+
+ // Clamped by hand rather than with mClamp, which rounds to a whole number and
+ // would turn every animation shorter than a second into no time at all. The
+ // floor matters: a time of zero divides by zero in ImageFrameProviderCore, so
+ // nothing may ever write one.
+ if(%time < 0.01) { %time = 0.01; }
+ if(%time > 3600) { %time = 3600; }
+
+ %this.committing = true;
+ %this.animationAsset.setAnimationTime(%time);
+ %this.committing = false;
+}
+
+// The inspector wrote something. Most of its fields do not concern the stage --
+// the refresh they raise is absorbed like any other -- but changing the image
+// asset moves what every frame number means, so the palette and the timeline
+// both have to be re-pointed at it.
+function AssetAnimationStage::onInspectorCommit(%this)
+{
+ if(!%this.built || %this.busy || !isObject(%this.animationAsset))
+ {
+ return;
+ }
+
+ %imageAssetId = %this.animationAsset.getImage();
+ if(%imageAssetId $= %this.imageAssetId)
+ {
+ return;
+ }
+
+ %this.imageAssetId = %imageAssetId;
+ %this.palettePane.load(%imageAssetId);
+ %this.loadTimeline();
+
+ %this.admin.transportBar.refresh();
+}
+
+function AssetAnimationStage::setCycle(%this, %on)
+{
+ if(!isObject(%this.animationAsset))
+ {
+ return;
+ }
+
+ // Writes the file, like every other asset edit. The write comes back through
+ // absorbRefresh, which re-arms the preview; there is nothing else to do.
+ %this.animationAsset.setAnimationCycle(%on);
+}
+
+function AssetAnimationStage::appendFrame(%this, %frame)
+{
+ if(!%this.built)
+ {
+ return;
+ }
+
+ %this.timelinePane.appendFrame(%frame);
+}
+
+function AssetAnimationStage::setFrames(%this, %frames)
+{
+ if(!%this.built)
+ {
+ return;
+ }
+
+ %this.timelinePane.setFrames(%frames);
+}
+
+function AssetAnimationStage::appendFrames(%this, %frames)
+{
+ if(!%this.built)
+ {
+ return;
+ }
+
+ %this.timelinePane.appendFrames(%frames);
+}
+
+function AssetAnimationStage::openRangeDialog(%this)
+{
+ if(!%this.built)
+ {
+ return;
+ }
+
+ // Six fields over three rows is 150, the answer line is 90 with room to grow
+ // into, and the buttons want 34 and a margin at the bottom. Plus the 34 the
+ // title bar and border take out of the window before the content sees any of
+ // it.
+ %width = 460;
+ %height = 340;
+
+ %dialog = new GuiControl()
+ {
+ class = "AssetAnimationRangeDialog";
+ superclass = "EditorDialog";
+ dialogSize = (%width + 8) SPC (%height + 8);
+ dialogCanClose = true;
+ dialogResizable = false;
+ dialogText = "Frame Range";
+ stage = %this;
+ };
+ %dialog.init(%width, %height);
+
+ Canvas.pushDialog(%dialog);
+}
diff --git a/editor/AssetAdmin/Animation/AssetAnimationTimelinePane.cs b/editor/AssetAdmin/Animation/AssetAnimationTimelinePane.cs
new file mode 100644
index 000000000..5da3faf5d
--- /dev/null
+++ b/editor/AssetAdmin/Animation/AssetAnimationTimelinePane.cs
@@ -0,0 +1,250 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2013 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+
+//-----------------------------------------------------------------------------
+// The bottom pane of the animation editor: the frames the animation plays, in
+// order, and the target of every drag out of the palette.
+//
+// The DROP TARGET is this pane rather than the grid inside it, on purpose. The
+// grid is only as wide as its cells, so with four frames in a wide pane most of
+// what looks like the timeline is not the grid at all -- and a frame let go over
+// that empty space obviously means "put it at the end", not "nowhere".
+//-----------------------------------------------------------------------------
+
+$AssetAnimationTimelinePane::captionHeight = 20;
+
+function AssetAnimationTimelinePane::onAdd(%this)
+{
+ ThemeManager.setProfile(%this, "emptyProfile");
+
+ %this.caption = new GuiControl()
+ {
+ HorizSizing = "width";
+ VertSizing = "bottom";
+ Position = "0 0";
+ Extent = "100" SPC $AssetAnimationTimelinePane::captionHeight;
+ Text = "Timeline";
+ };
+ // panelProfile for its color5 font, as the palette's caption is and the Asset
+ // Inspector's title bar is. See the note there.
+ ThemeManager.setProfile(%this.caption, "panelProfile");
+ %this.add(%this.caption);
+
+ // "height", not "fill": fill would put the scroller over the caption.
+ %this.scroller = new GuiScrollCtrl()
+ {
+ HorizSizing = "width";
+ VertSizing = "height";
+ Position = "0" SPC $AssetAnimationTimelinePane::captionHeight;
+ Extent = "100 80";
+ hScrollBar = "dynamic";
+ vScrollBar = "alwaysOff";
+ constantThumbHeight = false;
+ scrollBarThickness = 14;
+ showArrowButtons = true;
+ };
+ ThemeManager.setProfile(%this.scroller, "scrollingPanelProfile");
+ ThemeManager.setProfile(%this.scroller, "scrollingPanelThumbProfile", "ThumbProfile");
+ ThemeManager.setProfile(%this.scroller, "scrollingPanelTrackProfile", "TrackProfile");
+ ThemeManager.setProfile(%this.scroller, "scrollingPanelArrowProfile", "ArrowProfile");
+ %this.add(%this.scroller);
+
+ // The mirror of the palette: "fill" down the axis whose bar is alwaysOff, so
+ // the row is as tall as the scroller, and "right" across, where the strip
+ // sets its own width from its cells and the bar scrolls it.
+ %this.strip = new GuiEditFrameTimelineCtrl()
+ {
+ pane = %this;
+ HorizSizing = "right";
+ VertSizing = "fill";
+ Position = "0 0";
+ Extent = "100 100";
+ CellSize = 48;
+ CellPad = 4;
+ ShowFrameNumbers = true;
+ };
+ // frameGridProfile, not listBoxProfile: the grids read six of its colors for
+ // things a list has no equivalent of, and BaseTheme documents which is which.
+ // It also carries the canKeyFocus the Delete key depends on.
+ ThemeManager.setProfile(%this.strip, "frameGridProfile");
+ %this.scroller.add(%this.strip);
+}
+
+// The image FIRST in both of these, and that order is load bearing.
+//
+// The strip fills its index list and its name list together, and it can only do
+// that by asking the image what cell N is called or which cell is called N. Given
+// the frames before the image, every name resolves to nothing.
+function AssetAnimationTimelinePane::load(%this, %imageAssetId, %frames)
+{
+ %this.strip.setImageAsset(%imageAssetId);
+ %this.strip.setFrames(%frames);
+ %this.refreshCaption();
+}
+
+function AssetAnimationTimelinePane::loadNamed(%this, %imageAssetId, %names)
+{
+ %this.strip.setImageAsset(%imageAssetId);
+ %this.strip.setNamedFrames(%names);
+ %this.refreshCaption();
+}
+
+function AssetAnimationTimelinePane::setPreviewSprite(%this, %sprite)
+{
+ %this.strip.setPreviewSprite(%sprite);
+}
+
+function AssetAnimationTimelinePane::refreshCaption(%this)
+{
+ %count = %this.strip.getCellCount();
+ if(%count == 0)
+ {
+ %this.caption.setText("Timeline - empty");
+ return;
+ }
+
+ %this.caption.setText("Timeline -" SPC %count SPC (%count == 1 ? "frame" : "frames"));
+}
+
+//-----------------------------------------------------------------------------
+// Everything that changes the list ends up in commitFrames, and only here. The
+// grid reports that it changed; deciding what that means to the asset is this
+// pane's job, and writing it is the stage's.
+//-----------------------------------------------------------------------------
+
+// The list is no longer handed over here. The stage reads it off the strip in
+// whichever space the asset keeps its frames, and only the stage knows which that
+// is -- passing indices from here meant a named animation was committed as a row
+// of numbers to a setter that refuses them.
+function AssetAnimationTimelinePane::commitFrames(%this)
+{
+ %this.refreshCaption();
+ %this.stage.commitFrames();
+}
+
+function AssetAnimationTimelinePane::appendFrame(%this, %frame)
+{
+ %this.strip.insertFrame(%this.strip.getCellCount(), %frame);
+ %this.commitFrames();
+}
+
+function AssetAnimationTimelinePane::setFrames(%this, %frames)
+{
+ %this.strip.setFrames(%frames);
+ %this.commitFrames();
+}
+
+// Inserted one at a time rather than concatenated onto getFrames() and set back.
+//
+// The round trip through the index list was lossy once frames could be missing: a
+// frame whose cell has been deleted is index -1, and rebuilding the list from
+// indices would have turned every such frame into the same nameless hole. Adding
+// to the end touches nothing that is already there.
+function AssetAnimationTimelinePane::appendFrames(%this, %frames)
+{
+ %count = getWordCount(%frames);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %this.strip.insertFrame(%this.strip.getCellCount(), getWord(%frames, %i));
+ }
+
+ %this.commitFrames();
+}
+
+//-----------------------------------------------------------------------------
+// The drop, and the two traps in GuiDragAndDropCtrl that shape all of it.
+//
+// findDragTarget hit-tests from the drag control's PARENT, and
+// GuiControl::findHitControl ends in a bare "return this" without ever testing
+// its own bounds -- so a drop anywhere on the screen arrives here, over the
+// library, over the menu bar, over the inspector. The target has to police its
+// own boundary; nobody else will.
+//
+// And %position is not where the cursor is. GuiDragAndDropCtrl::sendDragEvent
+// builds it from the drag control's own bounds, which are local to that outer
+// frame set. So nothing here measures with it -- the payload is asked instead,
+// which a drag grabbed by the middle answers exactly.
+//-----------------------------------------------------------------------------
+
+function AssetAnimationTimelinePane::onControlDragged(%this, %payload, %position)
+{
+ %cursor = %this.cursorFrom(%payload);
+ if(!%this.isOverStrip(%cursor))
+ {
+ %this.strip.clearCaret();
+ return;
+ }
+
+ %this.strip.showCaretAt(%cursor);
+}
+
+function AssetAnimationTimelinePane::onControlDragExit(%this, %payload, %position)
+{
+ %this.strip.clearCaret();
+}
+
+function AssetAnimationTimelinePane::onControlDropped(%this, %payload, %position)
+{
+ %this.strip.clearCaret();
+
+ %cursor = %this.cursorFrom(%payload);
+ if(!%this.isOverStrip(%cursor))
+ {
+ return;
+ }
+
+ // Deliberately no commitFrames() here. insertFrameAtPoint announces the change
+ // itself -- notifyFramesChanged fires onFramesChanged, which comes straight
+ // back to this pane's commitFrames -- so committing again wrote the asset
+ // twice for one dropped frame. That was invisible while a change just rewrote
+ // the same file; with undo it is a step that puts nothing back, so a dropped
+ // frame took two presses of undo to remove and left a dead redo behind it.
+ //
+ // insertFrame (the plain one the palette click uses) does NOT announce, which
+ // is why appendFrame above still has to.
+ %this.strip.insertFrameAtPoint(%cursor, %payload.frameIndex);
+}
+
+// The middle of the payload, which is where the cursor is holding it.
+function AssetAnimationTimelinePane::cursorFrom(%this, %payload)
+{
+ %at = %payload.getGlobalPosition();
+ %size = %payload.getExtent();
+
+ return mFloor(getWord(%at, 0) + (getWord(%size, 0) / 2)) SPC
+ mFloor(getWord(%at, 1) + (getWord(%size, 1) / 2));
+}
+
+// Measured against the SCROLLER, not the grid. The grid stops at its last cell;
+// the scroller is the whole timeline as far as anyone looking at it is concerned.
+function AssetAnimationTimelinePane::isOverStrip(%this, %point)
+{
+ %at = %this.scroller.getGlobalPosition();
+ %size = %this.scroller.getExtent();
+
+ %x = getWord(%point, 0);
+ %y = getWord(%point, 1);
+
+ return %x >= getWord(%at, 0) && %y >= getWord(%at, 1) &&
+ %x < getWord(%at, 0) + getWord(%size, 0) &&
+ %y < getWord(%at, 1) + getWord(%size, 1);
+}
diff --git a/editor/AssetAdmin/Animation/AssetAnimationTransportBar.cs b/editor/AssetAdmin/Animation/AssetAnimationTransportBar.cs
new file mode 100644
index 000000000..0d5a3db3d
--- /dev/null
+++ b/editor/AssetAdmin/Animation/AssetAnimationTransportBar.cs
@@ -0,0 +1,134 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2013 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+
+//-----------------------------------------------------------------------------
+// Play, rewind, loop, and the two switches that decide what an edit does: the
+// bar over the animation preview.
+//
+// The chrome -- the sized buttons, the toggles, the gaps -- is EditorTransportBar
+// in EditorCore, shared with the particle preview's bar. What is left here is
+// what these particular buttons do.
+//
+// Play and Stop are two buttons with one hidden rather than one toggle, and the
+// difference is not cosmetic. A toggle says "this setting is on"; these two say
+// "here is what will happen if you press me", which is a different promise and
+// the one a transport makes. It also means the button cannot get stuck showing
+// Stop after something else halted the preview -- there is no state to fall out
+// of step, only whichever button is currently on show.
+//
+// Order reads left to right as rewind, then the big play, then a gap, then the
+// three that are settings rather than actions.
+//-----------------------------------------------------------------------------
+
+function AssetAnimationTransportBar::onAdd(%this)
+{
+ %this.init();
+
+ %this.addButton("rewind", $EditorIcon::playback_rew, "Back to the first frame",
+ $EditorTransportBar::buttonSize);
+
+ // The one you reach for, so it is half again the size of the rest. They sit
+ // in the same place, and exactly one of them is ever visible.
+ %this.playButton = %this.addButton("play", $EditorIcon::playback_play, "Play the preview",
+ $EditorTransportBar::playSize);
+ %this.stopButton = %this.addButton("stop", $EditorIcon::playback_stop, "Stop the preview",
+ $EditorTransportBar::playSize);
+ %this.stopButton.setVisible(false);
+
+ %this.addSpacer($EditorTransportBar::gap);
+
+ %this.loopButton = %this.addToggle("Loop", $EditorIcon::playback_reload, $EditorIcon::playback_reload,
+ "Looping. Click to play once and stop on the last frame.",
+ "Playing once. Click to loop.");
+
+ %this.rateButton = %this.addToggle("KeepRate", $EditorIcon::stop_watch, $EditorIcon::stop_watch,
+ "Keeping the frame rate: adding or removing frames rewrites the animation's time to match.",
+ "Keeping the animation's time: adding a frame makes every frame play faster.");
+
+ %this.addButton("openRangeDialog", $EditorIcon::list_num, "Fill the timeline from a range of frames",
+ $EditorTransportBar::buttonSize);
+}
+
+//-----------------------------------------------------------------------------
+// One handler, switching on which toggle spoke.
+//-----------------------------------------------------------------------------
+
+function AssetAnimationTransportBar::onToggleIconChanged(%this, %button)
+{
+ switch$(%button.toggleName)
+ {
+ case "Loop":
+ %this.stage.setCycle(%button.getValue());
+
+ case "KeepRate":
+ // An editor preference, not a property of the asset: it decides what
+ // the editor does on the user's behalf, and the user who wants it
+ // wants it next time too.
+ EditorPreferences.set("assetAnimationKeepFrameRate", %button.getValue());
+ }
+}
+
+function AssetAnimationTransportBar::play(%this)
+{
+ %this.stage.play();
+}
+
+function AssetAnimationTransportBar::stop(%this)
+{
+ %this.stage.stop();
+}
+
+function AssetAnimationTransportBar::rewind(%this)
+{
+ // The playing state is left alone on purpose. Rewinding while it plays
+ // restarts the run, which is what a rewind is.
+ %this.stage.scrubTo(0);
+}
+
+function AssetAnimationTransportBar::openRangeDialog(%this)
+{
+ %this.stage.openRangeDialog();
+}
+
+//-----------------------------------------------------------------------------
+// Reading the state back out. Called whenever something else may have moved it.
+//-----------------------------------------------------------------------------
+
+// Called from every path that can change the playing state, and there are more
+// of them than the two buttons: clicking a slot stops to scrub, dragging a frame
+// out stops, and a one-shot animation stops itself by reaching the end. Each of
+// those used to leave a Stop button on show over a preview that had stopped.
+function AssetAnimationTransportBar::refresh(%this)
+{
+ %playing = %this.stage.playing;
+ %this.playButton.setVisible(!%playing);
+ %this.stopButton.setVisible(%playing);
+ %this.relayout();
+
+ if(!isObject(%this.stage.animationAsset))
+ {
+ return;
+ }
+
+ %this.loopButton.setValue(%this.stage.animationAsset.getAnimationCycle());
+ %this.rateButton.setValue(EditorPreferences.get("assetAnimationKeepFrameRate", false));
+}
diff --git a/editor/AssetAdmin/Animation/GuiEditFramePaletteCtrl.cs b/editor/AssetAdmin/Animation/GuiEditFramePaletteCtrl.cs
new file mode 100644
index 000000000..94ead6d1c
--- /dev/null
+++ b/editor/AssetAdmin/Animation/GuiEditFramePaletteCtrl.cs
@@ -0,0 +1,148 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2013 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+
+//-----------------------------------------------------------------------------
+// The script half of the frame palette: what a click means, and what a frame
+// looks like while it is being dragged.
+//
+// Note there is no class= on the control this belongs to, and there must not be:
+// the C++ class owns this namespace. Setting class to the same name makes
+// Namespace::classLinkTo log "cannot change namespace parent linkage" every time
+// the editor opens. The owning pane arrives as an ordinary field, %this.pane.
+//-----------------------------------------------------------------------------
+
+// How big the thing under the cursor is while a frame is in flight. Bigger than a
+// palette cell on purpose: it is being carried, and it has to stay findable
+// against the preview art behind it.
+$GuiEditFramePaletteCtrl::payloadSize = 56;
+
+//-----------------------------------------------------------------------------
+// A click is a drop that never moved.
+//
+// It routes to the same appendFrame the drop path ends in rather than adding the
+// frame itself. Two ways into the list would each have to remember to commit, to
+// re-arm the preview and to keep the selection honest, and the second one would
+// go stale the first time any of that changed.
+//-----------------------------------------------------------------------------
+
+function GuiEditFramePaletteCtrl::onFrameClicked(%this, %frame)
+{
+ if(!isObject(%this.pane) || !isObject(%this.pane.stage))
+ {
+ return;
+ }
+
+ %this.pane.stage.appendFrame(%frame);
+}
+
+//-----------------------------------------------------------------------------
+// The drag. The control tells us it has begun and hands over the frame; making
+// the payload and deciding where it may be dropped are the editor's business,
+// not the grid's.
+//-----------------------------------------------------------------------------
+
+function GuiEditFramePaletteCtrl::onFrameDragBegan(%this, %frame, %x, %y)
+{
+ if(!isObject(%this.pane) || !isObject(%this.pane.stage))
+ {
+ return;
+ }
+
+ %payload = %this.makePayload(%frame);
+ if(!isObject(%payload))
+ {
+ return;
+ }
+
+ // The drag control lives in the outer frame set, for two reasons. It has to
+ // be an ancestor of the timeline, because GuiDragAndDropCtrl::findDragTarget
+ // hit-tests from its own PARENT downwards; and the payload has to be able to
+ // travel over the whole page, which a control parented into the palette could
+ // not do.
+ %host = AssetAdmin.content;
+
+ // Position is relative to that parent, so the drag control's own offset has
+ // to come out of the cursor position or the payload jumps on the first frame.
+ %hostAt = %host.getGlobalPosition();
+ %xOffset = (getWord(%payload.extent, 0) / 2) + getWord(%hostAt, 0);
+ %yOffset = (getWord(%payload.extent, 1) / 2) + getWord(%hostAt, 1);
+
+ %dragCtrl = new GuiDragAndDropCtrl()
+ {
+ canSaveDynamicFields = "0";
+ Profile = "GuiDragAndDropProfile";
+ HorizSizing = "anchorLeft";
+ VertSizing = "anchorTop";
+ Position = (%x - %xOffset) SPC (%y - %yOffset);
+ Extent = %payload.extent;
+ MinExtent = "16 16";
+ Visible = "1";
+ deleteOnMouseUp = true;
+ };
+
+ %dragCtrl.add(%payload);
+ %host.add(%dragCtrl);
+
+ // Again, now that it is on the canvas and awake. The fields above are what
+ // onWake reads, and this is what makes the frame right whichever order the
+ // waking happens in -- setImageFrame on an awake control is unambiguous.
+ %payload.setImageFrame(%frame);
+
+ // Grabbed by the middle, which is what lets the drop target work out where
+ // the cursor is from the payload alone -- the position the drop callback is
+ // handed is in the drag control's parent's space and cannot be used.
+ %dragCtrl.startDragging(%xOffset, %yOffset);
+}
+
+function GuiEditFramePaletteCtrl::makePayload(%this, %frame)
+{
+ %size = $GuiEditFramePaletteCtrl::payloadSize;
+
+ %payload = new GuiSpriteCtrl()
+ {
+ canSaveDynamicFields = "0";
+ Position = "0 0";
+ Extent = %size SPC %size;
+ imageColor = "255 255 255 255";
+ singleFrameBitmap = "0";
+ tileImage = "0";
+ fullSize = "1";
+ constrainProportions = "1";
+
+ // The FIELDS, not setImage(). A payload is built detached, and
+ // GuiSpriteCtrl::setImage returns early when the control is not awake --
+ // it records the asset id and drops the frame on the floor. Then onWake
+ // re-applies the image from mImageAssetId and mFrame, so whatever the
+ // Frame field says is what actually gets shown. Setting the image the
+ // obvious way left every dragged frame showing frame 0, right up until it
+ // was dropped and the correct one went in.
+ Image = %this.getImageAsset();
+ Frame = %frame;
+
+ // What the drop reads back. The payload IS the message.
+ frameIndex = %frame;
+ };
+ ThemeManager.setProfile(%payload, "emptyProfile");
+
+ return %payload;
+}
+
diff --git a/editor/AssetAdmin/Animation/GuiEditFrameTimelineCtrl.cs b/editor/AssetAdmin/Animation/GuiEditFrameTimelineCtrl.cs
new file mode 100644
index 000000000..675806d74
--- /dev/null
+++ b/editor/AssetAdmin/Animation/GuiEditFrameTimelineCtrl.cs
@@ -0,0 +1,53 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2013 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+
+//-----------------------------------------------------------------------------
+// The script half of the timeline grid: what a changed list and a picked slot
+// mean to the rest of the editor.
+//
+// As with the palette, there is no class= on the control -- the C++ class owns
+// this namespace -- and the owning pane arrives as %this.pane.
+//
+// Both handlers are deliberately thin. The grid has already done the editing by
+// the time it says anything, exactly once per completed gesture; all that is
+// left is to tell the asset and the preview.
+//-----------------------------------------------------------------------------
+
+function GuiEditFrameTimelineCtrl::onFramesChanged(%this)
+{
+ if(!isObject(%this.pane))
+ {
+ return;
+ }
+
+ %this.pane.commitFrames();
+}
+
+function GuiEditFrameTimelineCtrl::onSlotSelected(%this, %slot, %frame)
+{
+ if(!isObject(%this.pane) || !isObject(%this.pane.stage))
+ {
+ return;
+ }
+
+ %this.pane.stage.onSlotSelected(%slot, %frame);
+}
diff --git a/engine/compilers/VisualStudio 2019/main.cs b/editor/AssetAdmin/Animation/exec.cs
similarity index 80%
rename from engine/compilers/VisualStudio 2019/main.cs
rename to editor/AssetAdmin/Animation/exec.cs
index fafa27258..9f3fcb14d 100644
--- a/engine/compilers/VisualStudio 2019/main.cs
+++ b/editor/AssetAdmin/Animation/exec.cs
@@ -20,8 +20,11 @@
// IN THE SOFTWARE.
//-----------------------------------------------------------------------------
-// This file simply points to the real main.cs file in the root of the working directory.
-// This is needed if the project is run in debug mode from within VisualStudio.
-setMainDotCsDir(makeFullPath("../../../"));
-setCurrentDirectory(makeFullPath("./"));
-exec("../../../main.cs");
+exec("./AssetAnimationStage.cs");
+exec("./AssetAnimationPalettePane.cs");
+exec("./AssetAnimationTimelinePane.cs");
+exec("./AssetAnimationTransportBar.cs");
+exec("./AssetAnimationFrameRange.cs");
+exec("./AssetAnimationRangeDialog.cs");
+exec("./GuiEditFramePaletteCtrl.cs");
+exec("./GuiEditFrameTimelineCtrl.cs");
diff --git a/editor/AssetAdmin/AssetAdmin.cs b/editor/AssetAdmin/AssetAdmin.cs
index 125315ae8..614741721 100644
--- a/editor/AssetAdmin/AssetAdmin.cs
+++ b/editor/AssetAdmin/AssetAdmin.cs
@@ -22,9 +22,12 @@
function AssetAdmin::create(%this)
{
+ exec("./AssetLibraryWindow.cs");
exec("./AssetDictionary.cs");
exec("./AssetWindow.cs");
exec("./AssetDictionaryButton.cs");
+ exec("./AssetDictionarySprite.cs");
+ exec("./AssetBase.cs");
exec("./AssetInspector.cs");
exec("./AssetAudioPlayButton.cs");
exec("./NewAssetButton.cs");
@@ -36,6 +39,20 @@
exec("./DeleteAssetDialog.cs");
exec("./ParticleEditor/exec.cs");
exec("./ImageEditor/exec.cs");
+ exec("./Inspector/exec.cs");
+ exec("./Animation/exec.cs");
+ exec("./AssetPreviewSprite.cs");
+ exec("./AssetUndoRecorder.cs");
+ exec("./DuplicateAssetDialog.cs");
+ exec("./AssetAdminConfirmSaveDialog.cs");
+
+ // File and Edit, which this editor owns and lends to the shared bar for as
+ // long as it is the one open.
+ exec("./AssetAdminMenus.cs");
+
+ // Undo, redo and the record of what is unsaved. Built before the inspector,
+ // which asks it what to grey out as soon as it has a document bar.
+ %this.undoRecorder = new ScriptObject() { class = "AssetUndoRecorder"; };
%this.guiPage = EditorCore.RegisterEditor("Asset Manager", %this);
%this.content = %this.createFrameSet();
@@ -44,6 +61,33 @@
%this.buildInspector();
%this.buildLibrary();
+ // The manager that turns the preview into an animation editor and back. It
+ // owns the two panes it builds and deletes them in its own onRemove, so
+ // deleting it is the whole of the teardown.
+ %this.animationStage = new ScriptObject()
+ {
+ class = "AssetAnimationStage";
+ admin = %this;
+ };
+ // The range arithmetic, kept as an object so the dialog and the tests share
+ // one handle and no new global function appears.
+ %this.frameRange = new ScriptObject() { class = "AssetAnimationFrameRange"; };
+
+ %this.buildTransportBar();
+
+ // After the inspector, whose title dropdown the solo and mute switches ask
+ // which emitter is selected.
+ %this.buildParticleTransportBar();
+
+ // After the inspector, which its refresh asks what to grey out. Built into
+ // the shared bar and taken straight back off again; open() puts it on.
+ %this.menus = new ScriptObject()
+ {
+ class = "AssetAdminMenus";
+ superclass = "EditorMenuSet";
+ tool = %this;
+ };
+
EditorCore.FinishRegistration(%this.guiPage);
%this.isOpen = false;
@@ -70,20 +114,32 @@
%rightID = getWord(%idList, 1);
%content.anchorFrame(%rightID);
%content.setFrameSize(%rightID, 324);
-
+
%ids = %content.createVerticalSplit(%leftID);
%centerFrameID = getWord(%ids, 0);
%inspectorFrameID = getWord(%ids, 1);
%content.anchorFrame(%inspectorFrameID);
%content.setFrameSize(%inspectorFrameID, 360);
+ // Kept because the only way to move a divider from script is to name the
+ // frame, and the ids are handed out once here and never again. The inspector
+ // is the bottom frame, so it opens wide and short -- which is why everything
+ // in it reflows.
+ %this.libraryFrameId = %rightID;
+ %this.previewFrameId = %centerFrameID;
+ %this.inspectorFrameId = %inspectorFrameID;
+
return %content;
}
+// Everything inside the library -- the toolbar, the scroller, the chain and the
+// groups -- belongs to AssetLibraryWindow, which builds it in its own onAdd. All
+// this has to decide is where the window goes.
function AssetAdmin::buildLibrary(%this)
{
%this.libWindow = new GuiWindowCtrl()
{
+ Class = "AssetLibraryWindow";
HorizSizing = "right";
VertSizing = "bottom";
Position = "0 0";
@@ -104,62 +160,10 @@
ThemeManager.setProfile(%this.libWindow, "windowButtonProfile", "MaxButtonProfile");
%this.content.add(%this.libWindow);
- %this.libScroller = new GuiScrollCtrl()
- {
- HorizSizing = "width";
- VertSizing = "height";
- Position="0 0";
- Extent="324 356";
- MinExtent="0 0";
- hScrollBar="dynamic";
- vScrollBar="alwaysOn";
- constantThumbHeight="0";
- showArrowButtons="1";
- scrollBarThickness="14";
- };
- ThemeManager.setProfile(%this.libScroller, "scrollingPanelProfile");
- ThemeManager.setProfile(%this.libScroller, "scrollingPanelThumbProfile", ThumbProfile);
- ThemeManager.setProfile(%this.libScroller, "scrollingPanelTrackProfile", TrackProfile);
- ThemeManager.setProfile(%this.libScroller, "scrollingPanelArrowProfile", ArrowProfile);
- %this.libWindow.add(%this.libScroller);
-
- %this.dictionaryList = new GuiChainCtrl()
- {
- HorizSizing="width";
- VertSizing="height";
- Position="0 0";
- Extent="310 768";
- MinExtent="220 200";
- };
- ThemeManager.setProfile(%this.dictionaryList, "emptyProfile");
- %this.libScroller.add(%this.dictionaryList);
-
- %this.dictionaryList.add(%this.buildDictionary("Images", "ImageAsset"));
- %this.dictionaryList.add(%this.buildDictionary("Animations", "AnimationAsset"));
- %this.dictionaryList.add(%this.buildDictionary("Particle Effects", "ParticleAsset"));
- %this.dictionaryList.add(%this.buildDictionary("Fonts", "FontAsset"));
- %this.dictionaryList.add(%this.buildDictionary("Audio", "AudioAsset"));
- //%this.dictionaryList.add(%this.buildDictionary("Spines", "SpineAsset"));
-}
-
-function AssetAdmin::buildDictionary(%this, %title, %type)
-{
- %this.Dictionary[%type] = new GuiPanelCtrl()
- {
- Class = AssetDictionary;
- Text=%title;
- command="";
- HorizSizing="width";
- VertSizing="bottom";
- Position="0 0";
- Extent="306 22";
- MinExtent="80 22";
- Type = %type;
- };
- %this.Dictionary[%type].setExpandEase("EaseInOut", 1000);
- ThemeManager.setProfile(%this.Dictionary[%type], "panelProfile");
-
- return %this.Dictionary[%type];
+ // Measure again. The window sized its own contents in onAdd, which is before
+ // any of the five profiles above were on it and before the frame set gave it
+ // its real extent -- so that pass was against GuiDefaultProfile's title bar.
+ %this.libWindow.fitScroller();
}
function AssetAdmin::buildInspector(%this)
@@ -170,7 +174,14 @@
VertSizing = "bottom";
text = "Asset Inspector";
Extent = "706 380";
- MinExtent = "500 250";
+
+ // Narrow enough to hold the cell table and its scroll bar, and no
+ // narrower. A frame set moves its divider whatever the window in the frame
+ // thinks, so a minimum the user can drag past is not a floor -- it is 106
+ // pixels of window hanging off the right-hand edge, clipped away, taking
+ // the Find button and a column of settings with them. The pane reflows all
+ // the way down to one column, so there is nothing here to protect.
+ MinExtent = "260 200";
canMove = true;
canClose = false;
canMinimize = true;
@@ -199,6 +210,45 @@ class = "AssetInspector";
function AssetAdmin::buildAssetWindow(%this)
{
+ // Two layers between the frame and the preview, and each earns its place.
+ //
+ // previewFrames is a frame set so the preview can be split three ways for an
+ // animation -- the art, the frames available, and the timeline -- with
+ // dividers the user can drag. Unsplit it is a pass-through: GuiFrameSetCtrl
+ // resizes its one frame to its own extent with no insets, so every other
+ // asset type sees exactly what it saw before. Splitting it later never
+ // reparents anything either, because splitFrame only rewrites which frame
+ // holds a control, and removing one collapses the frame and hoists its twin.
+ //
+ // previewHost looks like a pointless wrapper and is not. GuiWindowCtrl finds
+ // its dock target as a cast of its parent's FIRST child to GuiFrameSetCtrl.
+ // Today that child is the background sprite, the cast fails, and docking is
+ // quietly off in the Asset Manager. Put the frame set there instead and
+ // docking switches itself on, aimed at the animation split -- so the Asset
+ // Inspector window would offer to dock into frames the stage later deletes
+ // out from under it. One plain control in between keeps that answer "no".
+ %this.previewHost = new GuiControl()
+ {
+ HorizSizing = "right";
+ VertSizing = "bottom";
+ Position = "0 0";
+ Extent = "100 100";
+ };
+ ThemeManager.setProfile(%this.previewHost, "emptyProfile");
+ %this.content.add(%this.previewHost);
+
+ %this.previewFrames = new GuiFrameSetCtrl()
+ {
+ HorizSizing = "width";
+ VertSizing = "height";
+ Position = "0 0";
+ Extent = "100 100";
+ DividerThickness = 6;
+ };
+ ThemeManager.setProfile(%this.previewFrames, "frameSetProfile");
+ ThemeManager.setProfile(%this.previewFrames, "dropButtonProfile", "dropButtonProfile");
+ %this.previewHost.add(%this.previewFrames);
+
%this.background = new GuiSpriteCtrl() {
HorizSizing = "right";
VertSizing = "bottom";
@@ -214,7 +264,7 @@ class = "AssetInspector";
constrainProportions = "1";
};
ThemeManager.setProfile(%this.background, "emptyProfile");
- %this.content.add(%this.background);
+ %this.previewFrames.add(%this.background);
%this.assetScene = new Scene();
%this.assetScene.setScenePause(true);
@@ -248,6 +298,47 @@ class = AssetWindow;
%this.background.add(%this.assetWindow);
}
+// Start the audio driver only if nothing else already has.
+//
+// The game owns audio. A project's Audio module calls OpenALInitDriver in its
+// create function and then sets the master and channel volumes, and that is the
+// arrangement -- nothing here should replace it, and nothing in a shipped game
+// may depend on the editor being present at all.
+//
+// But the Asset Manager can be opened before any project is picked, or against a
+// project with no audio module, and there the driver is simply not running: with
+// no context alxPlay answers with a null handle, and .wav is not even a
+// registered resource extension until OpenALInitDriver registers it. So this is a
+// fallback for that case and nothing more.
+//
+// Asking first is essential rather than tidy. OpenALInit BEGINS by calling
+// OpenALShutdown, so calling it when the driver is already up tears down the one
+// the game just built: every playing sound stops and every channel volume goes
+// back to 1, silently undoing the project's own audio settings.
+//
+// On demand rather than at editor startup, because an editor that takes the sound
+// card the moment it opens is a nuisance, and most of a session never plays
+// anything. Never shut down again either, for the same reason -- it is not ours
+// to stop.
+//
+// The attempt is remembered rather than the result, so a machine with no audio
+// device says so once instead of on every sound in the library.
+function AssetAdmin::ensureAudioDriver(%this)
+{
+ if(OpenALIsInitialized())
+ {
+ return true;
+ }
+
+ if(!%this.audioDriverTried)
+ {
+ %this.audioDriverTried = true;
+ %this.audioDriverReady = OpenALInitDriver();
+ }
+
+ return %this.audioDriverReady;
+}
+
function AssetAdmin::buildAudioPlayButton(%this)
{
%this.audioPlayButtonContainer = new GuiControl()
@@ -274,41 +365,338 @@ class = AssetWindow;
%this.background.add(%this.audioPlayButtonContainer);
}
-function AssetAdmin::destroy(%this)
+// The animation transport, overlaid on the preview exactly as the audio play
+// button above is -- which is what proves an overlay here receives clicks over
+// the SceneWindow. Built once and only shown or hidden, because the stage comes
+// and goes many times in a session and this does not have to.
+function AssetAdmin::buildTransportBar(%this)
+{
+ %this.transportBarContainer = new GuiControl()
+ {
+ position = "0 0";
+ extent = %this.background.extent;
+ HorizSizing = "width";
+ VertSizing = "height";
+ Visible = "0";
+ };
+ ThemeManager.setProfile(%this.transportBarContainer, "emptyProfile");
+
+ // "top" anchors the BOTTOM edge -- the sizing names read the opposite way
+ // round to how they sound -- so the bar keeps the gap it is given below it
+ // and rides the bottom of the preview as the frame grows. The position is set
+ // against the extent the background has now, which is why it is a gap rather
+ // than a coordinate.
+ %barGap = 16;
+ %barTop = (getWord(%this.background.extent, 1) - $EditorTransportBar::playSize) - %barGap;
+
+ %this.transportBar = new GuiChainCtrl()
+ {
+ class = "AssetAnimationTransportBar";
+ superclass = "EditorTransportBar";
+ stage = %this.animationStage;
+ HorizSizing = "center";
+ VertSizing = "top";
+ Position = "0" SPC %barTop;
+
+ // IsVertical BEFORE Extent, and the order is the whole thing.
+ //
+ // A chain sizes itself along its LENGTH and leaves the cross axis alone --
+ // and GuiChainCtrl::resize enforces that by refusing whichever axis is
+ // currently the length. A GuiChainCtrl is born VERTICAL, so an Extent
+ // applied before this line is read as "you may not change my height", the
+ // height stays at the constructor's mEditOpenSpace of 30, and the 36 pixel
+ // play button is laid out centred in 30 -- three pixels off the top and
+ // three off the bottom, which is exactly how it was being clipped.
+ //
+ // Fields are applied in the order they are written, so this is a
+ // one-line-of-difference bug and worth the paragraph.
+ IsVertical = false;
+
+ // The tallest button. Nothing computes this: a chain never grows to fit a
+ // taller child. (IsExtentDynamic would not help either -- it is a
+ // GuiGridCtrl field and a chain never reads it.)
+ Extent = "160" SPC $EditorTransportBar::playSize;
+
+ ChildSpacing = $EditorTransportBar::spacing;
+ };
+ ThemeManager.setProfile(%this.transportBar, "emptyProfile");
+ %this.transportBarContainer.add(%this.transportBar);
+
+ %this.background.add(%this.transportBarContainer);
+}
+
+// The particle transport, built the same way and in the same place as the
+// animation one above. Two bars rather than one with swappable buttons: they
+// share no state and drive different objects, and the chrome they do share is
+// EditorTransportBar.
+function AssetAdmin::buildParticleTransportBar(%this)
+{
+ %this.particleTransportBarContainer = new GuiControl()
+ {
+ position = "0 0";
+ extent = %this.background.extent;
+ HorizSizing = "width";
+ VertSizing = "height";
+ Visible = "0";
+ };
+ ThemeManager.setProfile(%this.particleTransportBarContainer, "emptyProfile");
+
+ %barGap = 16;
+ %barTop = (getWord(%this.background.extent, 1) - $EditorTransportBar::playSize) - %barGap;
+
+ %this.particleTransportBar = new GuiChainCtrl()
+ {
+ class = "AssetParticleTransportBar";
+ superclass = "EditorTransportBar";
+ HorizSizing = "center";
+ VertSizing = "top";
+ Position = "0" SPC %barTop;
+
+ // IsVertical BEFORE Extent, as on the animation bar -- a chain refuses a
+ // resize along whatever axis is currently its length, and it is born
+ // vertical, so an Extent written first is read as "you may not change my
+ // height" and the big play button is laid out clipped into 30 pixels.
+ IsVertical = false;
+ Extent = "260" SPC $EditorTransportBar::playSize;
+ ChildSpacing = $EditorTransportBar::spacing;
+ };
+ ThemeManager.setProfile(%this.particleTransportBar, "emptyProfile");
+ %this.particleTransportBarContainer.add(%this.particleTransportBar);
+
+ %this.background.add(%this.particleTransportBarContainer);
+}
+
+// A particle preview was just built. The bar drives that player and nothing else,
+// so it is handed the new one and everything it was holding about the old one is
+// dropped.
+function AssetAdmin::showParticleTransport(%this, %player, %assetId)
+{
+ if(!isObject(%this.particleTransportBar))
+ {
+ return;
+ }
+
+ %this.particleTransportBarContainer.setVisible(true);
+ %this.particleTransportBar.onPreviewRebuilt(%assetId);
+}
+
+function AssetAdmin::hideParticleTransport(%this)
{
+ if(isObject(%this.particleTransportBarContainer))
+ {
+ %this.particleTransportBarContainer.setVisible(false);
+ }
+}
+
+// Something about the selected asset changed and the preview has to catch up.
+//
+// The old answer was to re-click the tile, which rebuilds the preview scene from
+// nothing. That is right for an image or a font, where the picture is a pure
+// function of the asset -- and quite wrong for an animation being edited, where
+// it would clear the running sprite and start again from frame one on every
+// single change. Dragging one frame in the timeline would restart the playback.
+function AssetAdmin::refreshPreview(%this, %asset)
+{
+ // A live edit to what is already on show: the stage keeps the scene it has
+ // and re-reads only what moved, so the preview does not blink and the
+ // playhead does not jump back to the start.
+ if(%this.animationStage.absorbRefresh(%asset))
+ {
+ return;
+ }
+
+ if(isObject(%this.chosenButton))
+ {
+ %this.chosenButton.onClick();
+ }
+}
+function AssetAdmin::destroy(%this)
+{
+ if(isObject(%this.animationStage))
+ {
+ %this.animationStage.delete();
+ }
+ if(isObject(%this.frameRange))
+ {
+ %this.frameRange.delete();
+ }
+ // Its onRemove drops every snapshot it is holding.
+ if(isObject(%this.undoRecorder))
+ {
+ %this.undoRecorder.delete();
+ }
+ // Takes itself off the bar first if it is still on it.
+ if(isObject(%this.menus))
+ {
+ %this.menus.delete();
+ }
}
function AssetAdmin::open(%this)
{
- %this.Dictionary["ImageAsset"].load();
- %this.Dictionary["AnimationAsset"].load();
- %this.Dictionary["ParticleAsset"].load();
- %this.Dictionary["FontAsset"].load();
- %this.Dictionary["AudioAsset"].load();
- //%this.Dictionary["SpineAsset"].load();
+ %this.libWindow.loadAssets();
%this.assetScene.setScenePause(false);
%this.isOpen = true;
+
+ // After loadAssets, so what the menus grey themselves against is the library
+ // as it stands rather than as it was left.
+ EditorCore.setEditorMenus(%this.menus);
}
-function AssetAdmin::close(%this)
+//-----------------------------------------------------------------------------
+// Making one.
+//
+// Reached two ways: the New button on each library group, and the File menu's
+// New Asset submenu. Named methods rather than one newAsset(%kind), because both
+// callers want to name a command - the menu carries its command as a string, and
+// a string that reads AssetAdmin.newImageAsset() can be found by searching for
+// it. "Bitmap Font" would defeat a title built out of the type name anyway.
+//-----------------------------------------------------------------------------
+
+function AssetAdmin::openNewAssetDialog(%this, %class, %title, %height)
{
- %this.Dictionary["ImageAsset"].unload();
- %this.Dictionary["AnimationAsset"].unload();
- %this.Dictionary["ParticleAsset"].unload();
- %this.Dictionary["FontAsset"].unload();
- %this.Dictionary["AudioAsset"].unload();
- //%this.Dictionary["SpineAsset"].unload();
+ %width = 700;
+ %dialog = new GuiControl()
+ {
+ class = %class;
+ superclass = "EditorDialog";
+ dialogSize = (%width + 8) SPC (%height + 8);
+ dialogCanClose = true;
+ dialogText = %title;
+ };
+ %dialog.init(%width, %height);
- %this.assetScene.setScenePause(true);
- %this.isOpen = false;
+ Canvas.pushDialog(%dialog);
}
-function AssetBase::onRefresh(%this)
+function AssetAdmin::newImageAsset(%this)
{
- if(AssetAdmin.isOpen && isObject(AssetAdmin.chosenButton))
+ %this.openNewAssetDialog("NewImageAssetDialog", "New Image Asset", 340);
+}
+
+function AssetAdmin::newAnimationAsset(%this)
+{
+ %this.openNewAssetDialog("NewAnimationAssetDialog", "New Animation Asset", 390);
+}
+
+function AssetAdmin::newParticleAsset(%this)
+{
+ %this.openNewAssetDialog("NewParticleAssetDialog", "New Particle Asset", 440);
+}
+
+function AssetAdmin::newFontAsset(%this)
+{
+ %this.openNewAssetDialog("NewFontAssetDialog", "New Bitmap Font Asset", 340);
+}
+
+function AssetAdmin::newAudioAsset(%this)
+{
+ %this.openNewAssetDialog("NewAudioAssetDialog", "New Audio Asset", 340);
+}
+
+//-----------------------------------------------------------------------------
+// Unsaved assets.
+//
+// Any number of assets can be left unsaved at once, and switching between them --
+// or away from the Asset Manager entirely -- deliberately does not ask about it.
+// That is the point: trying a particle out, going to look at the image it uses,
+// and coming back should not cost three dialogs.
+//
+// The question is asked once, at the moments the work would actually be lost:
+// closing the project and leaving the application. See EditorCore::guardedCommand.
+//
+// The window's X cannot ask. quit() is posted straight from the window procedure
+// with no script in between, and onPreExit runs inside shutdown, long past the
+// point where a dialog could be shown. The Gui Editor has always had the same
+// hole; it is not one this can close from here.
+//-----------------------------------------------------------------------------
+
+function AssetAdmin::hasUnsavedAssets(%this)
+{
+ return AssetDatabase.getDirtyAssetCount() > 0;
+}
+
+function AssetAdmin::saveAllAssets(%this)
+{
+ // Compiled before saving, because saving is what takes them off the list.
+ %query = new AssetQuery();
+ AssetDatabase.findAssetDirty(%query, true);
+
+ %count = %query.getCount();
+ for(%i = 0; %i < %count; %i++)
{
- AssetAdmin.chosenButton.onClick();
+ %assetId = %query.getAsset(%i);
+
+ if(AssetDatabase.saveAsset(%assetId))
+ {
+ %this.undoRecorder.onAssetSaved(%assetId);
+ }
}
+
+ %query.delete();
+
+ %this.inspector.refreshDocumentBar();
+}
+
+// Ask about the unsaved assets, then hand %command on to whatever guards after
+// this one.
+function AssetAdmin::guardAssets(%this, %command)
+{
+ %this.pendingCommand = %command;
+
+ %count = AssetDatabase.getDirtyAssetCount();
+ %noun = (%count == 1) ? "asset has" : "assets have";
+
+ // The message line is 64 with room to grow into, and the buttons want 34 and a
+ // margin. Plus the 34 the title bar and border take out of the window before
+ // the content sees any of it.
+ %width = 460;
+ %height = 170;
+ %dialog = new GuiControl()
+ {
+ class = "AssetAdminConfirmSaveDialog";
+ superclass = "EditorDialog";
+ dialogSize = (%width + 8) SPC (%height + 8);
+ dialogCanClose = true;
+ dialogResizable = false;
+ dialogText = "Unsaved Assets";
+ message = %count SPC %noun SPC "changes that have not been saved.";
+ };
+ %dialog.init(%width, %height);
+
+ Canvas.pushDialog(%dialog);
+}
+
+// Carry on with what the user originally asked for.
+function AssetAdmin::runPendingCommand(%this)
+{
+ %command = %this.pendingCommand;
+ %this.pendingCommand = "";
+
+ if(%command !$= "")
+ {
+ EditorCore.guardedCommandAfterAssets(%command);
+ }
+}
+
+function AssetAdmin::dropPendingCommand(%this)
+{
+ %this.pendingCommand = "";
+}
+
+function AssetAdmin::close(%this)
+{
+ // The last chance to see where the user left the animation editor's dividers:
+ // a frame set announces nothing when one is dragged, so the sizes are read at
+ // the moments the split is about to go away.
+ %this.animationStage.rememberSizes();
+
+ %this.libWindow.unloadAssets();
+
+ %this.assetScene.setScenePause(true);
+ %this.isOpen = false;
+
+ EditorCore.setEditorMenus("");
}
diff --git a/editor/AssetAdmin/AssetAdminConfirmSaveDialog.cs b/editor/AssetAdmin/AssetAdminConfirmSaveDialog.cs
new file mode 100644
index 000000000..ed5cdb8ac
--- /dev/null
+++ b/editor/AssetAdmin/AssetAdminConfirmSaveDialog.cs
@@ -0,0 +1,117 @@
+//-----------------------------------------------------------------------------
+// What stands between unsaved assets and the two commands that would discard
+// them: Close Project and Exit.
+//
+// Modelled on GuiEditorConfirmSaveDialog, and the same three answers, because the
+// question is the same one: the third option people actually want is "save it,
+// then carry on with what I asked for", and making them Cancel, save by hand and
+// repeat themselves is not really an option at all.
+//
+// Where it differs is that this is about a set rather than a document. There is
+// no Save As to go wrong, so Save All is unconditional and the interrupted
+// command resumes immediately after it.
+//
+// It owns none of the decision. AssetAdmin holds the command that was
+// interrupted; each button here only says which way to go. See
+// AssetAdmin::guardAssets.
+//-----------------------------------------------------------------------------
+
+function AssetAdminConfirmSaveDialog::init(%this, %width, %height)
+{
+ %window = %this.getObject(0);
+ %content = %window.getObject(0);
+
+ %this.feedback = new GuiControl()
+ {
+ HorizSizing = "width";
+ VertSizing = "anchorTop";
+ Position = "12 12";
+ Extent = (%width - 24) SPC 64;
+ text = %this.message;
+ textWrap = true;
+ textExtend = true;
+ };
+ ThemeManager.setProfile(%this.feedback, "infoProfile");
+ %content.add(%this.feedback);
+
+ // Measured from the room the content actually has, not from the dialog's own
+ // height -- the title bar and border take 34 of it.
+ %bottom = %this.contentHeight() - 12;
+
+ // Right to left in the order they escalate: abandon what you asked for, go
+ // through with it, or write the files first.
+ //
+ // The middle one is wider than the other two because "Discard All" does not
+ // fit in the 100 the others use -- it came out as "Discard A".
+ %this.cancelButton = new GuiButtonCtrl()
+ {
+ HorizSizing = "anchorRight";
+ VertSizing = "anchorBottom";
+ Position = (%width - 356) SPC (%bottom - 32);
+ Extent = "100 30";
+ Text = "Cancel";
+ Command = %this.getID() @ ".onCancel();";
+ };
+ ThemeManager.setProfile(%this.cancelButton, "buttonProfile");
+ %content.add(%this.cancelButton);
+
+ %this.discardButton = new GuiButtonCtrl()
+ {
+ HorizSizing = "anchorRight";
+ VertSizing = "anchorBottom";
+ Position = (%width - 246) SPC (%bottom - 32);
+ Extent = "120 30";
+ Text = "Discard All";
+ Command = %this.getID() @ ".onDiscard();";
+ };
+ ThemeManager.setProfile(%this.discardButton, "buttonProfile");
+ %content.add(%this.discardButton);
+
+ %this.saveButton = new GuiButtonCtrl()
+ {
+ HorizSizing = "anchorRight";
+ VertSizing = "anchorBottom";
+ Position = (%width - 116) SPC (%bottom - 34);
+ Extent = "100 34";
+ Text = "Save All";
+ Command = %this.getID() @ ".onSave();";
+ };
+ ThemeManager.setProfile(%this.saveButton, "primaryButtonProfile");
+ %content.add(%this.saveButton);
+}
+
+function AssetAdminConfirmSaveDialog::onCancel(%this)
+{
+ AssetAdmin.dropPendingCommand();
+ %this.closeNow();
+}
+
+// The assets stay unsaved, and the command goes ahead anyway. It resumes AFTER
+// this guard rather than at the top of the chain, or the assets would still be
+// unsaved and the same question would be asked forever.
+function AssetAdminConfirmSaveDialog::onDiscard(%this)
+{
+ %this.closeNow();
+ AssetAdmin.runPendingCommand();
+}
+
+function AssetAdminConfirmSaveDialog::onSave(%this)
+{
+ %this.closeNow();
+ AssetAdmin.saveAllAssets();
+ AssetAdmin.runPendingCommand();
+}
+
+// The window X, which is the same answer as Cancel: no to the question asked.
+function AssetAdminConfirmSaveDialog::onClose(%this)
+{
+ %this.onCancel();
+}
+
+// Not the shared EditorCore.dialog slot: the Gui Editor's own confirm dialog can
+// follow this one within the scheduled delay, and the two would race for it.
+function AssetAdminConfirmSaveDialog::closeNow(%this)
+{
+ Canvas.popDialog(%this);
+ EditorCore.schedule(100, "deleteDialogObject", %this);
+}
diff --git a/editor/AssetAdmin/AssetAdminMenus.cs b/editor/AssetAdmin/AssetAdminMenus.cs
new file mode 100644
index 000000000..abe1294d4
--- /dev/null
+++ b/editor/AssetAdmin/AssetAdminMenus.cs
@@ -0,0 +1,83 @@
+//-----------------------------------------------------------------------------
+// The Asset Manager's two menus: File and Edit.
+//
+// They are called File and Edit and they say nothing the Gui Editor's File and
+// Edit say, which is the whole arrangement: EditorCore swaps whole sets in and
+// out, so only one editor's menus are ever on the bar and both are free to mean
+// what their own editor means. Ctrl+S saves the asset here and the Gui there,
+// and neither has to know the other exists. See EditorMenuSet.
+//
+// There is no Asset menu, because every command in this editor is about an
+// asset - an Asset menu would be the whole menu bar.
+//
+// Everything below already existed as a command on the inspector's document bar
+// or on AssetAdmin, with the same predicates. This is a second way in, not new
+// behavior.
+//-----------------------------------------------------------------------------
+
+function AssetAdminMenus::onAdd(%this)
+{
+ %this.init();
+}
+
+function AssetAdminMenus::build(%this)
+{
+ %file = %this.addMenu("File");
+
+ // A submenu rather than five items, and no accelerator on any of them: there
+ // is no single "new asset" here for Ctrl+N to mean, and picking a favorite of
+ // the five would be arbitrary.
+ %new = %file.addSubMenu("New Asset");
+ %new.addItem("Image Asset...", "AssetAdmin.newImageAsset();");
+ %new.addItem("Animation Asset...", "AssetAdmin.newAnimationAsset();");
+ %new.addItem("Particle Asset...", "AssetAdmin.newParticleAsset();");
+ %new.addItem("Bitmap Font Asset...", "AssetAdmin.newFontAsset();");
+ %new.addItem("Audio Asset...", "AssetAdmin.newAudioAsset();");
+ %file.addSeparator();
+
+ %this.save = %file.addItem("Save Asset", "AssetAdmin.inspector.SaveAsset();", "Ctrl S");
+ %this.saveAll = %file.addItem("Save All Assets", "AssetAdmin.saveAllAssets();", "Ctrl-Shift S");
+ %file.addSeparator();
+
+ // No accelerator, for the reason the Gui Editor's Revert has none: it throws
+ // away everything since the last save and cannot be taken back.
+ %this.revert = %file.addItem("Revert Asset", "AssetAdmin.inspector.RevertAsset();");
+
+ %edit = %this.addMenu("Edit");
+
+ // These two carry the step label - "Undo Move Frame" - the way the document
+ // bar's tooltips already do, so their text is rewritten on every refresh.
+ // Anything looking for them must hold the handle rather than search by text.
+ %this.undo = %edit.addItem("Undo", "AssetAdmin.inspector.UndoAsset();", "Ctrl Z");
+ %this.redo = %edit.addItem("Redo", "AssetAdmin.inspector.RedoAsset();", "Ctrl-Shift Z");
+ %edit.addSeparator();
+ %this.duplicate = %edit.addItem("Duplicate Asset...", "AssetAdmin.inspector.DuplicateAsset();", "Ctrl D");
+ %edit.addSeparator();
+
+ // No accelerator: this offers to take the asset's files off disk with it.
+ %this.deleteAsset = %edit.addItem("Delete Asset...", "AssetAdmin.inspector.deleteAsset();");
+}
+
+// Called when the set goes back on the bar, and by AssetInspector on every
+// change to the document - which is often, so this stays down to reading the
+// same predicates the document bar's buttons read and writing a flag each.
+function AssetAdminMenus::refresh(%this)
+{
+ %inspector = %this.tool.inspector;
+ %hasDocument = isObject(%inspector.documentAsset());
+
+ %this.save.setActive(%inspector.getSaveAssetEnabled());
+ %this.saveAll.setActive(%this.tool.hasUnsavedAssets());
+ %this.revert.setActive(%inspector.getRevertAssetEnabled());
+
+ %this.undo.setActive(%inspector.getUndoAssetEnabled());
+ %this.redo.setActive(%inspector.getRedoAssetEnabled());
+ %this.duplicate.setActive(%hasDocument);
+ %this.deleteAsset.setActive(%hasDocument);
+
+ // The same text the buttons put in their tooltips. setText goes through the
+ // bar's own update, and a dropdown re-measures itself every time it opens, so
+ // a longer label is not clipped.
+ %this.undo.setText(%inspector.getUndoAssetTooltip());
+ %this.redo.setText(%inspector.getRedoAssetTooltip());
+}
diff --git a/editor/AssetAdmin/AssetAudioPlayButton.cs b/editor/AssetAdmin/AssetAudioPlayButton.cs
index b0e2417f9..b9b4f4a98 100644
--- a/editor/AssetAdmin/AssetAudioPlayButton.cs
+++ b/editor/AssetAdmin/AssetAudioPlayButton.cs
@@ -7,7 +7,13 @@
}
else
{
- %this.sound = alxPlay(%this.assetID);
+ // alxPlayPreview, not alxPlay: an editor auditions the ASSET, not the
+ // asset as the project currently loaded happens to be mixing it. Played
+ // through alxPlay, a game that had turned its music channel down to
+ // nothing made every music asset here silent -- and not quietly silent,
+ // since the engine refuses to create a source on a muted channel at all,
+ // so there was no handle and no way to tell that from a broken file.
+ %this.sound = alxPlayPreview(%this.assetID);
%this.setText("Stop");
if(!%this.asset.Looping)
diff --git a/editor/AssetAdmin/AssetBase.cs b/editor/AssetAdmin/AssetBase.cs
new file mode 100644
index 000000000..fefe0cf15
--- /dev/null
+++ b/editor/AssetAdmin/AssetBase.cs
@@ -0,0 +1,73 @@
+//-----------------------------------------------------------------------------
+// What the Asset Manager does when an asset changes underneath it.
+//
+// Every setter on an asset ends in refreshAsset(), which marks it unsaved and
+// fires this. So this is the one place that hears about a change however it was
+// made -- from the inspector, from the Explicit Frames or Image Layers tab, from
+// the particle graph editor down in C++, or as a cascade from some other asset
+// that this one depends on.
+//
+// Note refreshAsset does NOT write the file any more. Saving is a thing the user
+// asks for; see AssetInspector's document bar and [[AssetUndoRecorder]].
+//
+// %direct separates the two reasons this fires:
+//
+// true this asset was changed, and now has unsaved work in it
+// false something this asset READS FROM was changed, so whatever it derives
+// from that needs rebuilding -- but nothing it saves has moved, and it
+// is not unsaved on account of it
+//
+// Four things have to be told:
+//
+// the recorder a direct change is one step of undo, and it needs the state
+// from before it, which is the snapshot it has been holding
+// the library a tile caches the name, description and category it is
+// searched and sorted by, and the inspector edits all three
+// the preview the scene showing the asset is built from its values
+// the inspector a change made on another tab -- explicit mode, a new layer --
+// is a change to what the inspector is showing
+//-----------------------------------------------------------------------------
+
+function AssetBase::onRefresh(%this, %direct)
+{
+ // The library has nothing loaded while the Asset Manager is shut, and this
+ // also fires as assets are acquired during the load itself, before there is
+ // anything to refresh.
+ if(!AssetAdmin.isOpen)
+ {
+ return;
+ }
+
+ // First, so that the step records the state from before anything below reacts
+ // to the change.
+ AssetAdmin.undoRecorder.onAssetChanged(%this, %direct);
+
+ AssetAdmin.libWindow.onAssetRefreshed(%this.getAssetId());
+ AssetAdmin.inspector.onAssetRefreshed(%this);
+
+ // Redraws the preview. It does not re-enter the inspector: onClick only loads
+ // an asset into it when the selection actually moved.
+ AssetAdmin.refreshPreview(%this);
+}
+
+//-----------------------------------------------------------------------------
+// An asset gained or lost unsaved changes.
+//
+// A global, fired by AssetManager::markAssetDirty, saveAsset, revertAsset and
+// setAssetDirty -- only on the edge, never on every change. It exists so the
+// library can mark a tile without asking every asset every frame, and it is
+// separate from onRefresh because the two do not coincide: a change to an already
+// unsaved asset fires onRefresh and not this, and a save fires this and not
+// onRefresh.
+//-----------------------------------------------------------------------------
+
+function onAssetDirtyChanged(%assetId)
+{
+ if(!isObject(AssetAdmin) || !AssetAdmin.isOpen)
+ {
+ return;
+ }
+
+ AssetAdmin.libWindow.onAssetDirtyChanged(%assetId);
+ AssetAdmin.inspector.refreshDocumentBar();
+}
diff --git a/editor/AssetAdmin/AssetDictionary.cs b/editor/AssetAdmin/AssetDictionary.cs
index 6eb48db91..d05309f5f 100644
--- a/editor/AssetAdmin/AssetDictionary.cs
+++ b/editor/AssetAdmin/AssetDictionary.cs
@@ -20,8 +20,42 @@
// IN THE SOFTWARE.
//-----------------------------------------------------------------------------
+// One collapsible group of the Asset Library -- all the assets of one type.
+// A GuiPanelCtrl whose header is the toggle, with a "New" button and a grid of
+// AssetDictionaryButtons inside it.
+//
+// The tiles go in that inner grid and never directly on the panel:
+// GuiExpandCtrl::toggleHiddenChildren force-writes mVisible on every DIRECT
+// child whenever the panel expands, collapses or resizes, which would fight the
+// search filter for control of exactly the same flag. Grandchildren are left
+// alone.
+//
+// The group owns the arrangement; AssetLibraryWindow owns the decisions. It is
+// told which view mode to draw, which field to sort on and what to filter by,
+// because all three apply to the whole library at once.
+
+$AssetDictionary::gridCell = 72;
+$AssetDictionary::gridCellHeight = 96;
+$AssetDictionary::rowHeight = 40;
+
+// Where the grid starts: under the header and the New button.
+$AssetDictionary::gridTop = 62;
+
function AssetDictionary::onAdd(%this)
{
+ if(%this.viewMode $= "")
+ {
+ %this.viewMode = "grid";
+ }
+ if(%this.sortField $= "")
+ {
+ %this.sortField = "name";
+ }
+ if(%this.title $= "")
+ {
+ %this.title = %this.getText();
+ }
+
%this.newButton = new GuiButtonCtrl()
{
class = "NewAssetButton";
@@ -34,23 +68,42 @@ class = "NewAssetButton";
ThemeManager.setProfile(%this.newButton, "buttonProfile");
%this.add(%this.newButton);
+ // CellModeX variable makes CellSizeX a MINIMUM column width -- as many columns
+ // as fit, with the remainder shared out -- which is the whole reflow, and it is
+ // also what lets a single row stretch across the group in rows mode. CellModeY
+ // has to stay absolute; variable would size a row to its tallest child, which
+ // puts a tile-sized cell in a 40 pixel row.
+ //
+ // IsExtentDynamic is what lets the grid grow and shrink with its contents, so
+ // the panel has something to measure and a fully filtered group collapses to
+ // its header instead of leaving a hole.
%this.grid = new GuiGridCtrl()
{
- Position="0 62";
+ Position = "0" SPC $AssetDictionary::gridTop;
Extent = "310 50";
HorizSizing = "width";
VertSizing = "height";
- CellSizeX = 60;
- CellSizeY = 60;
- CellModeX = variable;
+ CellSizeX = $AssetDictionary::gridCell;
+ CellSizeY = $AssetDictionary::gridCellHeight;
+ CellModeX = "variable";
+ CellModeY = "absolute";
CellSpacingX = 4;
CellSpacingY = 4;
+ MaxColCount = 0;
+ MaxRowCount = 0;
OrderMode = "LRTB";
+ IsExtentDynamic = true;
};
ThemeManager.setProfile(%this.grid, "emptyProfile");
%this.add(%this.grid);
+
+ %this.setViewMode(%this.viewMode);
}
+//-----------------------------------------------------------------------------
+// Contents.
+//-----------------------------------------------------------------------------
+
function AssetDictionary::load(%this)
{
%query = new AssetQuery();
@@ -63,33 +116,70 @@ class = "NewAssetButton";
if(!AssetDatabase.isAssetInternal(%assetID))
{
- %this.addButton(%assetID);
+ %this.addButton(%assetID, true);
}
}
%query.delete();
+ // findAllAssets walks a hash table, so what it returns is in no particular
+ // order and not even the same order twice. Sorting once here is what makes the
+ // library's default order mean something.
+ %this.applySort(%this.sortField);
+
%this.newButton.text = "New" SPC %this.type;
%this.newButton.type = %this.type;
}
-function AssetDictionary::addButton(%this, %assetID)
+// %deferPlacement is for load(), which sorts once at the end rather than paying
+// for a sort per asset. Everything else -- the New Asset dialogs -- adds one
+// asset to a library that is already open, and wants it to land where it belongs.
+function AssetDictionary::addButton(%this, %assetID, %deferPlacement)
{
+ // Authored at the cell size for the current mode so the tile's first pass at
+ // arranging itself is close; the grid resizes it for real on add, and the
+ // second setViewMode below is what actually settles it. Spelled from the
+ // constants rather than read back off the grid, because CellSizeX and
+ // CellSizeY are floats and "72.000000 96.000000" is not a Point2I.
+ %cellHeight = (%this.viewMode $= "rows")
+ ? $AssetDictionary::rowHeight
+ : $AssetDictionary::gridCellHeight;
+
%button = new GuiButtonCtrl()
{
- Class = AssetDictionaryButton;
- HorizSizing="center";
- VertSizing="center";
- Extent = "100 100";
+ Class = "AssetDictionaryButton";
+ HorizSizing = "center";
+ VertSizing = "center";
+ Extent = $AssetDictionary::gridCell SPC %cellHeight;
Tooltip = AssetDatabase.getAssetName(%assetID);
Text = "";
AssetID = %assetID;
Type = %this.Type;
+ viewMode = %this.viewMode;
};
ThemeManager.setProfile(%button, "itemSelectProfile");
ThemeManager.setProfile(%button, "tipProfile", "TooltipProfile");
%this.grid.add(%button);
- %this.fixSize();
+ // Again, now that the grid has resized it to a real cell: the pass inside
+ // onAdd could only measure the extent the tile was authored with.
+ %button.setViewMode(%this.viewMode);
+
+ if(!%deferPlacement)
+ {
+ %this.applySort(%this.sortField);
+
+ // The library owns the needle, and a new asset has to face it like every
+ // other: adding one while a filter is up must not sneak an unmatched tile
+ // onto the screen.
+ if(isObject(%this.owner))
+ {
+ %this.owner.applyFilter();
+ }
+ else
+ {
+ %this.fixSize();
+ }
+ }
return %button;
}
@@ -100,21 +190,20 @@ class = "NewAssetButton";
if(isObject(%button))
{
%button.delete();
- %this.fixSize();
+
+ if(isObject(%this.owner))
+ {
+ %this.owner.applyFilter();
+ }
+ else
+ {
+ %this.fixSize();
+ }
return true;
}
return false;
}
-function AssetDictionary::fixSize(%this)
-{
- if(%this.getExpanded())
- {
- %this.setExpanded(false);
- %this.setExpanded(true);
- }
-}
-
function AssetDictionary::getButton(%this, %assetID)
{
for(%i = 0; %i < %this.grid.getCount(); %i++)
@@ -128,6 +217,11 @@ class = "NewAssetButton";
return 0;
}
+function AssetDictionary::getButtonCount(%this)
+{
+ return %this.grid.getCount();
+}
+
function AssetDictionary::unload(%this)
{
//Remove all the child gui controls
@@ -142,14 +236,195 @@ class = "NewAssetButton";
{
%this.unload();
%this.load();
+
+ if(isObject(%this.owner))
+ {
+ %this.owner.applyFilter();
+ }
}
-function AssetDictionarySprite::onAnimationEnd(%this, %animationAssetID)
+//-----------------------------------------------------------------------------
+// View mode.
+//-----------------------------------------------------------------------------
+
+// Both modes are one grid with different cell metrics, not two layouts. Nothing
+// is rebuilt, hidden or swapped -- every level rewrites its own numbers and the
+// same controls stay put, so the selection and the running animations survive a
+// mode change.
+function AssetDictionary::setViewMode(%this, %mode)
{
- %this.schedule(2000, "restartAnimation", %animationAssetID);
+ %this.viewMode = %mode;
+ %width = getWord(%this.getExtent(), 0);
+
+ if(%mode $= "rows")
+ {
+ // One column, said outright. MaxColCount clamps the chain length, and
+ // CellModeX variable then hands that single column the whole width, so a
+ // row fills the group however wide the frame is dragged. Asking for a cell
+ // wider than the pane would do it too, but only until the next resize.
+ %this.grid.MaxColCount = 1;
+ %this.grid.CellSizeY = $AssetDictionary::rowHeight;
+ }
+ else
+ {
+ %this.grid.MaxColCount = 0;
+ %this.grid.CellSizeY = $AssetDictionary::gridCellHeight;
+ }
+
+ // The grid lays out on resize, so nudge it before the tiles read their own
+ // extents -- otherwise each one measures the cell it had in the other mode.
+ %this.grid.resize(0, $AssetDictionary::gridTop, %width, 4);
+
+ for(%i = 0; %i < %this.grid.getCount(); %i++)
+ {
+ %this.grid.getObject(%i).setViewMode(%mode);
+ }
}
-function AssetDictionarySprite::restartAnimation(%this, %animationAssetID)
+//-----------------------------------------------------------------------------
+// Search.
+//-----------------------------------------------------------------------------
+
+// Hide what does not match and report what is left. The needle arrives already
+// lowercased and trimmed, and every tile's searchKey was lowercased once when it
+// was built, because this walks every tile on every keystroke.
+function AssetDictionary::applyFilter(%this, %needle)
+{
+ %shown = 0;
+
+ for(%i = 0; %i < %this.grid.getCount(); %i++)
+ {
+ %button = %this.grid.getObject(%i);
+ %match = (%needle $= "") || (strstr(%button.searchKey, %needle) != -1);
+ %button.setVisible(%match);
+
+ if(%match)
+ {
+ %shown++;
+ }
+ }
+
+ // A group whose matches are all gone keeps its header and its New button, so
+ // the shape of the library does not change under the person typing.
+ %this.setText(%this.title SPC "(" @ %shown @ ")");
+
+ %this.reflowGrid();
+
+ return %shown;
+}
+
+// A grid re-lays out when a child is added, removed, moved or resized, and
+// setVisible is none of those -- so without this the hidden cells leave their
+// holes behind and the grid keeps its old height. Resizing it to the size it
+// already has walks the children again, and the walk skips the invisible ones.
+function AssetDictionary::reflowGrid(%this)
{
- %this.setAnimation(%animationAssetID);
+ %position = %this.grid.getPosition();
+ %extent = %this.grid.getExtent();
+ %this.grid.resize(getWord(%position, 0), getWord(%position, 1),
+ getWord(%extent, 0), getWord(%extent, 1));
+}
+
+//-----------------------------------------------------------------------------
+// Sort.
+//-----------------------------------------------------------------------------
+
+// Reorder the tiles that are already there rather than rebuilding them. A
+// rebuild would release and re-acquire every asset, throw away and remake every
+// sprite, restart every animation and drop the current selection -- all to
+// change the order of a list.
+function AssetDictionary::applySort(%this, %field)
+{
+ %this.sortField = %field;
+
+ %count = %this.grid.getCount();
+ if(%count < 2)
+ {
+ return;
+ }
+
+ for(%i = 0; %i < %count; %i++)
+ {
+ %button = %this.grid.getObject(%i);
+ %item[%i] = %button;
+ %major[%i] = (%field $= "category") ? %button.sortCategory : %button.sortName;
+ %minor[%i] = %button.sortName;
+ }
+
+ // Insertion sort, the same shape as GuiProfileEditorLibrary::sortTabList. A
+ // group holds tens of assets, sometimes low hundreds, and this runs when the
+ // sort field changes or a group loads -- never per frame and never per
+ // keystroke -- so the quadratic cost never shows.
+ for(%i = 1; %i < %count; %i++)
+ {
+ %heldItem = %item[%i];
+ %heldMajor = %major[%i];
+ %heldMinor = %minor[%i];
+
+ %j = %i - 1;
+ while(%j >= 0 && %this.sortsAfter(%major[%j], %minor[%j], %heldMajor, %heldMinor))
+ {
+ %item[%j + 1] = %item[%j];
+ %major[%j + 1] = %major[%j];
+ %minor[%j + 1] = %minor[%j];
+ %j--;
+ }
+
+ %item[%j + 1] = %heldItem;
+ %major[%j + 1] = %heldMajor;
+ %minor[%j + 1] = %heldMinor;
+ }
+
+ // SimSet::reOrder inserts its first argument IN FRONT OF its second, so
+ // walking backwards and putting each tile ahead of the one that follows it
+ // lands the whole list in order. The grid does not hear about it on its own.
+ for(%i = %count - 2; %i >= 0; %i--)
+ {
+ %this.grid.reorderChild(%item[%i], %item[%i + 1]);
+ }
+ %this.grid.childrenReordered();
+}
+
+// True when A belongs after B. A category sort falls back to the name, so the
+// contents of one category are still alphabetical -- and so the many assets that
+// carry no category at all keep a stable order among themselves rather than
+// whatever the hash table last handed over.
+function AssetDictionary::sortsAfter(%this, %aMajor, %aMinor, %bMajor, %bMinor)
+{
+ %order = stricmp(%aMajor, %bMajor);
+ if(%order != 0)
+ {
+ return %order > 0;
+ }
+
+ return stricmp(%aMinor, %bMinor) > 0;
+}
+
+//-----------------------------------------------------------------------------
+// Layout.
+//-----------------------------------------------------------------------------
+
+// A GuiPanelCtrl caches the height it opens to, measured from the children it
+// had at the moment it opened. Anything that changes the size of what is inside
+// has to throw that cache away.
+function AssetDictionary::fixSize(%this)
+{
+ if(%this.getExpanded())
+ {
+ %this.setExpanded(false);
+ %this.setExpanded(true);
+ }
+}
+
+// fixSize plus a width nudge: one parentResized through every child with the
+// widths unchanged, which is what makes the grid re-measure its cells before the
+// panel measures the grid.
+function AssetDictionary::forceLayout(%this)
+{
+ %w = getWord(%this.getExtent(), 0);
+ %h = getWord(%this.getExtent(), 1);
+ %this.resize(0, 0, %w + 1, %h);
+ %this.resize(0, 0, %w, %h);
+
+ %this.fixSize();
}
diff --git a/editor/AssetAdmin/AssetDictionaryButton.cs b/editor/AssetAdmin/AssetDictionaryButton.cs
index 92e9b24b5..9c5f5bbef 100644
--- a/editor/AssetAdmin/AssetDictionaryButton.cs
+++ b/editor/AssetAdmin/AssetDictionaryButton.cs
@@ -20,11 +20,140 @@
// IN THE SOFTWARE.
//-----------------------------------------------------------------------------
+// One asset in the library: a picture and the asset's name, arranged as a tile
+// or as a row depending on the library's view mode.
+//
+// The picture is whatever the asset can show of itself -- the image, the running
+// animation -- falling back to a flat icon for the kinds that have no likeness.
+// The name is drawn as well as being the tooltip, because the library can now be
+// searched and sorted by it and an order you cannot read is not an order.
+
+$AssetDictionaryButton::gridArt = 50;
+
+// The band at the bottom of a tile the caption is allowed to fill.
+$AssetDictionaryButton::gridCaption = 34;
+
+$AssetDictionaryButton::rowArt = 28;
+$AssetDictionaryButton::rowTextLeft = 36;
+
+// The square badge in the corner that says an asset has unsaved changes.
+$AssetDictionaryButton::dirtyMark = 16;
+
function AssetDictionaryButton::onAdd(%this)
{
+ %this.buildSearchKey();
+ %this.buildCaption();
+ %this.buildDirtyMark();
+
%this.call("load" @ %this.type, %this.assetID);
+
+ // The asset may already have unsaved changes -- a tile built by a duplicate,
+ // or the library being reopened on a project left mid-edit.
+ %this.refreshDirtyMark();
+
+ if(%this.viewMode $= "")
+ {
+ %this.viewMode = "grid";
+ }
+ %this.setViewMode(%this.viewMode);
+}
+
+// Everything the library asks about this asset, worked out once.
+//
+// The name, description and category all come off the AssetDefinition without
+// loading anything, and any of the three may be empty -- most assets carry no
+// description and no category at all. They are lowercased here rather than in
+// the filter because the filter walks every tile on every keystroke, and strstr
+// is case sensitive (unlike $=, which is not).
+function AssetDictionaryButton::buildSearchKey(%this)
+{
+ %name = AssetDatabase.getAssetName(%this.assetID);
+ %description = AssetDatabase.getAssetDescription(%this.assetID);
+ %category = AssetDatabase.getAssetCategory(%this.assetID);
+
+ %this.assetName = %name;
+ %this.assetCategory = %category;
+
+ %this.sortName = strlwr(%name);
+ %this.sortCategory = strlwr(%category);
+
+ // Trimmed, because two of the three are usually empty: without it an asset
+ // with neither a description nor a category ends up keyed "name ", and one
+ // with nothing at all ends up keyed " " -- which a needle of a single space
+ // would match. (The needle is trimmed too, so that case cannot arise today;
+ // the key should not depend on that staying true.)
+ %this.searchKey = trim(strlwr(%name SPC %description SPC %category));
+}
+
+// The three fields are editable in the inspector, so what was worked out once
+// has to be worked out again when they change. Everything the tile shows or is
+// found by comes from here.
+function AssetDictionaryButton::refreshKeys(%this)
+{
+ %this.buildSearchKey();
+
+ %this.caption.setText(%this.assetName);
+ %this.Tooltip = %this.assetName;
+}
+
+// The badge that says this asset has changes that have not been saved.
+//
+// A control of its own rather than an asterisk on the end of the caption, so that
+// the mark never becomes part of the name: the library is searched and sorted by
+// what the caption holds, and a name that grows a " *" is a name that sorts
+// somewhere else and stops matching a search for itself.
+//
+// UseInput is off so it cannot swallow the click that selects the tile it sits on.
+function AssetDictionaryButton::buildDirtyMark(%this)
+{
+ %size = $AssetDictionaryButton::dirtyMark;
+
+ %this.dirtyMark = new GuiControl()
+ {
+ HorizSizing = "anchorRight";
+ VertSizing = "anchorTop";
+ Position = "0 0";
+ Extent = %size SPC %size;
+ MinExtent = "0 0";
+ Text = "*";
+ UseInput = false;
+ Visible = false;
+ };
+ ThemeManager.setProfile(%this.dirtyMark, "impactProfile");
+ %this.add(%this.dirtyMark);
+}
+
+// The asset's unsaved state changed. Only the badge appears or goes; nothing
+// about the tile's placement, its caption or its keys depends on it.
+function AssetDictionaryButton::refreshDirtyMark(%this)
+{
+ if(isObject(%this.dirtyMark))
+ {
+ %this.dirtyMark.setVisible(AssetDatabase.isAssetDirty(%this.assetID));
+ }
}
+function AssetDictionaryButton::buildCaption(%this)
+{
+ %this.caption = new GuiControl()
+ {
+ Position = "0 0";
+ Extent = "60 20";
+ MinExtent = "0 0";
+ Text = %this.assetName;
+ align = "center";
+ vAlign = "bottom";
+ textWrap = true;
+ UseInput = false;
+ };
+ ThemeManager.setProfile(%this.caption, "labelProfile");
+ %this.add(%this.caption);
+}
+
+//-----------------------------------------------------------------------------
+// The picture, one loader per asset kind.
+//-----------------------------------------------------------------------------
+
function AssetDictionaryButton::loadImageAsset(%this, %assetID)
{
%imageAsset = AssetDatabase.acquireAsset(%assetID);
@@ -111,24 +240,110 @@
%this.add(%texture);
}
+// MinExtent is deliberately tiny: setViewMode drives this down to the row art
+// size, and a minimum of the tile size would silently refuse.
function AssetDictionaryButton::buildIcon(%this)
{
- %texture = new GuiSpriteCtrl()
+ %this.icon = new GuiSpriteCtrl()
{
class = "AssetDictionarySprite";
- HorizSizing="center";
- VertSizing="center";
- Extent = "50 50";
- minExtent = "50 50";
+ HorizSizing = "center";
+ VertSizing = "center";
+ Extent = $AssetDictionaryButton::gridArt SPC $AssetDictionaryButton::gridArt;
+ minExtent = "8 8";
Position = "0 0";
constrainProportions = "1";
fullSize = "1";
UseInput = false;
};
- ThemeManager.setProfile(%texture, "spriteProfile");
- return %texture;
+ ThemeManager.setProfile(%this.icon, "spriteProfile");
+ return %this.icon;
}
+//-----------------------------------------------------------------------------
+// View mode.
+//-----------------------------------------------------------------------------
+
+// The picture and the caption swap places rather than the tile being rebuilt.
+// The sprite scales itself (constrainProportions and fullSize), so only its
+// extent and position change -- the image it holds, and an animation part way
+// through, are untouched.
+function AssetDictionaryButton::setViewMode(%this, %mode)
+{
+ %this.viewMode = %mode;
+
+ if(!isObject(%this.icon) || !isObject(%this.caption) || !isObject(%this.dirtyMark))
+ {
+ return;
+ }
+
+ %w = getWord(%this.getExtent(), 0);
+ %h = getWord(%this.getExtent(), 1);
+ %mark = $AssetDictionaryButton::dirtyMark;
+
+ if(%mode $= "rows")
+ {
+ %art = $AssetDictionaryButton::rowArt;
+ %left = $AssetDictionaryButton::rowTextLeft;
+
+ %this.icon.HorizSizing = "anchorLeft";
+ %this.icon.VertSizing = "center";
+ %this.icon.setExtent(%art, %art);
+ %this.icon.setPosition(4, (%h - %art) / 2);
+
+ // The caption stops short of the badge rather than running under it: a row
+ // is one line of text with nothing above it to move the mark out of.
+ %this.caption.HorizSizing = "width";
+ %this.caption.VertSizing = "center";
+ %this.caption.setExtent(%w - %left - 8 - %mark, %art);
+ %this.caption.setPosition(%left, (%h - %art) / 2);
+ %this.caption.align = "left";
+ %this.caption.vAlign = "middle";
+ %this.caption.textWrap = false;
+
+ %this.dirtyMark.VertSizing = "center";
+ %this.dirtyMark.setExtent(%mark, %mark);
+ %this.dirtyMark.setPosition(%w - %mark - 4, (%h - %mark) / 2);
+ }
+ else
+ {
+ %art = $AssetDictionaryButton::gridArt;
+ %band = $AssetDictionaryButton::gridCaption;
+
+ %this.caption.HorizSizing = "fill";
+ %this.caption.VertSizing = "fill";
+ %this.caption.align = "center";
+ %this.caption.vAlign = "bottom";
+ %this.caption.textWrap = true;
+ %this.caption.applySizing();
+
+ // And that fill is how the button's own border inset gets measured: what
+ // the caption reports back after filling IS the content rect, and where it
+ // sits IS that rect's origin.
+ %innerW = getWord(%this.caption.getExtent(), 0);
+ %innerH = getWord(%this.caption.getExtent(), 1);
+ %innerX = getWord(%this.caption.getPosition(), 0);
+ %innerY = getWord(%this.caption.getPosition(), 1);
+
+ %this.icon.HorizSizing = "center";
+ %this.icon.VertSizing = "anchorTop";
+ %this.icon.setExtent(%art, %art);
+ %this.icon.setPosition(0, (%innerH - %band - %art) / 2);
+ %this.icon.applySizing();
+
+ // Hard into the top right of the content rect, over the corner of the
+ // picture. Measured from the caption rather than from the button's own
+ // extent so the badge lands inside the border rather than under it.
+ %this.dirtyMark.VertSizing = "anchorTop";
+ %this.dirtyMark.setExtent(%mark, %mark);
+ %this.dirtyMark.setPosition(%innerX + %innerW - %mark, %innerY);
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Selection.
+//-----------------------------------------------------------------------------
+
function AssetDictionaryButton::onClick(%this)
{
%firstLoad = false;
@@ -146,9 +361,23 @@ class = "AssetDictionarySprite";
AssetAdmin.audioPlayButtonContainer.setVisible(false);
AssetAdmin.AssetWindow.setVisible(true);
+ // One line, above the whole chain, and every branch below is untouched. The
+ // stage keeps the animation split up if this asset is the one it is already
+ // showing and takes it down otherwise, which is the entire answer for the
+ // five asset kinds that have never heard of it.
+ AssetAdmin.animationStage.retainFor(%this.AnimationAssetID);
+
+ // Same idea for the particle transport: the particle branch below puts it back
+ // up with the player it just built, so the only thing that has to happen here
+ // is that it is not left over the preview of something that has no transport.
+ AssetAdmin.hideParticleTransport();
+
+ // The animation branch has to stay first: an animation tile caches its image
+ // asset too, so the image branch would swallow it.
if(isObject(%this.AnimationAsset) && %this.AnimationAssetID !$= "")
{
AssetAdmin.AssetWindow.displayAnimationAsset(%this.imageAsset, %this.AnimationAsset, %this.AnimationAssetID);
+ AssetAdmin.animationStage.select(%this.imageAsset, %this.AnimationAsset, %this.AnimationAssetID);
if(%firstLoad)
{
AssetAdmin.inspector.loadAnimationAsset(%this.AnimationAsset, %this.AnimationAssetID);
diff --git a/engine/compilers/VisualStudio 2022/main.cs b/editor/AssetAdmin/AssetDictionarySprite.cs
similarity index 70%
rename from engine/compilers/VisualStudio 2022/main.cs
rename to editor/AssetAdmin/AssetDictionarySprite.cs
index fafa27258..005d32e61 100644
--- a/engine/compilers/VisualStudio 2022/main.cs
+++ b/editor/AssetAdmin/AssetDictionarySprite.cs
@@ -20,8 +20,18 @@
// IN THE SOFTWARE.
//-----------------------------------------------------------------------------
-// This file simply points to the real main.cs file in the root of the working directory.
-// This is needed if the project is run in debug mode from within VisualStudio.
-setMainDotCsDir(makeFullPath("../../../"));
-setCurrentDirectory(makeFullPath("./"));
-exec("../../../main.cs");
+// The picture on an AssetDictionaryButton.
+//
+// An animation asset plays in the library so a person can tell two similar ones
+// apart, but it plays on a loop with a breath between passes rather than
+// running flat out: a wall of thumbnails all animating at once is unreadable.
+
+function AssetDictionarySprite::onAnimationEnd(%this, %animationAssetID)
+{
+ %this.schedule(2000, "restartAnimation", %animationAssetID);
+}
+
+function AssetDictionarySprite::restartAnimation(%this, %animationAssetID)
+{
+ %this.setAnimation(%animationAssetID);
+}
diff --git a/editor/AssetAdmin/AssetInspector.cs b/editor/AssetAdmin/AssetInspector.cs
index 882bd8d04..6785f085b 100644
--- a/editor/AssetAdmin/AssetInspector.cs
+++ b/editor/AssetAdmin/AssetInspector.cs
@@ -56,7 +56,7 @@
{
HorizSizing = "left";
Class = "EditorIconButton";
- Frame = 48;
+ Frame = $EditorIcon::trash;
Position = "660 5";
Command = %this.getId() @ ".deleteAsset();";
Tooltip = "Delete Asset";
@@ -77,10 +77,35 @@
};
ThemeManager.setProfile(%this.emitterButtonBar, "emptyProfile");
%this.add(%this.emitterButtonBar);
- %this.emitterButtonBar.addButton("AddEmitter", 25, "Add Emitter", "");
- %this.emitterButtonBar.addButton("MoveEmitterBackward", 27, "Move Emitter Backward", "getMoveEmitterBackwardEnabled");
- %this.emitterButtonBar.addButton("MoveEmitterForward", 28, "Move Emitter Forward", "getMoveEmitterForwardEnabled");
- %this.emitterButtonBar.addButton("RemoveEmitter", 23, "Remove Emitter", "getRemoveEmitterEnabled");
+ %this.emitterButtonBar.addButton("AddEmitter", $EditorIcon::round_plus, "Add Emitter", "");
+ %this.emitterButtonBar.addButton("MoveEmitterBackward", $EditorIcon::rnd_br_up, "Move Emitter Backward", "getMoveEmitterBackwardEnabled");
+ %this.emitterButtonBar.addButton("MoveEmitterForward", $EditorIcon::rnd_br_down, "Move Emitter Forward", "getMoveEmitterForwardEnabled");
+ %this.emitterButtonBar.addButton("RemoveEmitter", $EditorIcon::round_delete, "Remove Emitter", "getRemoveEmitterEnabled");
+
+ // What the user does to a whole asset rather than to a field of one: save the
+ // changes, throw them away, branch them, or step through them.
+ //
+ // Five 24 pixel buttons at 4 apart is 136 wide, and it sits to the LEFT of the
+ // delete button, which is itself pinned to the right edge. The emitter bar
+ // starts at 340 and runs to about 448, so there is room for both.
+ %this.documentButtonBar = new GuiChainCtrl()
+ {
+ Class = "EditorButtonBar";
+ HorizSizing = "left";
+ Position = "514 5";
+ Extent = "0 24";
+ ChildSpacing = 4;
+ IsVertical = false;
+ Tool = %this;
+ Visible = false;
+ };
+ ThemeManager.setProfile(%this.documentButtonBar, "emptyProfile");
+ %this.add(%this.documentButtonBar);
+ %this.documentButtonBar.addButton("SaveAsset", $EditorIcon::save, "Save Asset", "getSaveAssetEnabled");
+ %this.documentButtonBar.addButton("RevertAsset", $EditorIcon::reload, "Revert Asset", "getRevertAssetEnabled");
+ %this.documentButtonBar.addButton("DuplicateAsset", $EditorIcon::clipboard_copy, "Duplicate Asset", "");
+ %this.documentButtonBar.addButton("UndoAsset", $EditorIcon::undo, "Undo", "getUndoAssetEnabled", "getUndoAssetTooltip");
+ %this.documentButtonBar.addButton("RedoAsset", $EditorIcon::redo, "Redo", "getRedoAssetEnabled", "getRedoAssetTooltip");
%this.tabBook = new GuiTabBookCtrl()
{
@@ -105,6 +130,32 @@
%this.inspector = %this.createInspector();
%this.insScroller.add(%this.inspector);
+ // An asset kind with a pane of its own gets it here, sharing the Inspector
+ // page with the generic inspector rather than taking a tab -- for that kind
+ // of asset the pane IS the inspector, and chooseInspector decides which one is
+ // on show. None of them is ever rebuilt or freed.
+ %this.registerPane("Image", %this.createImagePane());
+ %this.registerPane("Animation", %this.createAnimationPane());
+ %this.registerPane("Font", %this.createFontPane());
+ %this.registerPane("Sound", %this.createSoundPane());
+
+ // A particle asset takes two, because the dropdown beside the title chooses
+ // between the effect and one of its emitters and those are different objects
+ // with different fields. They are two panes rather than one that rebuilds for
+ // the same reason as all the others: a pane is built once and only ever bound.
+ %this.registerPane("Particle", %this.createParticlePane());
+ %this.registerPane("Emitter", %this.createEmitterPane());
+
+ // Named handles for the ones the tests and the load methods reach for
+ // directly. The registry is the truth; these are just shorter.
+ %this.imageScroller = %this.paneScroller["Image"];
+ %this.imagePane = %this.pane["Image"];
+ %this.animationPane = %this.pane["Animation"];
+ %this.fontPane = %this.pane["Font"];
+ %this.soundPane = %this.pane["Sound"];
+ %this.particlePane = %this.pane["Particle"];
+ %this.emitterPane = %this.pane["Emitter"];
+
//Particle Graph Tool
%this.scaleGraphPage = %this.createTabPage("Scale Graph", "AssetParticleGraphTool", "");
@@ -135,12 +186,20 @@
return %page;
}
+// Fill, not width/height. A scroller here is its tab page's only child and wants
+// the whole content rect, and the two flags answer that differently: "height"
+// keeps the gap to each edge that the control had when it was added, so a page
+// resized twice -- once when it joined the book and again when the frame set
+// gave the window its real size -- left the scroller 12 pixels taller than the
+// page holding it, and the page clipped the 12 pixels at the bottom. That is the
+// scroll bar's down arrow. Fill recomputes from the parent's content rect every
+// time, so it cannot drift. (GuiEditorInspectorWindow says the same thing.)
function AssetInspector::createScroller(%this)
{
%scroller = new GuiScrollCtrl()
{
- HorizSizing="width";
- VertSizing="height";
+ HorizSizing="fill";
+ VertSizing="fill";
Position="0 0";
Extent="700 320";
hScrollBar="alwaysOff";
@@ -194,6 +253,200 @@
return %inspector;
}
+// The scroller is 700 wide with a bar always on, so the pane lays out against
+// what is left. "width" from there: the pane follows the frame as it is dragged,
+// and its grids answer by reflowing into more or fewer columns.
+function AssetInspector::createImagePane(%this)
+{
+ %width = 686;
+
+ return new GuiChainCtrl()
+ {
+ class = "AssetImageInspectorPane";
+ superclass = "AssetInspectorPane";
+ HorizSizing = "width";
+ Position = "0 0";
+ Extent = %width SPC 320;
+ IsVertical = true;
+ ChildSpacing = 6;
+ paneWidth = %width;
+ };
+}
+
+// Give a custom pane its own scroller on the Inspector page and remember it
+// under a key. Built once, here, and never rebuilt or freed.
+function AssetInspector::registerPane(%this, %key, %pane)
+{
+ %scroller = %this.createScroller();
+ %scroller.setVisible(false);
+ %this.insPage.add(%scroller);
+
+ %scroller.add(%pane);
+ %pane.build();
+
+ %this.paneScroller[%key] = %scroller;
+ %this.pane[%key] = %pane;
+ %this.paneKeys = (%this.paneKeys $= "") ? %key : (%this.paneKeys SPC %key);
+}
+
+function AssetInspector::createAnimationPane(%this)
+{
+ %width = 686;
+
+ return new GuiChainCtrl()
+ {
+ class = "AssetAnimationInspectorPane";
+ superclass = "AssetInspectorPane";
+ HorizSizing = "width";
+ Position = "0 0";
+ Extent = %width SPC 320;
+ IsVertical = true;
+ ChildSpacing = 6;
+ paneWidth = %width;
+ };
+}
+
+function AssetInspector::createFontPane(%this)
+{
+ %width = 686;
+
+ return new GuiChainCtrl()
+ {
+ class = "AssetFontInspectorPane";
+ superclass = "AssetInspectorPane";
+ HorizSizing = "width";
+ Position = "0 0";
+ Extent = %width SPC 320;
+ IsVertical = true;
+ ChildSpacing = 6;
+ paneWidth = %width;
+ };
+}
+
+function AssetInspector::createSoundPane(%this)
+{
+ %width = 686;
+
+ return new GuiChainCtrl()
+ {
+ class = "AssetSoundInspectorPane";
+ superclass = "AssetInspectorPane";
+ HorizSizing = "width";
+ Position = "0 0";
+ Extent = %width SPC 320;
+ IsVertical = true;
+ ChildSpacing = 6;
+ paneWidth = %width;
+ };
+}
+
+function AssetInspector::createParticlePane(%this)
+{
+ %width = 686;
+
+ return new GuiChainCtrl()
+ {
+ class = "AssetParticleInspectorPane";
+ superclass = "AssetInspectorPane";
+ HorizSizing = "width";
+ Position = "0 0";
+ Extent = %width SPC 320;
+ IsVertical = true;
+ ChildSpacing = 6;
+ paneWidth = %width;
+ };
+}
+
+function AssetInspector::createEmitterPane(%this)
+{
+ %width = 686;
+
+ return new GuiChainCtrl()
+ {
+ class = "AssetEmitterInspectorPane";
+ superclass = "AssetInspectorPane";
+ HorizSizing = "width";
+ Position = "0 0";
+ Extent = %width SPC 320;
+ IsVertical = true;
+ ChildSpacing = 6;
+ paneWidth = %width;
+ };
+}
+
+// Which inspector the Inspector page is showing: a registered pane by key, or ""
+// for the generic one. The panes standing down are hidden rather than emptied,
+// so nothing they hold is ever freed while the engine might be dispatching on it
+// -- but they are unbound, so a stale target cannot be written to.
+function AssetInspector::chooseInspector(%this, %key)
+{
+ %this.insScroller.setVisible(%key $= "");
+ %this.activePane = "";
+
+ %count = getWordCount(%this.paneKeys);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %thisKey = getWord(%this.paneKeys, %i);
+ %chosen = (%thisKey $= %key);
+
+ %this.paneScroller[%thisKey].setVisible(%chosen);
+
+ if(%chosen)
+ {
+ %this.activePane = %this.pane[%thisKey];
+ }
+ else
+ {
+ %this.pane[%thisKey].unbind();
+ }
+ }
+}
+
+// The pane currently standing in for the inspector, or "" when the generic one
+// is on show. The one accessor everything below goes through, so there is no
+// second place that has to know how many panes there are.
+function AssetInspector::activePaneObject(%this)
+{
+ return %this.activePane;
+}
+
+// What the title bar's delete button acts on. The generic inspector knows what
+// it was handed; a pane has to be asked, because it is not one.
+function AssetInspector::inspectedObject(%this)
+{
+ %pane = %this.activePaneObject();
+ if(isObject(%pane))
+ {
+ return %pane.target;
+ }
+
+ return %this.inspector.getInspectObject();
+}
+
+// An asset changed -- possibly the one on show, possibly one it depends on.
+// AssetBase::onRefresh sends every one of them here; the pane decides whether it
+// is the one it is bound to.
+function AssetInspector::onAssetRefreshed(%this, %asset)
+{
+ %pane = %this.activePaneObject();
+ if(isObject(%pane))
+ {
+ %pane.onAssetRefreshed(%asset);
+ }
+}
+
+// The four fields no asset kind wants to see, and the inspect call that follows
+// them. Repeated verbatim in five load methods before this.
+function AssetInspector::inspectStock(%this, %asset)
+{
+ %this.inspector.clearHiddenFields();
+ %this.inspector.addHiddenField("hidden");
+ %this.inspector.addHiddenField("locked");
+ %this.inspector.addHiddenField("AssetInternal");
+ %this.inspector.addHiddenField("AssetPrivate");
+ %this.inspector.inspect(%asset);
+}
+
function AssetInspector::hideInspector(%this)
{
%this.titlebar.setText("");
@@ -201,6 +454,245 @@
%this.tabBook.Visible = false;
%this.emitterButtonBar.visible = false;
%this.deleteAssetButton.visible = false;
+ %this.documentButtonBar.visible = false;
+ %this.document = "";
+
+ // Nothing is selected, so nothing is bound. The pane keeps its rows.
+ %this.chooseInspector("");
+}
+
+//-----------------------------------------------------------------------------
+// The document bar: Save, Revert, Duplicate, Undo, Redo.
+//
+// These act on the asset as a whole. Note the asset they act on is the ASSET,
+// never the emitter that may be showing in the inspector instead -- an emitter
+// has no file of its own, and saving one means saving the particle asset that
+// owns it. documentAsset() is what settles that.
+//-----------------------------------------------------------------------------
+
+// Start keeping undo history for an asset, and show the bar. Every load method
+// calls this with whatever it just put on show.
+function AssetInspector::beginDocument(%this, %asset)
+{
+ if(!isObject(%asset))
+ {
+ return;
+ }
+
+ AssetAdmin.undoRecorder.track(%asset);
+
+ %this.document = %asset;
+ %this.documentButtonBar.visible = true;
+ %this.refreshDocumentBar();
+}
+
+// The asset the document commands act on: the one beginDocument was handed,
+// which is the one with a file.
+//
+// Remembered rather than worked out from what is on screen. Deducing it went
+// through inspectedObject(), and that has two holes: it is answered by the
+// active pane, which every load method binds AFTER calling beginDocument, so
+// during the refresh that follows a load there is no pane to ask yet; and when
+// there is no pane it falls through to whatever the generic inspector was last
+// given, which nothing clears when the selection goes away. Neither showed while
+// only the document bar asked - the bar is hidden in exactly those moments - but
+// the menus are never hidden, and both "this asset" and "no asset" are things
+// they have to be able to say.
+//
+// It also settles the particle case for free. An emitter is what the rows edit
+// and what inspectedObject answers with, but an emitter has no file of its own;
+// saving one means saving the particle asset that owns it. That asset is what
+// beginDocument was given, so it is what comes back here whichever emitter the
+// title dropdown is showing.
+function AssetInspector::documentAsset(%this)
+{
+ return isObject(%this.document) ? %this.document : 0;
+}
+
+function AssetInspector::refreshDocumentBar(%this)
+{
+ if(%this.documentButtonBar.visible)
+ {
+ %this.documentButtonBar.refreshEnabled();
+ }
+
+ // Outside the guard above, deliberately. The bar is hidden whenever nothing
+ // is selected; the menus never are, and "nothing is selected" is exactly what
+ // they have to be able to say. The predicates answer correctly either way.
+ if(isObject(AssetAdmin.menus))
+ {
+ AssetAdmin.menus.refresh();
+ }
+}
+
+function AssetInspector::getSaveAssetEnabled(%this)
+{
+ %asset = %this.documentAsset();
+
+ return isObject(%asset) && %asset.isAssetDirty();
+}
+
+// Revert is offered for exactly as long as there is something to throw away.
+function AssetInspector::getRevertAssetEnabled(%this)
+{
+ return %this.getSaveAssetEnabled();
+}
+
+function AssetInspector::getUndoAssetEnabled(%this)
+{
+ %asset = %this.documentAsset();
+
+ return isObject(%asset) && AssetAdmin.undoRecorder.getUndoCount(%asset.getAssetId()) > 0;
+}
+
+function AssetInspector::getRedoAssetEnabled(%this)
+{
+ %asset = %this.documentAsset();
+
+ return isObject(%asset) && AssetAdmin.undoRecorder.getRedoCount(%asset.getAssetId()) > 0;
+}
+
+function AssetInspector::getUndoAssetTooltip(%this)
+{
+ %asset = %this.documentAsset();
+ if(!isObject(%asset))
+ {
+ return "Undo";
+ }
+
+ %label = AssetAdmin.undoRecorder.getUndoLabel(%asset.getAssetId());
+
+ return (%label $= "") ? "Undo" : ("Undo" SPC %label);
+}
+
+function AssetInspector::getRedoAssetTooltip(%this)
+{
+ %asset = %this.documentAsset();
+ if(!isObject(%asset))
+ {
+ return "Redo";
+ }
+
+ %label = AssetAdmin.undoRecorder.getRedoLabel(%asset.getAssetId());
+
+ return (%label $= "") ? "Redo" : ("Redo" SPC %label);
+}
+
+function AssetInspector::SaveAsset(%this)
+{
+ %asset = %this.documentAsset();
+ if(!isObject(%asset) || !%asset.isAssetDirty())
+ {
+ return;
+ }
+
+ %assetId = %asset.getAssetId();
+
+ if(!%asset.saveAsset())
+ {
+ return;
+ }
+
+ AssetAdmin.undoRecorder.onAssetSaved(%assetId);
+ %this.refreshDocumentBar();
+}
+
+function AssetInspector::RevertAsset(%this)
+{
+ %asset = %this.documentAsset();
+ if(!isObject(%asset) || !%asset.isAssetDirty())
+ {
+ return;
+ }
+
+ if(!%asset.revertAsset())
+ {
+ return;
+ }
+
+ // The undo history described the document that was just thrown away.
+ AssetAdmin.undoRecorder.onAssetReverted(%asset);
+
+ // A revert can change anything, including which tabs and rows apply, so this
+ // reloads rather than refreshing in place.
+ %this.reloadDocument(%asset);
+}
+
+function AssetInspector::UndoAsset(%this)
+{
+ %asset = %this.documentAsset();
+ if(!isObject(%asset))
+ {
+ return;
+ }
+
+ if(AssetAdmin.undoRecorder.undo(%asset))
+ {
+ %this.reloadDocument(%asset);
+ }
+}
+
+function AssetInspector::RedoAsset(%this)
+{
+ %asset = %this.documentAsset();
+ if(!isObject(%asset))
+ {
+ return;
+ }
+
+ if(AssetAdmin.undoRecorder.redo(%asset))
+ {
+ %this.reloadDocument(%asset);
+ }
+}
+
+function AssetInspector::DuplicateAsset(%this)
+{
+ %asset = %this.documentAsset();
+ if(!isObject(%asset))
+ {
+ return;
+ }
+
+ // One field is 50, the feedback line is 96 with room to grow into, and the
+ // buttons want 34 and a margin. Plus the 34 the title bar and border take out
+ // of the window before the content sees any of it.
+ %width = 420;
+ %height = 250;
+
+ %dialog = new GuiControl()
+ {
+ class = "DuplicateAssetDialog";
+ superclass = "EditorDialog";
+ dialogSize = (%width + 8) SPC (%height + 8);
+ dialogCanClose = true;
+ dialogResizable = false;
+ dialogText = "Duplicate Asset";
+ sourceAssetId = %asset.getAssetId();
+ };
+ %dialog.init(%width, %height);
+
+ Canvas.pushDialog(%dialog);
+}
+
+// Put the asset back on show from scratch.
+//
+// An undo or a revert can move anything, including the things that decide which
+// tabs exist -- explicit mode, the emitter list, named cells mode -- so the tile
+// is re-clicked rather than the rows being refreshed in place. Clearing
+// chosenButton is what makes onClick treat it as a fresh selection instead of one
+// of the re-clicks it usually gets.
+function AssetInspector::reloadDocument(%this, %asset)
+{
+ %button = AssetAdmin.chosenButton;
+
+ if(isObject(%button))
+ {
+ AssetAdmin.chosenButton = "";
+ %button.onClick();
+ }
+
+ %this.refreshDocumentBar();
}
function AssetInspector::resetInspector(%this)
@@ -216,6 +708,10 @@
%this.emitterButtonBar.visible = false;
%this.deleteAssetButton.visible = true;
+
+ // Back to the generic inspector. The one asset kind with a pane of its own
+ // says so straight after.
+ %this.chooseInspector("");
}
function AssetInspector::loadImageAsset(%this, %imageAsset, %assetID)
@@ -226,13 +722,10 @@
%this.tabBook.selectPage(0);
%this.titlebar.setText("Image Asset:" SPC %imageAsset.AssetName);
- %this.inspector.clearHiddenFields();
- %this.inspector.addHiddenField("hidden");
- %this.inspector.addHiddenField("locked");
- %this.inspector.addHiddenField("AssetInternal");
- %this.inspector.addHiddenField("AssetPrivate");
- %this.inspector.addHiddenField("ExplicitMode");
- %this.inspector.inspect(%imageAsset);
+ %this.beginDocument(%imageAsset);
+
+ %this.chooseInspector("Image");
+ %this.imagePane.bind(%imageAsset, %assetID);
%this.imageFrameEditPage.inspect(%imageAsset);
%this.imageLayersEditPage.inspect(%imageAsset);
@@ -242,19 +735,25 @@
{
%this.resetInspector();
%this.titlebar.setText("Animation Asset:" SPC %animationAsset.AssetName);
+ %this.beginDocument(%animationAsset);
- %this.inspector.clearHiddenFields();
- %this.inspector.addHiddenField("hidden");
- %this.inspector.addHiddenField("locked");
- %this.inspector.addHiddenField("AssetInternal");
- %this.inspector.addHiddenField("AssetPrivate");
- %this.inspector.inspect(%animationAsset);
+ // Named cells come here too now.
+ //
+ // They used to fall through to the generic inspector, and there were two good
+ // reasons at the time: setNamedAnimationFrames split on whitespace while the
+ // field joined with commas, so a named list did not survive its own TAML file;
+ // and getNamedAnimationFrames formatted a StringTableEntry through %d, so what
+ // came back was a row of pointers. Both are fixed, and the pane reads a named
+ // animation the same way it reads a numbered one.
+ %this.chooseInspector("Animation");
+ %this.animationPane.bind(%animationAsset, %assetID);
}
function AssetInspector::loadParticleAsset(%this, %particleAsset, %assetID)
{
%this.resetInspector();
%this.titleDropDown.visible = true;
+ %this.beginDocument(%particleAsset);
%this.refreshParticleTitleDropDown(%particleAsset, 0);
%this.titleDropDown.Command = %this.getId() @ ".onChooseParticleAsset(" @ %particleAsset.getId() @ ");";
@@ -275,28 +774,28 @@
%this.titleDropDown.setCurSel(%index);
}
+// Index 0 is the effect; anything above it is one of its emitters. Each gets a
+// pane of its own and the graph tab that belongs with it.
function AssetInspector::onChooseParticleAsset(%this, %particleAsset)
{
%index = %this.titleDropDown.getSelectedItem();
- %this.inspector.clearHiddenFields();
%curSel = %this.tabBook.getSelectedPage();
- if(%index == 0)
+
+ if(%index <= 0)
{
- %this.inspector.addHiddenField("hidden");
- %this.inspector.addHiddenField("locked");
- %this.inspector.addHiddenField("AssetInternal");
- %this.inspector.addHiddenField("AssetPrivate");
- %this.inspector.inspect(%particleAsset);
+ %this.chooseInspector("Particle");
+ %this.particlePane.bind(%particleAsset, %particleAsset.getAssetId());
%this.tabBook.removeIfMember(%this.emitterGraphPage);
%this.tabBook.add(%this.scaleGraphPage);
%this.scaleGraphPage.inspect(%particleAsset);
}
- else if(%index > 0)
+ else
{
- %this.inspector.addHiddenField("hidden");
- %this.inspector.addHiddenField("locked");
- %this.inspector.inspect(%particleAsset.getEmitter(%index - 1));
+ %emitter = %particleAsset.getEmitter(%index - 1);
+
+ %this.chooseInspector("Emitter");
+ %this.emitterPane.bind(%emitter, %particleAsset.getAssetId());
%this.tabBook.removeIfMember(%this.scaleGraphPage);
%this.tabBook.add(%this.emitterGraphPage);
@@ -306,53 +805,83 @@
%this.emitterButtonBar.visible = true;
%this.emitterButtonBar.refreshEnabled();
+
+ // Solo and mute act on whichever emitter is selected, so moving the selection
+ // moves what they isolate -- and on the effect itself there is nothing to
+ // isolate and both stand down.
+ if(isObject(AssetAdmin.particleTransportBar))
+ {
+ AssetAdmin.particleTransportBar.refresh();
+ }
+}
+
+// Re-label the dropdown without disturbing what is selected or rebuilding the
+// pane under it. Renaming an emitter is the only thing that needs this: the name
+// is a field on the pane and the caption is a copy of it in the title bar.
+function AssetInspector::refreshEmitterLabels(%this)
+{
+ %asset = %this.documentAsset();
+ if(!isObject(%asset) || !%this.titleDropDown.isVisible())
+ {
+ return;
+ }
+
+ %this.refreshParticleTitleDropDown(%asset, %this.titleDropDown.getSelectedItem());
+}
+
+// Which emitter the dropdown is on, or "" when it is on the effect itself. The
+// one place that knows how the list maps to the asset, so nothing below has to
+// repeat the minus one.
+function AssetInspector::selectedEmitter(%this)
+{
+ %asset = %this.documentAsset();
+ %index = %this.titleDropDown.getSelectedItem();
+
+ if(!isObject(%asset) || %index <= 0 || %index > %asset.getEmitterCount())
+ {
+ return "";
+ }
+
+ return %asset.getEmitter(%index - 1);
}
function AssetInspector::loadFontAsset(%this, %fontAsset, %assetID)
{
%this.resetInspector();
%this.titlebar.setText("Font Asset:" SPC %fontAsset.AssetName);
+ %this.beginDocument(%fontAsset);
- %this.inspector.clearHiddenFields();
- %this.inspector.addHiddenField("hidden");
- %this.inspector.addHiddenField("locked");
- %this.inspector.addHiddenField("AssetInternal");
- %this.inspector.addHiddenField("AssetPrivate");
- %this.inspector.inspect(%fontAsset);
+ %this.chooseInspector("Font");
+ %this.fontPane.bind(%fontAsset, %assetID);
}
function AssetInspector::loadAudioAsset(%this, %audioAsset, %assetID)
{
%this.resetInspector();
%this.titlebar.setText("Audio Asset:" SPC %audioAsset.AssetName);
+ %this.beginDocument(%audioAsset);
- %this.inspector.clearHiddenFields();
- %this.inspector.addHiddenField("hidden");
- %this.inspector.addHiddenField("locked");
- %this.inspector.addHiddenField("AssetInternal");
- %this.inspector.addHiddenField("AssetPrivate");
- %this.inspector.inspect(%audioAsset);
+ %this.chooseInspector("Sound");
+ %this.soundPane.bind(%audioAsset, %assetID);
}
function AssetInspector::loadSpineAsset(%this, %spineAsset, %assetID)
{
%this.resetInspector();
%this.titlebar.setText("Spine Asset:" SPC %spineAsset.AssetName);
+ %this.beginDocument(%spineAsset);
- %this.inspector.clearHiddenFields();
- %this.inspector.addHiddenField("hidden");
- %this.inspector.addHiddenField("locked");
- %this.inspector.addHiddenField("AssetInternal");
- %this.inspector.addHiddenField("AssetPrivate");
- %this.inspector.inspect(%spineAsset);
+ %this.inspectStock(%spineAsset);
}
function AssetInspector::deleteAsset(%this)
{
- %asset = %this.inspector.getInspectObject();
- if(%this.titleDropDown.visible && %this.titleDropDown.getSelectedItem() != 0)
+ // The asset, never the emitter showing in its place - deleting one emitter of
+ // a particle is RemoveEmitter's job, and this offers to take files off disk.
+ %asset = %this.documentAsset();
+ if(!isObject(%asset))
{
- %asset = %asset.getOwner();
+ return;
}
%width = 700;
@@ -371,12 +900,24 @@ class = "DeleteAssetDialog";
Canvas.pushDialog(%dialog);
}
+//-----------------------------------------------------------------------------
+// The emitter bar beside the dropdown.
+//
+// Every one of these used to start from %this.inspector.getInspectObject() --
+// the generic inspector -- and work out from the dropdown index whether what it
+// answered was the asset or one of its emitters. That only held while index 0
+// went through the generic inspector, which it no longer does: the effect has a
+// pane of its own now and the generic inspector is never handed a particle at
+// all. They go through documentAsset() and selectedEmitter() instead, which are
+// true whichever inspector is on show.
+//-----------------------------------------------------------------------------
+
function AssetInspector::addEmitter(%this)
{
- %asset = %this.inspector.getInspectObject();
- if(%this.titleDropDown.getSelectedItem() != 0)
+ %asset = %this.documentAsset();
+ if(!isObject(%asset))
{
- %asset = %asset.getOwner();
+ return;
}
%width = 700;
@@ -397,88 +938,108 @@ class = "NewParticleEmitterDialog";
function AssetInspector::MoveEmitterForward(%this)
{
- %emitter = %this.inspector.getInspectObject();
- %asset = %emitter.getOwner();
+ %asset = %this.documentAsset();
%index = %this.titleDropDown.getSelectedItem();
- %asset.moveEmitter(%index-1, %index);
- %this.refreshParticleTitleDropDown(%asset, %index+1);
+ if(!isObject(%asset) || %index <= 0 || %index >= %asset.getEmitterCount())
+ {
+ return;
+ }
+
+ %asset.moveEmitter(%index - 1, %index);
+ %this.refreshParticleTitleDropDown(%asset, %index + 1);
%asset.refreshAsset();
+ %this.onChooseParticleAsset(%asset);
}
function AssetInspector::MoveEmitterBackward(%this)
{
- %emitter = %this.inspector.getInspectObject();
- %asset = %emitter.getOwner();
+ %asset = %this.documentAsset();
%index = %this.titleDropDown.getSelectedItem();
- %asset.moveEmitter(%index-1, %index-2);
- %this.refreshParticleTitleDropDown(%asset, %index-1);
+ // Index 1 is the FIRST emitter, so it has nowhere to go: moveEmitter(0, -1)
+ // is what the missing half of this test used to ask for.
+ if(!isObject(%asset) || %index <= 1)
+ {
+ return;
+ }
+
+ %asset.moveEmitter(%index - 1, %index - 2);
+ %this.refreshParticleTitleDropDown(%asset, %index - 1);
%asset.refreshAsset();
+ %this.onChooseParticleAsset(%asset);
}
function AssetInspector::RemoveEmitter(%this)
{
- %emitter = %this.inspector.getInspectObject();
- %asset = %emitter.getOwner();
- %asset.RemoveEmitter(%emitter, true);
+ %asset = %this.documentAsset();
+ %emitter = %this.selectedEmitter();
+
+ if(!isObject(%asset) || !isObject(%emitter))
+ {
+ return;
+ }
%index = %this.titleDropDown.getSelectedItem();
- %this.titleDropDown.deleteItem(%index);
+ %asset.RemoveEmitter(%emitter, true);
- if(%this.titleDropDown.getItemCount() <= %index)
+ // Rebuilt rather than deleteItem'd, so the captions cannot drift out of step
+ // with the emitters they name.
+ //
+ // Selection falls back to the emitter that took this one's place, or to the
+ // last one if this was the last -- and to the EFFECT at index 0 when the one
+ // removed was the only emitter. That last case is why this is clamped at all:
+ // it used to clamp to an item index of 0 and then ask for getEmitter(-1).
+ %count = %asset.getEmitterCount();
+ if(%index > %count)
{
- %index = %this.titleDropDown.getItemCount() - 1;
+ %index = %count;
}
- %this.titleDropDown.setCurSel(%index);
- %this.inspector.inspect(%asset.getEmitter(%index - 1));
- %this.emitterGraphPage.inspect(%asset, %index - 1);
- %this.emitterButtonBar.refreshEnabled();
+
+ %this.refreshParticleTitleDropDown(%asset, %index);
%asset.refreshAsset();
+ %this.onChooseParticleAsset(%asset);
}
+//-----------------------------------------------------------------------------
+// What the bar greys itself against. All three read the dropdown, which is the
+// thing the buttons act through -- they used to read emitterGraphPage.emitterID,
+// a tab page that is only on the book while an emitter is selected.
+//-----------------------------------------------------------------------------
+
function AssetInspector::getMoveEmitterForwardEnabled(%this)
{
- if(isObject(%this.titleDropDown) && %this.titleDropDown.getSelectedItem() <= 0)
+ %asset = %this.documentAsset();
+ %index = %this.titleDropDown.getSelectedItem();
+
+ if(!isObject(%asset) || %index <= 0)
{
return false;
}
- if(isObject(%this.inspector))
- {
- %asset = %this.inspector.getInspectObject();
- %emitterID = %this.emitterGraphPage.emitterID;
- return %emitterID != (%asset.getOwner().getEmitterCount() - 1);
- }
- return false;
+ // The last emitter is at item index getEmitterCount().
+ return %index < %asset.getEmitterCount();
}
function AssetInspector::getMoveEmitterBackwardEnabled(%this)
{
- if(isObject(%this.titleDropDown) && %this.titleDropDown.getSelectedItem() <= 0)
- {
- return false;
- }
- if(isObject(%this.inspector))
- {
- return %this.emitterGraphPage.emitterID != 0;
- }
- return false;
+ return isObject(%this.documentAsset()) && %this.titleDropDown.getSelectedItem() > 1;
}
function AssetInspector::getRemoveEmitterEnabled(%this)
{
- if(isObject(%this.titleDropDown) && %this.titleDropDown.getSelectedItem() <= 0)
+ %asset = %this.documentAsset();
+
+ if(!isObject(%asset) || %this.titleDropDown.getSelectedItem() <= 0)
{
return false;
}
- if(isObject(%this.inspector))
- {
- %asset = %this.inspector.getInspectObject();
- return %asset.getOwner().getEmitterCount() > 1;
- }
- return false;
+
+ // An effect with no emitters at all draws nothing, so the last one is not
+ // removable. RemoveEmitter above still handles the empty case, because a
+ // predicate is a greyed button rather than a guarantee.
+ return %asset.getEmitterCount() > 1;
}
diff --git a/editor/AssetAdmin/AssetLibraryWindow.cs b/editor/AssetAdmin/AssetLibraryWindow.cs
new file mode 100644
index 000000000..6434aca8b
--- /dev/null
+++ b/editor/AssetAdmin/AssetLibraryWindow.cs
@@ -0,0 +1,520 @@
+//AssetLibraryWindow.cs
+//
+// The Asset Library: a fixed toolbar over a scroller of collapsible asset
+// groups.
+//
+// toolbar view mode, sort field, and the search box
+// scroller everything else, so the toolbar never scrolls away
+// dictionaryList a chain of AssetDictionary panels, one per asset type
+//
+// The window owns all three. It also owns the three pieces of state the groups
+// share -- view mode, sort field and the search needle -- because all of them
+// apply to every group at once: a person looking for "rock" wants the rock
+// image AND the rock sound, and switching to rows is a statement about the
+// library, not about one type of asset.
+//
+// Groups are reached from elsewhere through AssetAdmin.Dictionary[%type], which
+// is what the New/Delete dialogs have always used; addDictionary keeps writing
+// it.
+
+$AssetLibraryWindow::toolbarHeight = 58;
+$AssetLibraryWindow::pad = 4;
+$AssetLibraryWindow::rowHeight = 24;
+$AssetLibraryWindow::searchY = 30;
+$AssetLibraryWindow::iconSize = 16;
+$AssetLibraryWindow::countWidth = 88;
+
+function AssetLibraryWindow::onAdd(%this)
+{
+ // The view and the sort are remembered between runs; the search box is not.
+ // A filter is about the thing you are doing right now, and reopening the
+ // editor to a library that is mysteriously missing most of its assets is a
+ // bug report waiting to happen.
+ %this.viewMode = EditorPreferences.get("assetLibraryViewMode", "grid");
+ %this.sortField = EditorPreferences.get("assetLibrarySortField", "name");
+ %this.dictionaryCount = 0;
+
+ %this.buildToolbar();
+
+ // Built filling the whole content rect, then moved down under the toolbar by
+ // fitScroller. Fill is the only way to find out how big that rect is: it is
+ // the window's extent less the title bar and whatever borders the profile
+ // asks for, and script cannot ask for any of those numbers.
+ %this.scroller = new GuiScrollCtrl()
+ {
+ HorizSizing = "fill";
+ VertSizing = "fill";
+ Position = "0 0";
+ Extent = "324 356";
+ MinExtent = "0 0";
+ hScrollBar = "alwaysOff";
+ vScrollBar = "alwaysOn";
+ constantThumbHeight = "0";
+ showArrowButtons = "1";
+ scrollBarThickness = "14";
+ };
+ ThemeManager.setProfile(%this.scroller, "scrollingPanelProfile");
+ ThemeManager.setProfile(%this.scroller, "scrollingPanelThumbProfile", "ThumbProfile");
+ ThemeManager.setProfile(%this.scroller, "scrollingPanelTrackProfile", "TrackProfile");
+ ThemeManager.setProfile(%this.scroller, "scrollingPanelArrowProfile", "ArrowProfile");
+ %this.add(%this.scroller);
+ %this.fitScroller();
+
+ // Fill across, and nothing about widths below. The horizontal bar is off, so
+ // across is an axis that does not scroll and the engine knows exactly how much
+ // room there is. Down is left alone; that axis scrolls, so the chain is as tall
+ // as its groups and GuiScrollCtrl refuses fill there.
+ %this.dictionaryList = new GuiChainCtrl()
+ {
+ HorizSizing = "fill";
+ Position = "0 0";
+ Extent = "310 4";
+ MinExtent = "0 0";
+ IsVertical = true;
+ ChildSpacing = 2;
+ };
+ ThemeManager.setProfile(%this.dictionaryList, "emptyProfile");
+ %this.scroller.add(%this.dictionaryList);
+
+ %this.populate();
+}
+
+function AssetLibraryWindow::onRemove(%this)
+{
+ %this.stopListening(ThemeManager);
+
+ // The toolbar and the scroller are this window's two children; the groups and
+ // their tiles hang off the scroller and go with it, and the toolbar's own
+ // controls go with the toolbar.
+ if(isObject(%this.toolbar))
+ {
+ %this.toolbar.delete();
+ }
+ if(isObject(%this.scroller))
+ {
+ %this.scroller.delete();
+ }
+}
+
+//-----------------------------------------------------------------------------
+// The toolbar.
+//-----------------------------------------------------------------------------
+
+function AssetLibraryWindow::buildToolbar(%this)
+{
+ %this.toolbar = new GuiControl()
+ {
+ HorizSizing = "width";
+ VertSizing = "bottom";
+ Position = "0 0";
+ Extent = "310" SPC $AssetLibraryWindow::toolbarHeight;
+ };
+ ThemeManager.setProfile(%this.toolbar, "emptyProfile");
+ %this.add(%this.toolbar);
+
+ // Two segmented rows rather than one: they answer different questions, and a
+ // four-button strip would read as one choice of four.
+ //
+ // labelWidth is 4, not 0 -- EditorChoiceRow::build sizes its caption at
+ // labelWidth - 4, so a zero asks for a control four pixels wide in the wrong
+ // direction and silently eats the whole row.
+ %this.viewRow = new GuiControl()
+ {
+ class = "EditorChoiceRow";
+ HorizSizing = "right";
+ Position = $AssetLibraryWindow::pad SPC 2;
+ labelText = "";
+ labelWidth = 4;
+ owner = %this;
+ fieldName = "viewMode";
+ };
+ %this.toolbar.add(%this.viewRow);
+ %this.viewRow.addChoice("grid", $EditorIcon::grid_2x2, "Show assets as a grid of thumbnails");
+ %this.viewRow.addChoice("rows", $EditorIcon::list_bullets, "Show assets as a compact list");
+ %this.viewRow.build();
+ %this.viewRow.setValue(%this.viewMode);
+
+ %this.sortRow = new GuiControl()
+ {
+ class = "EditorChoiceRow";
+ HorizSizing = "left";
+ Position = "0 2";
+ labelText = "";
+ labelWidth = 4;
+ owner = %this;
+ fieldName = "sortField";
+ };
+ %this.toolbar.add(%this.sortRow);
+ // font_size is the sheet's "Aa" pair, which is what every other tool uses for
+ // "alphabetical" -- there is no A-Z glyph here. text_letter_t was tried first
+ // and is a serif T whose crossbar and stem read as two bars at 16 pixels once
+ // the theme tints it.
+ %this.sortRow.addChoice("name", $EditorIcon::font_size, "Sort by asset name");
+ %this.sortRow.addChoice("category", $EditorIcon::tag, "Sort by asset category, then by name");
+ %this.sortRow.build();
+ %this.sortRow.setValue(%this.sortField);
+
+ // A funnel rather than the word "Search": the caption was clipped in every
+ // theme, and widening it would have come straight out of the box it labels --
+ // this pane is 324 wide and the sort row already owns the other end. The
+ // funnel also says the truer thing, since the box narrows the library in
+ // place rather than jumping to a hit.
+ // Sizing left at the default, which pins the top-left corner. NOT "center":
+ // that recentres the control in its PARENT on every resize, and the parent
+ // here is the whole 58-pixel toolbar -- so the icon jumped up to the middle of
+ // the bar, level with the view toggle, instead of staying level with the box.
+ %this.searchIcon = new GuiSpriteCtrl()
+ {
+ Extent = $AssetLibraryWindow::iconSize SPC $AssetLibraryWindow::iconSize;
+ MinExtent = $AssetLibraryWindow::iconSize SPC $AssetLibraryWindow::iconSize;
+ Position = $AssetLibraryWindow::pad SPC ($AssetLibraryWindow::searchY
+ + (($AssetLibraryWindow::rowHeight - $AssetLibraryWindow::iconSize) / 2));
+ Image = "EditorCore:EditorIcons16";
+ ImageSize = "16 16";
+ constrainProportions = "1";
+ fullSize = "0";
+ Frame = $EditorIcon::filter;
+ UseInput = false;
+ };
+ ThemeManager.setProfile(%this.searchIcon, "spriteProfile");
+ %this.toolbar.add(%this.searchIcon);
+
+ // The sheets are greyscale, drawn to be modulated -- an untinted icon blends
+ // with opaque white, which happens to look right on the theme the editor opens
+ // in and is a white smear on the light ones. And the tint has to be re-read on
+ // a theme change: ThemeManager swaps the profile object, which carries
+ // backgrounds and text for free, but a color COPIED onto a sprite stays behind.
+ %this.startListening(ThemeManager);
+ %this.refreshSearchIcon();
+
+ // Command fires on every keystroke, which is what makes the library narrow as
+ // you type; AltCommand would only fire when the box lost focus.
+ %this.searchBox = new GuiTextEditCtrl()
+ {
+ HorizSizing = "width";
+ Position = "0" SPC $AssetLibraryWindow::searchY;
+ Extent = "180" SPC $AssetLibraryWindow::rowHeight;
+ align = "left";
+ Tooltip = "Filter every group by asset name, description or category";
+ };
+ ThemeManager.setProfile(%this.searchBox, "textEditProfile");
+ ThemeManager.setProfile(%this.searchBox, "tipProfile", "TooltipProfile");
+ %this.searchBox.Command = %this.getID() @ ".onSearchChanged();";
+ %this.searchBox.EscapeCommand = %this.getID() @ ".clearSearch();";
+ %this.toolbar.add(%this.searchBox);
+
+ // labelProfile rather than infoProfile: this is a status line, and infoProfile
+ // draws a border and a fill, which reads as a second empty text box.
+ %this.countLabel = new GuiControl()
+ {
+ HorizSizing = "left";
+ Position = "0" SPC $AssetLibraryWindow::searchY;
+ Extent = $AssetLibraryWindow::countWidth SPC $AssetLibraryWindow::rowHeight;
+ Text = "";
+ align = "right";
+ vAlign = "middle";
+ };
+ ThemeManager.setProfile(%this.countLabel, "labelProfile");
+ %this.toolbar.add(%this.countLabel);
+}
+
+// The colour the caption this replaced would have been drawn in, so the icon
+// reads as part of the same row rather than as a picture sitting next to it.
+function AssetLibraryWindow::refreshSearchIcon(%this)
+{
+ %this.searchIcon.setImageColor(ThemeManager.activeTheme.labelProfile.fontColor);
+}
+
+function AssetLibraryWindow::onThemeChange(%this, %theme)
+{
+ %this.refreshSearchIcon();
+}
+
+// The three controls that hang off the right edge cannot be placed until the
+// content rect has been measured, so this is called from fitScroller with the
+// width it found. Their sizing flags hold them there through every later resize.
+function AssetLibraryWindow::layoutToolbar(%this, %width)
+{
+ %pad = $AssetLibraryWindow::pad;
+ %count = $AssetLibraryWindow::countWidth;
+ %y = $AssetLibraryWindow::searchY;
+
+ %this.toolbar.resize(0, 0, %width, $AssetLibraryWindow::toolbarHeight);
+
+ %sortWidth = getWord(%this.sortRow.getExtent(), 0);
+ %this.sortRow.setPosition(%width - %pad - %sortWidth, 2);
+
+ %boxLeft = %pad + $AssetLibraryWindow::iconSize + 6;
+ %boxWidth = %width - %boxLeft - %pad - %count - 6;
+ %this.searchBox.resize(%boxLeft, %y, %boxWidth, $AssetLibraryWindow::rowHeight);
+
+ %this.countLabel.setPosition(%width - %pad - %count, %y);
+}
+
+// Put the scroller under the toolbar without ever naming a border thickness.
+//
+// A fixed bar above a stretching pane has no sizing flag of its own: "fill"
+// takes the whole inner rect and throws the position away, so the bar ends up
+// underneath it, and "height" keeps whatever gaps the authored extent happened
+// to start with -- which means guessing the title height and the border sizes.
+//
+// So let the engine measure instead: the scroller is built filling, one resize
+// pass makes that real, and what it reports back IS the content rect. Take the
+// bar off the top of it and switch to "height", which from then on holds the top
+// edge where it was put and lets the bottom follow the window.
+//
+// Callable more than once, which the palette's version is not. The measurement
+// this makes is only as good as the profile the window is wearing at the time,
+// and AssetAdmin::buildLibrary applies the window profiles AFTER the new{} block
+// returns -- so the pass inside onAdd measures GuiDefaultProfile's title bar and
+// borders. Going back to fill before measuring makes a second pass, once the
+// real profiles are on and the frame set has sized the window, give the right
+// answer instead of subtracting the bar height twice.
+function AssetLibraryWindow::fitScroller(%this)
+{
+ %x = getWord(%this.getPosition(), 0);
+ %y = getWord(%this.getPosition(), 1);
+ %w = getWord(%this.getExtent(), 0);
+ %h = getWord(%this.getExtent(), 1);
+
+ %this.scroller.HorizSizing = "fill";
+ %this.scroller.VertSizing = "fill";
+
+ // Nudge the width by a pixel and back: one parentResized through every child,
+ // widths unchanged. The position is carried through rather than zeroed, so
+ // this does not move the window it is measuring.
+ %this.resize(%x, %y, %w + 1, %h);
+ %this.resize(%x, %y, %w, %h);
+
+ %inner = %this.scroller.getExtent();
+ %bar = $AssetLibraryWindow::toolbarHeight;
+
+ %this.layoutToolbar(getWord(%inner, 0));
+
+ %this.scroller.HorizSizing = "width";
+ %this.scroller.VertSizing = "height";
+ %this.scroller.resize(0, %bar, getWord(%inner, 0), getWord(%inner, 1) - %bar);
+}
+
+//-----------------------------------------------------------------------------
+// The groups.
+//-----------------------------------------------------------------------------
+
+function AssetLibraryWindow::populate(%this)
+{
+ %this.addDictionary("Images", "ImageAsset");
+ %this.addDictionary("Animations", "AnimationAsset");
+ %this.addDictionary("Particle Effects", "ParticleAsset");
+ %this.addDictionary("Fonts", "FontAsset");
+ %this.addDictionary("Audio", "AudioAsset");
+ //%this.addDictionary("Spines", "SpineAsset");
+}
+
+// Groups size with "width" rather than "fill": GuiExpandCtrl::parentResized
+// writes mExpandedExtent straight into mBounds.extent, bypassing resize(), which
+// is the only thing that honours fill.
+function AssetLibraryWindow::addDictionary(%this, %title, %type)
+{
+ %dictionary = new GuiPanelCtrl()
+ {
+ Class = "AssetDictionary";
+ Text = %title;
+ command = "";
+ HorizSizing = "width";
+ VertSizing = "bottom";
+ Position = "0 0";
+ Extent = "306 22";
+ MinExtent = "80 22";
+ Type = %type;
+ title = %title;
+ owner = %this;
+ viewMode = %this.viewMode;
+ sortField = %this.sortField;
+ };
+ %dictionary.setExpandEase("EaseInOut", 1000);
+ ThemeManager.setProfile(%dictionary, "panelProfile");
+ %this.dictionaryList.add(%dictionary);
+
+ %this.dictionary[%this.dictionaryCount] = %dictionary;
+ %this.dictionaryCount++;
+
+ // How the New and Delete dialogs have always found a group.
+ AssetAdmin.Dictionary[%type] = %dictionary;
+
+ return %dictionary;
+}
+
+function AssetLibraryWindow::loadAssets(%this)
+{
+ for(%i = 0; %i < %this.dictionaryCount; %i++)
+ {
+ %this.dictionary[%i].load();
+ }
+
+ %this.applyFilter();
+}
+
+// An asset's name, description or category was edited. The tile that shows it
+// cached all three when it was built, so it has to re-read them -- and then the
+// order and the filter it fed into are both potentially wrong.
+//
+// Which group holds it is not known here, so ask them all; that is the same move
+// DeleteAssetDialog makes, and there are five.
+function AssetLibraryWindow::onAssetRefreshed(%this, %assetID)
+{
+ for(%i = 0; %i < %this.dictionaryCount; %i++)
+ {
+ %button = %this.dictionary[%i].getButton(%assetID);
+ if(isObject(%button))
+ {
+ %button.refreshKeys();
+ %this.dictionary[%i].applySort(%this.sortField);
+ %this.applyFilter();
+ return;
+ }
+ }
+}
+
+// An asset gained or lost unsaved changes. Only the tile's caption is affected --
+// no re-sort and no re-filter, because the mark is not part of the name anything
+// is sorted or searched by.
+function AssetLibraryWindow::onAssetDirtyChanged(%this, %assetID)
+{
+ for(%i = 0; %i < %this.dictionaryCount; %i++)
+ {
+ %button = %this.dictionary[%i].getButton(%assetID);
+ if(isObject(%button))
+ {
+ %button.refreshDirtyMark();
+ return;
+ }
+ }
+}
+
+function AssetLibraryWindow::unloadAssets(%this)
+{
+ for(%i = 0; %i < %this.dictionaryCount; %i++)
+ {
+ %this.dictionary[%i].unload();
+ }
+}
+
+//-----------------------------------------------------------------------------
+// View mode and sort field.
+//-----------------------------------------------------------------------------
+
+function AssetLibraryWindow::onChoiceRowChanged(%this, %row)
+{
+ if(%row.fieldName $= "viewMode")
+ {
+ %this.setViewMode(%row.getValue());
+ }
+ else if(%row.fieldName $= "sortField")
+ {
+ %this.setSortField(%row.getValue());
+ }
+}
+
+function AssetLibraryWindow::setViewMode(%this, %mode)
+{
+ %this.viewMode = %mode;
+
+ for(%i = 0; %i < %this.dictionaryCount; %i++)
+ {
+ %this.dictionary[%i].setViewMode(%mode);
+ }
+
+ %this.relayout();
+
+ EditorPreferences.set("assetLibraryViewMode", %mode);
+}
+
+function AssetLibraryWindow::setSortField(%this, %field)
+{
+ %this.sortField = %field;
+
+ for(%i = 0; %i < %this.dictionaryCount; %i++)
+ {
+ %this.dictionary[%i].applySort(%field);
+ }
+
+ EditorPreferences.set("assetLibrarySortField", %field);
+}
+
+//-----------------------------------------------------------------------------
+// Search.
+//-----------------------------------------------------------------------------
+
+function AssetLibraryWindow::onSearchChanged(%this)
+{
+ %this.applyFilter();
+}
+
+function AssetLibraryWindow::clearSearch(%this)
+{
+ %this.searchBox.setText("");
+ %this.applyFilter();
+}
+
+// One needle, lowercased and trimmed once, then handed to every group. Each
+// group hides what does not match and reports how much survived, so the count
+// line can say "12 of 40" for the library as a whole.
+function AssetLibraryWindow::applyFilter(%this)
+{
+ %needle = strlwr(trim(%this.searchBox.getText()));
+ %shown = 0;
+ %total = 0;
+
+ for(%i = 0; %i < %this.dictionaryCount; %i++)
+ {
+ %dictionary = %this.dictionary[%i];
+ %shown += %dictionary.applyFilter(%needle);
+ %total += %dictionary.getButtonCount();
+ }
+
+ // "152/152" rather than "152 of 152": this shares a row with the search box in
+ // a 324 pane, and the words were clipped on a library of three figures. The
+ // compact form still fits at four, which no real project reaches.
+ %this.countLabel.setText(%shown @ "/" @ %total);
+
+ // settle(), not relayout(): no group changed width, so none of them needs the
+ // width nudge -- and this runs on every keystroke.
+ %this.settle();
+}
+
+//-----------------------------------------------------------------------------
+// Layout.
+//-----------------------------------------------------------------------------
+
+// A GuiPanelCtrl caches the height it opens to, so anything that changes the
+// size of what is inside one has to make it measure again -- and a chain
+// positions its children without resizing them, so it has to be told too.
+//
+// The cheap one: the groups are the width they already were, and only the number
+// of visible cells changed. This is the keystroke path.
+function AssetLibraryWindow::settle(%this)
+{
+ for(%i = 0; %i < %this.dictionaryCount; %i++)
+ {
+ %this.dictionary[%i].fixSize();
+ }
+
+ %w = getWord(%this.dictionaryList.getExtent(), 0);
+ %h = getWord(%this.dictionaryList.getExtent(), 1);
+ %this.dictionaryList.resize(0, 0, %w, %h);
+}
+
+// The full one, for a change of view mode: every cell is a different size now,
+// so each group needs the width nudge that makes its grid re-measure before the
+// panel measures the grid.
+function AssetLibraryWindow::relayout(%this)
+{
+ for(%i = 0; %i < %this.dictionaryCount; %i++)
+ {
+ %this.dictionary[%i].forceLayout();
+ }
+
+ %w = getWord(%this.dictionaryList.getExtent(), 0);
+ %h = getWord(%this.dictionaryList.getExtent(), 1);
+ %this.dictionaryList.resize(0, 0, %w, %h);
+}
diff --git a/editor/AssetAdmin/AssetPreviewSprite.cs b/editor/AssetAdmin/AssetPreviewSprite.cs
new file mode 100644
index 000000000..2cd5df5d8
--- /dev/null
+++ b/editor/AssetAdmin/AssetPreviewSprite.cs
@@ -0,0 +1,40 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2013 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+
+//-----------------------------------------------------------------------------
+// The sprite showing an animation in the Asset Manager's preview.
+//
+// It exists for one callback. A non-cycling animation reaching its end is the one
+// thing about playback that the engine announces and nothing else would notice --
+// the preview simply stops, and a Play button left showing "stop" would be lying
+// about what it does next.
+//-----------------------------------------------------------------------------
+
+function AssetPreviewSprite::onAnimationEnd(%this)
+{
+ if(!isObject(AssetAdmin.animationStage))
+ {
+ return;
+ }
+
+ AssetAdmin.animationStage.onPreviewFinished();
+}
diff --git a/editor/AssetAdmin/AssetUndoRecorder.cs b/editor/AssetAdmin/AssetUndoRecorder.cs
new file mode 100644
index 000000000..01c889fe9
--- /dev/null
+++ b/editor/AssetAdmin/AssetUndoRecorder.cs
@@ -0,0 +1,478 @@
+//-----------------------------------------------------------------------------
+// Undo and redo for assets.
+//
+// The unit of undo here is a whole asset, snapshotted. That is not the shape the
+// Gui Editor uses -- its recorder stores individual field writes -- and the
+// reason for the difference is that two of the Asset Manager's editors never
+// reach TorqueScript at all. GuiParticleGraphInspector does every graph key drag
+// in C++, and the stock GuiInspector writes particle, emitter, font and audio
+// fields straight onto the object. A recorder built out of script-side writes
+// would be blind to both, which is to say blind to the particle editor, which is
+// the thing that most needed undo in the first place.
+//
+// What every change does reach is AssetBase::onRefresh. So this keeps a snapshot
+// of each asset as it currently stands -- the baseline -- and when a change is
+// announced, the baseline is what the asset looked like before it. Push that,
+// take a new one, and the history builds itself no matter who made the change or
+// in what language.
+//
+// Snapshots are engine objects (AssetBase::createStateSnapshot). They are
+// unowned, so making one is inert: no file, no dirty mark, no notification, and
+// for an image no texture work.
+//
+// Each asset gets its own history, because assets are edited independently and
+// any of them can be left unsaved. See [[AssetAdmin]].
+//
+// NOTE ON ARGUMENTS: the methods that need to read or write an asset take the
+// asset OBJECT, not its id, and every caller already has one. Looking an asset up
+// by id from script means acquireAsset/releaseAsset, and that pair is not free of
+// consequences: releasing an asset whose reference count was zero unloads it, so
+// merely asking about an asset could delete it.
+//-----------------------------------------------------------------------------
+
+// How many steps to keep per asset. A snapshot of a particle asset with a few
+// emitters is not free, and no one is stepping back further than this by hand.
+$AssetUndoRecorder::maxSteps = 50;
+
+function AssetUndoRecorder::onAdd(%this)
+{
+ // The snapshots live in groups so that dropping a history deletes every
+ // snapshot in it -- a SimSet would leave them behind.
+ %this.history = new SimGroup();
+
+ // Nesting depth of the current transaction, and whether it has already
+ // pushed a step. See begin().
+ %this.txDepth = 0;
+ %this.txPushed = false;
+
+ // Set by whoever is about to change something, consumed by the step it
+ // produces.
+ %this.pendingLabel = "";
+
+ // True while this recorder is the one doing the changing, so that replaying a
+ // step does not record itself.
+ %this.replaying = false;
+}
+
+function AssetUndoRecorder::onRemove(%this)
+{
+ if(isObject(%this.history))
+ {
+ %this.history.deleteObjects();
+ %this.history.delete();
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Per-asset bookkeeping.
+//
+// Three things are kept for each asset: the baseline snapshot, a group of undo
+// steps and a group of redo steps, all indexed by asset id.
+//-----------------------------------------------------------------------------
+
+function AssetUndoRecorder::isTracking(%this, %assetId)
+{
+ return isObject(%this.undoGroup[%assetId]);
+}
+
+// Start keeping history for an asset, if we are not already. Called when an
+// asset is loaded into the inspector.
+function AssetUndoRecorder::track(%this, %asset)
+{
+ if(!isObject(%asset))
+ {
+ return;
+ }
+
+ %assetId = %asset.getAssetId();
+ if(%assetId $= "" || %this.isTracking(%assetId))
+ {
+ return;
+ }
+
+ %this.undoGroup[%assetId] = new SimGroup();
+ %this.redoGroup[%assetId] = new SimGroup();
+ %this.history.add(%this.undoGroup[%assetId]);
+ %this.history.add(%this.redoGroup[%assetId]);
+
+ %this.baseline[%assetId] = %this.takeSnapshot(%asset);
+}
+
+// Forget an asset's history. Reverting does this, because the steps describe a
+// document that no longer exists; so does deleting the asset.
+function AssetUndoRecorder::forget(%this, %assetId)
+{
+ if(!%this.isTracking(%assetId))
+ {
+ return;
+ }
+
+ %this.undoGroup[%assetId].deleteObjects();
+ %this.undoGroup[%assetId].delete();
+ %this.undoGroup[%assetId] = "";
+
+ %this.redoGroup[%assetId].deleteObjects();
+ %this.redoGroup[%assetId].delete();
+ %this.redoGroup[%assetId] = "";
+
+ if(isObject(%this.baseline[%assetId]))
+ {
+ %this.baseline[%assetId].delete();
+ }
+ %this.baseline[%assetId] = "";
+}
+
+// A snapshot of the asset as it stands, tagged with what its unsaved state was
+// at the time. The tag is what lets an undo that lands back on the saved state
+// report the asset as clean again.
+//
+// Every snapshot goes into the history group immediately, including the ones that
+// are only ever a baseline. createStateSnapshot hands back a registered object
+// that belongs to nobody, and a baseline is not in either stack -- so without a
+// home here it would still be alive after this recorder was deleted. Pushing one
+// onto a stack later just reparents it, which is what a SimGroup add does.
+function AssetUndoRecorder::takeSnapshot(%this, %asset)
+{
+ %snapshot = %asset.createStateSnapshot();
+
+ if(isObject(%snapshot))
+ {
+ %this.setWasDirty(%snapshot, %asset.isAssetDirty());
+ %this.setStepLabel(%snapshot, "");
+ %this.history.add(%snapshot);
+ }
+
+ return %snapshot;
+}
+
+//-----------------------------------------------------------------------------
+// What the recorder knows about each snapshot: what to call the step, and
+// whether the asset counted as unsaved at that point.
+//
+// Kept HERE, keyed by the snapshot's id, and deliberately not as fields on the
+// snapshot itself. A snapshot is copied onto the live asset when it is restored,
+// and copyFieldsFrom carries dynamic fields across -- so a stepLabel written on a
+// snapshot ends up on the asset, and from there into the asset's .taml the next
+// time it is saved. Which is exactly what happened: real content files grew
+// stepLabel="Set Frames" wasDirty="1" the first time anyone undid and saved.
+//
+// An object id can be reused once its snapshot has been deleted, but every
+// snapshot has both of these written by takeSnapshot before anything reads them,
+// so a recycled id is always overwritten rather than inherited.
+//-----------------------------------------------------------------------------
+
+function AssetUndoRecorder::setStepLabel(%this, %snapshot, %label)
+{
+ %this.stepLabelOf[%snapshot] = %label;
+}
+
+function AssetUndoRecorder::getStepLabel(%this, %snapshot)
+{
+ return %this.stepLabelOf[%snapshot];
+}
+
+function AssetUndoRecorder::setWasDirty(%this, %snapshot, %wasDirty)
+{
+ %this.wasDirtyOf[%snapshot] = %wasDirty;
+}
+
+function AssetUndoRecorder::getWasDirty(%this, %snapshot)
+{
+ return %this.wasDirtyOf[%snapshot];
+}
+
+//-----------------------------------------------------------------------------
+// Transactions.
+//
+// One thing the user did should be one step, and a couple of paths in the Asset
+// Manager write twice for a single action -- committing animation frames also
+// rewrites the frame rate when Keep Frame Rate is on. Wrapping those in
+// begin/end folds them together: the first change inside the transaction pushes
+// a step, and the rest only move the baseline forward.
+//
+// Nesting is by depth, so an inner begin/end pair inside an outer one is
+// absorbed rather than closing the transaction early.
+//-----------------------------------------------------------------------------
+
+function AssetUndoRecorder::begin(%this, %label)
+{
+ if(%this.txDepth == 0)
+ {
+ %this.txPushed = false;
+ %this.pendingLabel = %label;
+ }
+
+ %this.txDepth++;
+}
+
+function AssetUndoRecorder::end(%this)
+{
+ %this.txDepth--;
+
+ if(%this.txDepth <= 0)
+ {
+ %this.txDepth = 0;
+ %this.txPushed = false;
+ %this.pendingLabel = "";
+ }
+}
+
+// What the next step will be called. Set by the script that is about to make a
+// change and knows what to call it; anything that does not say gets "Edit",
+// which is what every change made in C++ gets.
+function AssetUndoRecorder::setLabel(%this, %label)
+{
+ if(%this.txDepth == 0)
+ {
+ %this.pendingLabel = %label;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Recording.
+//-----------------------------------------------------------------------------
+
+// An asset announced a change. %direct is false when the announcement is only a
+// cascade -- something this asset reads from was changed, and nothing this asset
+// saves has moved -- so there is nothing to record.
+function AssetUndoRecorder::onAssetChanged(%this, %asset, %direct)
+{
+ if(%this.replaying || !%direct || !isObject(%asset))
+ {
+ return;
+ }
+
+ %assetId = %asset.getAssetId();
+ if(!%this.isTracking(%assetId))
+ {
+ return;
+ }
+
+ // Inside a transaction, only the first change opens a step.
+ if(%this.txDepth > 0 && %this.txPushed)
+ {
+ %this.rebaseline(%asset);
+ return;
+ }
+
+ %before = %this.baseline[%assetId];
+ if(!isObject(%before))
+ {
+ %this.rebaseline(%asset);
+ return;
+ }
+
+ %this.setStepLabel(%before, (%this.pendingLabel $= "") ? "Edit" : %this.pendingLabel);
+
+ %this.undoGroup[%assetId].add(%before);
+ %this.trimHistory(%assetId);
+
+ // A new change is a new future: whatever was undone is no longer reachable.
+ %this.redoGroup[%assetId].deleteObjects();
+
+ // The baseline object was handed to the undo group, so this is a fresh one
+ // rather than a move.
+ %this.baseline[%assetId] = %this.takeSnapshot(%asset);
+
+ if(%this.txDepth > 0)
+ {
+ %this.txPushed = true;
+ }
+ else
+ {
+ %this.pendingLabel = "";
+ }
+
+ %this.refreshUI();
+}
+
+// Move the baseline to where the asset is now, without recording anything.
+function AssetUndoRecorder::rebaseline(%this, %asset)
+{
+ %assetId = %asset.getAssetId();
+
+ if(isObject(%this.baseline[%assetId]))
+ {
+ %this.baseline[%assetId].delete();
+ }
+
+ %this.baseline[%assetId] = %this.takeSnapshot(%asset);
+}
+
+function AssetUndoRecorder::trimHistory(%this, %assetId)
+{
+ %group = %this.undoGroup[%assetId];
+
+ while(%group.getCount() > $AssetUndoRecorder::maxSteps)
+ {
+ // The oldest step is the one at the front.
+ %oldest = %group.getObject(0);
+ %group.remove(%oldest);
+ %oldest.delete();
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Stepping.
+//-----------------------------------------------------------------------------
+
+function AssetUndoRecorder::getUndoCount(%this, %assetId)
+{
+ return %this.isTracking(%assetId) ? %this.undoGroup[%assetId].getCount() : 0;
+}
+
+function AssetUndoRecorder::getRedoCount(%this, %assetId)
+{
+ return %this.isTracking(%assetId) ? %this.redoGroup[%assetId].getCount() : 0;
+}
+
+function AssetUndoRecorder::getUndoLabel(%this, %assetId)
+{
+ %count = %this.getUndoCount(%assetId);
+
+ return (%count == 0) ? "" : %this.getStepLabel(%this.undoGroup[%assetId].getObject(%count - 1));
+}
+
+function AssetUndoRecorder::getRedoLabel(%this, %assetId)
+{
+ %count = %this.getRedoCount(%assetId);
+
+ return (%count == 0) ? "" : %this.getStepLabel(%this.redoGroup[%assetId].getObject(%count - 1));
+}
+
+function AssetUndoRecorder::undo(%this, %asset)
+{
+ if(!isObject(%asset))
+ {
+ return false;
+ }
+
+ %assetId = %asset.getAssetId();
+ if(%this.getUndoCount(%assetId) == 0)
+ {
+ return false;
+ }
+
+ %group = %this.undoGroup[%assetId];
+ %step = %group.getObject(%group.getCount() - 1);
+ %group.remove(%step);
+
+ // Where we are now becomes the way back.
+ %current = %this.baseline[%assetId];
+ %this.setStepLabel(%current, %this.getStepLabel(%step));
+ %this.redoGroup[%assetId].add(%current);
+
+ %this.applyStep(%asset, %step);
+
+ return true;
+}
+
+function AssetUndoRecorder::redo(%this, %asset)
+{
+ if(!isObject(%asset))
+ {
+ return false;
+ }
+
+ %assetId = %asset.getAssetId();
+ if(%this.getRedoCount(%assetId) == 0)
+ {
+ return false;
+ }
+
+ %group = %this.redoGroup[%assetId];
+ %step = %group.getObject(%group.getCount() - 1);
+ %group.remove(%step);
+
+ %current = %this.baseline[%assetId];
+ %this.setStepLabel(%current, %this.getStepLabel(%step));
+ %this.undoGroup[%assetId].add(%current);
+
+ %this.applyStep(%asset, %step);
+
+ return true;
+}
+
+// Put a snapshot back onto the asset and make it the new baseline.
+//
+// The snapshot is not deleted: it becomes the baseline, which is exactly what it
+// describes. The replaying guard stops the change this causes from being
+// recorded as a fresh step.
+function AssetUndoRecorder::applyStep(%this, %asset, %step)
+{
+ %assetId = %asset.getAssetId();
+
+ %this.replaying = true;
+ %asset.restoreStateSnapshot(%step);
+ %this.replaying = false;
+
+ // restoreStateSnapshot deliberately leaves the unsaved state alone, because
+ // only this knows what the restored state means: back at the last save is
+ // clean, anywhere else is not.
+ AssetDatabase.setAssetDirty(%assetId, %this.getWasDirty(%step));
+
+ // Taken off its stack above, so it needs a home again as the baseline.
+ %this.history.add(%step);
+ %this.baseline[%assetId] = %step;
+
+ %this.refreshUI();
+}
+
+//-----------------------------------------------------------------------------
+// Saving and reverting.
+//
+// Both settle the asset against its file, so both change what every step in its
+// history means about being saved.
+//-----------------------------------------------------------------------------
+
+// After a save, the state we are in is the saved one -- but every step still in
+// the history describes a state that is not, and so does every redo step.
+function AssetUndoRecorder::onAssetSaved(%this, %assetId)
+{
+ if(!%this.isTracking(%assetId))
+ {
+ return;
+ }
+
+ %undoGroup = %this.undoGroup[%assetId];
+ for(%i = 0; %i < %undoGroup.getCount(); %i++)
+ {
+ %this.setWasDirty(%undoGroup.getObject(%i), true);
+ }
+
+ %redoGroup = %this.redoGroup[%assetId];
+ for(%i = 0; %i < %redoGroup.getCount(); %i++)
+ {
+ %this.setWasDirty(%redoGroup.getObject(%i), true);
+ }
+
+ if(isObject(%this.baseline[%assetId]))
+ {
+ %this.setWasDirty(%this.baseline[%assetId], false);
+ }
+
+ %this.refreshUI();
+}
+
+// A revert throws the document away and starts again from the file, so the steps
+// that described the old one go with it.
+function AssetUndoRecorder::onAssetReverted(%this, %asset)
+{
+ if(!isObject(%asset))
+ {
+ return;
+ }
+
+ %this.forget(%asset.getAssetId());
+ %this.track(%asset);
+
+ %this.refreshUI();
+}
+
+function AssetUndoRecorder::refreshUI(%this)
+{
+ if(isObject(AssetAdmin.inspector))
+ {
+ AssetAdmin.inspector.refreshDocumentBar();
+ }
+}
diff --git a/editor/AssetAdmin/AssetWindow.cs b/editor/AssetAdmin/AssetWindow.cs
index 8f0268c4d..98358a329 100644
--- a/editor/AssetAdmin/AssetWindow.cs
+++ b/editor/AssetAdmin/AssetWindow.cs
@@ -46,7 +46,7 @@
Scene = AssetAdmin.AssetScene;
Image = %assetID;
size = %size;
- BlandColor = "1 1 1 1";
+ BlendColor = "1 1 1 1";
SceneLayer = 1;
Position = "0 0";
BodyType = static;
@@ -118,32 +118,52 @@
AssetAdmin.AssetScene.clear(true);
%size = %this.getWorldSize(%imageAsset.getFrameSize(0));
- new Sprite()
+ %sprite = new Sprite()
{
+ // It needs a class only so onAnimationEnd has somewhere to land -- that is
+ // how the transport bar learns a one-shot animation has finished.
+ class = "AssetPreviewSprite";
Scene = AssetAdmin.AssetScene;
Animation = %assetID;
size = %size;
- BlandColor = "1 1 1 1";
+ BlendColor = "1 1 1 1";
SceneLayer = 1;
Position = "0 0";
BodyType = static;
};
+
+ // This sprite is a different object every time -- the scene is cleared and
+ // rebuilt above -- so whatever was following the old one has to be told.
+ AssetAdmin.previewSprite = %sprite;
+ AssetAdmin.animationStage.onPreviewRebuilt(%sprite);
}
function AssetWindow::displayParticleAsset(%this, %particleAsset, %assetID)
{
AssetAdmin.AssetScene.clear(true);
- new ParticlePlayer()
+ // Fitted to the camera like the image and font previews are, rather than left
+ // at a hardcoded ten metres. A particle player's own size does not bound what
+ // it draws -- the emitters do that -- but it is what the emitter offsets and
+ // the size scale are measured against, so an effect authored around one scale
+ // arrived at another.
+ %size = %this.getWorldSize("10 10");
+
+ %player = new ParticlePlayer()
{
Scene = AssetAdmin.AssetScene;
Particle = %assetID;
- size = "10 10";
- BlandColor = "1 1 1 1";
+ size = %size;
+ BlendColor = "1 1 1 1";
SceneLayer = 1;
Position = "0 0";
BodyType = static;
};
+
+ // A different object every time -- the scene is cleared above -- so the
+ // transport, which drives this and nothing else, has to be handed the new one.
+ AssetAdmin.previewPlayer = %player;
+ AssetAdmin.showParticleTransport(%player, %assetID);
}
function AssetWindow::displayFontAsset(%this, %fontAsset, %assetID)
@@ -157,7 +177,7 @@
Font = %assetID;
fontSize = 4;
size = %size;
- BlandColor = "1 1 1 1";
+ BlendColor = "1 1 1 1";
SceneLayer = 1;
Position = "0 0";
BodyType = static;
@@ -173,6 +193,13 @@
{
AssetAdmin.AssetScene.clear(true);
+ // Before anything tries to make a sound. Nothing in the editor starts the
+ // audio driver -- only a game module ever did -- so until now alxPlay had no
+ // context to play through and answered with a null handle, and .wav was not
+ // even a registered resource extension. The Play button worked in the sense
+ // that it changed its own label.
+ AssetAdmin.ensureAudioDriver();
+
AssetAdmin.audioPlayButtonContainer.setVisible(true);
AssetAdmin.AssetWindow.setVisible(false);
@@ -241,6 +268,16 @@
%this.setCameraArea(%area);
%this.setViewLimitOn(%area);
+ // The animation stage gets first refusal. It resizes the sprite it already has
+ // rather than letting the whole preview be rebuilt, which would restart the
+ // animation every time a divider moved -- and while it is putting its split up
+ // or taking it down it answers for the resizes that causes, which are its own
+ // and not a reason to rebuild anything.
+ if(AssetAdmin.animationStage.absorbResize())
+ {
+ return;
+ }
+
if(isObject(AssetAdmin.chosenButton))
{
AssetAdmin.chosenButton.onClick();
diff --git a/editor/AssetAdmin/DuplicateAssetDialog.cs b/editor/AssetAdmin/DuplicateAssetDialog.cs
new file mode 100644
index 000000000..a04e8c187
--- /dev/null
+++ b/editor/AssetAdmin/DuplicateAssetDialog.cs
@@ -0,0 +1,189 @@
+//-----------------------------------------------------------------------------
+// Branch an asset.
+//
+// Duplicating copies the asset AS IT STANDS IN MEMORY, unsaved edits and all --
+// that is what makes it useful next to undo. Try something on a particle, decide
+// you want to keep both, duplicate, and the copy has what is on screen rather
+// than what was last written to disk.
+//
+// The copy is written and declared immediately. A declared asset has to have a
+// file: there is no such thing as an asset that exists only in memory and still
+// appears in the library. So the copy starts saved, and the original keeps
+// whatever unsaved state it had.
+//
+// The engine does the work in AssetManager::duplicateAsset. This is the name and
+// the module, and the checks that stop the copy landing somewhere that will not
+// have it.
+//-----------------------------------------------------------------------------
+
+function DuplicateAssetDialog::init(%this, %width, %height)
+{
+ //Get the dialog contents
+ %window = %this.getObject(0);
+ %content = %window.getObject(0);
+
+ %form = new GuiGridCtrl()
+ {
+ class = "EditorForm";
+ extent = %width SPC %height;
+ cellSizeX = %width;
+ cellSizeY = 50;
+ };
+ %form.addListener(%this);
+
+ %item = %form.addFormItem("New Asset Name", %width SPC 30);
+ %this.assetNameBox = %form.createTextEditItem(%item);
+ %this.assetNameBox.Command = %this.getId() @ ".Validate();";
+
+ %content.add(%form);
+
+ // Below whatever the form actually came out as, rather than below a number
+ // written here.
+ %formBottom = getWord(%form.getPosition(), 1) + getWord(%form.getExtent(), 1);
+
+ // The feedback line says quite a lot in the refusal cases -- the library
+ // module one runs to three lines at this width -- and textExtend grows the
+ // control downward to fit. Hence the height it starts at, and the clearance
+ // below it before the buttons.
+ %this.feedback = new GuiControl()
+ {
+ HorizSizing = "width";
+ VertSizing = "anchorTop";
+ Position = "12" SPC (%formBottom + 8);
+ Extent = (%width - 24) SPC 96;
+ text = "";
+ textWrap = true;
+ textExtend = true;
+ };
+ ThemeManager.setProfile(%this.feedback, "infoProfile");
+
+ // Measured from the room the content actually has, not from the dialog's own
+ // height -- the title bar and border take 34 of it.
+ %bottom = %this.contentHeight() - 12;
+
+ %this.cancelButton = new GuiButtonCtrl()
+ {
+ HorizSizing = "anchorRight";
+ VertSizing = "anchorBottom";
+ Position = (%width - 222) SPC (%bottom - 32);
+ Extent = "100 30";
+ Text = "Cancel";
+ Command = %this.getID() @ ".onClose();";
+ };
+ ThemeManager.setProfile(%this.cancelButton, "buttonProfile");
+
+ %this.duplicateButton = new GuiButtonCtrl()
+ {
+ HorizSizing = "anchorRight";
+ VertSizing = "anchorBottom";
+ Position = (%width - 112) SPC (%bottom - 34);
+ Extent = "100 34";
+ Text = "Duplicate";
+ Command = %this.getID() @ ".onDuplicate();";
+ };
+ ThemeManager.setProfile(%this.duplicateButton, "primaryButtonProfile");
+
+ %content.add(%this.feedback);
+ %content.add(%this.cancelButton);
+ %content.add(%this.duplicateButton);
+
+ // A name that is free to begin with, so the dialog opens ready to go.
+ %this.assetNameBox.setText(%this.suggestName());
+
+ %this.Validate();
+}
+
+// "thing" becomes "thingCopy", then "thingCopy2", "thingCopy3" and so on until
+// one of them is not taken.
+function DuplicateAssetDialog::suggestName(%this)
+{
+ %sourceName = AssetDatabase.getAssetName(%this.sourceAssetId);
+ %moduleName = getUnit(%this.sourceAssetId, 0, ":");
+
+ %candidate = %sourceName @ "Copy";
+ %suffix = 2;
+
+ while(AssetDatabase.isDeclaredAsset(%moduleName @ ":" @ %candidate))
+ {
+ %candidate = %sourceName @ "Copy" @ %suffix;
+ %suffix++;
+ }
+
+ return %candidate;
+}
+
+// Where the copy will be written: beside the asset it came from, with the same
+// file extension, so the module's DeclaredAssets glob picks it up. A copy that
+// landed under a different extension would be written and then never seen again.
+function DuplicateAssetDialog::targetPath(%this, %assetName)
+{
+ %sourcePath = AssetDatabase.getAssetFilePath(%this.sourceAssetId);
+
+ // fileBase strips only the last extension, and these are doubled --
+ // "rocket.image.taml" -- so take everything after the first dot of the name.
+ %sourceFile = fileName(%sourcePath);
+ %firstDot = strpos(%sourceFile, ".");
+ %extension = (%firstDot == -1) ? "asset.taml" : getSubStr(%sourceFile, %firstDot + 1, strlen(%sourceFile));
+
+ return pathConcat(filePath(%sourcePath), %assetName @ "." @ %extension);
+}
+
+function DuplicateAssetDialog::Validate(%this)
+{
+ %this.duplicateButton.active = false;
+
+ %assetName = %this.assetNameBox.getText();
+ %moduleName = getUnit(%this.sourceAssetId, 0, ":");
+
+ if(%assetName $= "")
+ {
+ %this.feedback.setText("The copy must have an Asset Name.");
+ return false;
+ }
+
+ if(AssetDatabase.isDeclaredAsset(%moduleName @ ":" @ %assetName))
+ {
+ %this.feedback.setText("An asset by this name already exists in this module. Try choosing a different name.");
+ return false;
+ }
+
+ %module = AssetDatabase.getAssetModule(%this.sourceAssetId);
+ if(isObject(%module) && %module.Synchronized)
+ {
+ %this.feedback.setText("You cannot add assets to a library module. Updates to the module would remove your assets. Instead, copy this asset into your own module.");
+ return false;
+ }
+
+ %this.duplicateButton.active = true;
+ %this.feedback.setText("The copy will be made beside the original, and will include any changes you have not saved.");
+ return true;
+}
+
+function DuplicateAssetDialog::onDuplicate(%this)
+{
+ if(!%this.Validate())
+ {
+ return;
+ }
+
+ %assetName = %this.assetNameBox.getText();
+ %moduleName = getUnit(%this.sourceAssetId, 0, ":");
+ %assetId = %moduleName @ ":" @ %assetName;
+ %assetType = AssetDatabase.getAssetType(%this.sourceAssetId);
+
+ if(!AssetDatabase.duplicateAsset(%this.sourceAssetId, %this.targetPath(%assetName), %assetName))
+ {
+ %this.feedback.setText("The copy could not be made. See the console for what went wrong.");
+ return;
+ }
+
+ // Put it in the library and select it, the same way a newly created asset is.
+ %button = AssetAdmin.Dictionary[%assetType].getButton(%assetId);
+ if(!isObject(%button))
+ {
+ %button = AssetAdmin.Dictionary[%assetType].addButton(%assetId);
+ }
+ %button.onClick();
+
+ %this.onClose();
+}
diff --git a/editor/AssetAdmin/ImageEditor/AssetImageFrameEditRow.cs b/editor/AssetAdmin/ImageEditor/AssetImageFrameEditRow.cs
index 69fdb6f3f..2b8d8c1fc 100644
--- a/editor/AssetAdmin/ImageEditor/AssetImageFrameEditRow.cs
+++ b/editor/AssetAdmin/ImageEditor/AssetImageFrameEditRow.cs
@@ -102,9 +102,9 @@
};
ThemeManager.setProfile(%this.buttonBar, "emptyProfile");
%this.add(%this.buttonBar);
- %this.buttonBar.addButton("MoveCellUp", 2, "Move Cell Up", "getMoveCellUpEnabled");
- %this.buttonBar.addButton("MoveCellDown", 6, "Move Cell Down", "getMoveCellDownEnabled");
- %this.buttonBar.addButton("RemoveCell", 23, "Remove Cell", "getRemoveCellEnabled");
+ %this.buttonBar.addButton("MoveCellUp", $EditorIcon::arrow_top, "Move Cell Up", "getMoveCellUpEnabled");
+ %this.buttonBar.addButton("MoveCellDown", $EditorIcon::arrow_bottom, "Move Cell Down", "getMoveCellDownEnabled");
+ %this.buttonBar.addButton("RemoveCell", $EditorIcon::round_delete, "Remove Cell", "getRemoveCellEnabled");
}
function AssetImageFrameEditRow::CellNameChange(%this)
diff --git a/editor/AssetAdmin/ImageEditor/AssetImageFrameEditTool.cs b/editor/AssetAdmin/ImageEditor/AssetImageFrameEditTool.cs
index 2d681af03..1091d43c8 100644
--- a/editor/AssetAdmin/ImageEditor/AssetImageFrameEditTool.cs
+++ b/editor/AssetAdmin/ImageEditor/AssetImageFrameEditTool.cs
@@ -141,10 +141,17 @@
%this.startListening(%row);
}
+// The name is the engine's to choose, and then read back.
+//
+// This used to build "Frame" @ %index itself, with no uniqueness check at all --
+// so adding a cell after deleting one from the middle produced a second cell
+// with a name that already existed, which onCellNameChange right below would have
+// refused had a person typed it. The engine names an unnamed cell on the way
+// through calculateExplicitMode, walking past any name already taken, and that is
+// now the only place the rule lives.
function AssetImageFrameEditTool::addNewCell(%this)
{
%index = %this.asset.getExplicitCellCount();
- %name = "Frame" @ %index;
%x = 0;
%y = 0;
%width = %this.asset.getImageWidth();
@@ -152,7 +159,11 @@
%this.rowChain.callOnChildrenNoRecurse("updateCellCount", %index + 1);
- %this.asset.addExplicitCell(%x, %y, %width, %height, %name);
+ // addExplicitCell refreshes the asset before it returns, so the name it picked
+ // is there to be read on the next line.
+ %this.asset.addExplicitCell(%x, %y, %width, %height, "");
+ %name = %this.asset.getExplicitCellName(%index);
+
%this.addImageFrameRow(%name, %x SPC %y, %width, %height, %index);
}
diff --git a/editor/AssetAdmin/ImageEditor/AssetImageLayersEditRow.cs b/editor/AssetAdmin/ImageEditor/AssetImageLayersEditRow.cs
index f4fa80740..d7ca02aec 100644
--- a/editor/AssetAdmin/ImageEditor/AssetImageLayersEditRow.cs
+++ b/editor/AssetAdmin/ImageEditor/AssetImageLayersEditRow.cs
@@ -1,4 +1,34 @@
+//-----------------------------------------------------------------------------
+// One layer of a composed image: its picture, where it sits, and the color it is
+// tinted with.
+//
+// Row 0 is not a layer the user added -- it is the asset's own image, the base
+// that every other row is drawn onto, and its tint is the asset's BlendColor.
+// The engine synthesizes it the moment a first real layer is added
+// (ImageAsset::insertLayer), which is why its image and offset boxes are inert:
+// they belong to the asset, not to a layer.
+//
+// Its color is inert too, but only while it is alone. ImageAsset::setBlendColor
+// stores the value, warns to the console and returns before redrawing anything
+// when there are no layers -- so with nothing composed onto the base there is
+// nothing for a tint to show up on. Once a layer exists the picker comes to
+// life.
+//
+// A locked picker wears a padlock rather than simply refusing to open. Every
+// other inert control in the editor shows it -- a greyed text box plainly is one
+// -- and a swatch does not: GuiColorPopupCtrl::onRender fills its face with the
+// color whatever state the control is in, so an inert swatch is pixel for pixel
+// a live one. The padlock is drawn at 30% black over a base color that is white
+// until somebody has a reason to change it.
+//-----------------------------------------------------------------------------
+
+// Where the color column sits, and how big the padlock over it is.
+$AssetImageLayersEditRow::colorX = 392;
+$AssetImageLayersEditRow::colorWidth = 164;
+$AssetImageLayersEditRow::lockSize = 16;
+$AssetImageLayersEditRow::lockTint = "0 0 0 77";
+
function AssetImageLayersEditRow::onAdd(%this)
{
%this.errorColor = "255 0 0 255";
@@ -62,20 +92,7 @@
%this.add(%this.offsetYBox);
%this.LayerColor = %this.scrubColor(%this.LayerColor);
- %this.colorBox = new GuiTextEditCtrl()
- {
- HorizSizing="width";
- VertSizing="height";
- Position="392 3";
- Extent="164 32";
- Align = right;
- Text = %this.LayerColor;
- AltCommand = %this.getID() @ ".LayerColorChange();";
- FontColor = %this.errorColor;
- InputMode = "AllText";
- };
- ThemeManager.setProfile(%this.colorBox, "textEditProfile");
- %this.add(%this.colorBox);
+ %this.buildColorColumn();
%this.buttonBar = new GuiChainCtrl()
{
@@ -91,9 +108,9 @@
if(%this.LayerIndex > 0)
{
- %this.buttonBar.addButton("MoveLayerUp", 2, "Move Layer Up", "getMoveLayerUpEnabled");
- %this.buttonBar.addButton("MoveLayerDown", 6, "Move Layer Down", "getMoveLayerDownEnabled");
- %this.buttonBar.addButton("RemoveLayer", 23, "Remove Layer", "");
+ %this.buttonBar.addButton("MoveLayerUp", $EditorIcon::arrow_top, "Move Layer Up", "getMoveLayerUpEnabled");
+ %this.buttonBar.addButton("MoveLayerDown", $EditorIcon::arrow_bottom, "Move Layer Down", "getMoveLayerDownEnabled");
+ %this.buttonBar.addButton("RemoveLayer", $EditorIcon::round_delete, "Remove Layer", "");
}
else
{
@@ -101,6 +118,109 @@
%this.offsetXBox.active = false;
%this.offsetYBox.active = false;
}
+
+ %this.refreshColorLock();
+}
+
+//-----------------------------------------------------------------------------
+// The color column: a swatch, and a padlock over it for when the swatch is not
+// yet worth using.
+//
+// A picker rather than the four numbers it used to be. The numbers are still
+// there -- showColorValues puts an R/G/B/A row inside the popup, so an exact
+// value can still be typed -- but a color is a thing you look at, and four
+// decimals in a text box is the one form in which you cannot.
+//
+// Both live in a container of their own so the padlock can be centred on the
+// swatch rather than on the row: "center" sizing recentres a control in its
+// PARENT, and with the row as the parent the padlock would sit in the middle of
+// the row instead of over the color.
+//-----------------------------------------------------------------------------
+
+function AssetImageLayersEditRow::buildColorColumn(%this)
+{
+ %w = $AssetImageLayersEditRow::colorWidth;
+ %h = 32;
+ %lock = $AssetImageLayersEditRow::lockSize;
+
+ %this.colorArea = new GuiControl()
+ {
+ HorizSizing = "width";
+ VertSizing = "height";
+ Position = $AssetImageLayersEditRow::colorX SPC 3;
+ Extent = %w SPC %h;
+ };
+ ThemeManager.setProfile(%this.colorArea, "emptyProfile");
+ %this.add(%this.colorArea);
+
+ %this.colorBox = new GuiColorPopupCtrl()
+ {
+ HorizSizing = "width";
+ VertSizing = "height";
+ Position = "0 0";
+ Extent = %w SPC %h;
+ showColorValues = true;
+ };
+ ThemeManager.setProfile(%this.colorBox, "colorPickerProfile");
+ ThemeManager.setProfile(%this.colorBox, "emptyProfile", "backgroundProfile");
+ ThemeManager.setProfile(%this.colorBox, "colorPopupProfile", "popupProfile");
+ ThemeManager.setProfile(%this.colorBox, "emptyProfile", "pickerProfile");
+ ThemeManager.setProfile(%this.colorBox, "colorPickerSelectorProfile", "selectorProfile");
+ ThemeManager.setProfile(%this.colorBox, "textEditProfile", "valueProfile");
+ // Passed on to the popup's R/G/B/A boxes, which name their channel with a
+ // tooltip; without it each would fall back to a profile of its own.
+ ThemeManager.setProfile(%this.colorBox, "tipProfile", "TooltipProfile");
+ %this.colorBox.Command = %this.getID() @ ".LayerColorChange();";
+ %this.colorArea.add(%this.colorBox);
+
+ %this.colorBox.setColorF(%this.LayerColor);
+
+ // Drawn after the swatch, so it draws over it, and deaf to input so a click
+ // aimed at the swatch is not swallowed by the thing explaining why the swatch
+ // will not respond.
+ %this.lockIcon = new GuiSpriteCtrl()
+ {
+ HorizSizing = "center";
+ VertSizing = "center";
+ Position = ((%w - %lock) / 2) SPC ((%h - %lock) / 2);
+ Extent = %lock SPC %lock;
+ MinExtent = %lock SPC %lock;
+ Image = "EditorCore:EditorIcons16";
+ ImageSize = "16 16";
+ constrainProportions = "1";
+ fullSize = "0";
+ Frame = $EditorIcon::padlock_closed;
+ UseInput = false;
+ Visible = false;
+ };
+ ThemeManager.setProfile(%this.lockIcon, "spriteProfile");
+ %this.colorArea.add(%this.lockIcon);
+
+ // Not the theme's label color, which is what every other icon in the editor
+ // takes: this one is read against the color being edited rather than against
+ // the panel, and the base is white until somebody has a reason to change it.
+ %this.lockIcon.setImageColor($AssetImageLayersEditRow::lockTint);
+}
+
+// Row 0 alone is the asset's own image with nothing composed onto it, and a tint
+// on that shows up nowhere. Every other row, and row 0 once it has company, is
+// live.
+function AssetImageLayersEditRow::refreshColorLock(%this)
+{
+ %this.setColorLocked(%this.LayerIndex == 0 && %this.LayerCount == 0);
+}
+
+function AssetImageLayersEditRow::setColorLocked(%this, %locked)
+{
+ %this.colorLocked = %locked;
+ %this.colorBox.setActive(!%locked);
+ %this.lockIcon.setVisible(%locked);
+
+ // The tooltip still reaches an inactive control -- the hit test does not ask
+ // whether a control is active -- so this is where the reason goes.
+ %this.colorBox.Tooltip = %locked
+ ? "This tints the base image that layers are drawn onto, and there are no layers yet. Add one and this unlocks."
+ : "";
}
function AssetImageLayersEditRow::LayerImageChange(%this)
@@ -154,10 +274,12 @@
}
}
+// getColorF, not getText: a layer's tint is a ColorF, so its four numbers run 0
+// to 1 and reading them off a ColorI swatch would round every one but white to
+// black.
function AssetImageLayersEditRow::LayerColorChange(%this)
{
- %color = %this.scrubColor(%this.colorBox.getText());
- %this.colorBox.setText(%color);
+ %color = %this.scrubColor(%this.colorBox.getColorF());
if(%color !$= %this.LayerColor)
{
@@ -186,10 +308,13 @@
return true;
}
+// Sent to every row whenever a layer is added or removed, which is also the only
+// thing that can lock or unlock row 0's color.
function AssetImageLayersEditRow::updateLayerCount(%this, %newCount)
{
%this.LayerCount = %newCount;
%this.buttonBar.refreshEnabled();
+ %this.refreshColorLock();
}
function AssetImageLayersEditRow::MoveLayerUp(%this)
@@ -213,7 +338,8 @@
%this.imageBox.setText(%this.LayerImage);
%this.offsetXBox.setText(getWord(%this.LayerPosition, 0));
%this.offsetYBox.setText(getWord(%this.LayerPosition, 1));
- %this.colorBox.setText(%this.LayerColor);
+ %this.colorBox.setColorF(%this.LayerColor);
+ %this.refreshColorLock();
}
function AssetImageLayersEditRow::onRemove(%this)
diff --git a/editor/AssetAdmin/Inspector/AssetAnimationInspectorPane.cs b/editor/AssetAdmin/Inspector/AssetAnimationInspectorPane.cs
new file mode 100644
index 000000000..5aff153a4
--- /dev/null
+++ b/editor/AssetAdmin/Inspector/AssetAnimationInspectorPane.cs
@@ -0,0 +1,327 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2013 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+
+//-----------------------------------------------------------------------------
+// The inspector for an animation asset, in place of the generic one: three
+// blocks that reflow, and a line saying what the numbers add up to.
+//
+// The single most important thing about it is a field that is NOT here.
+// AnimationFrames is the timeline, in the editor above. A box of space-separated
+// numbers beside a timeline editing the same list is two sources of truth, and
+// the one that is only a box cannot say which frame 67 is.
+//
+// Also absent, each for a checkable reason:
+// NamedAnimationFrames the same field as AnimationFrames, in
+// name space, and the timeline owns that
+// one too
+// NamedCellsMode not a field at all any more. Whether an
+// animation names its frames is read from
+// the image -- explicit mode means named
+// cells -- so a switch here would be a
+// second, disagreeing answer
+// AssetInternal, AssetPrivate they exist to keep an asset out of the
+// editor
+// asset id, asset file the module and the name are on show,
+// and the file is where the manager put it
+//-----------------------------------------------------------------------------
+
+$AssetAnimationInspectorPane::cellWidth = 300;
+$AssetAnimationInspectorPane::cellCount = 3;
+$AssetAnimationInspectorPane::descriptionHeight = 150;
+
+// A time of zero has no length, and worse: ImageFrameProviderCore divides the
+// total by the frame count to get a frame's length and then divides by that.
+$AssetAnimationInspectorPane::minimumTime = 0.01;
+
+function AssetAnimationInspectorPane::onAdd(%this)
+{
+ %this.init();
+}
+
+function AssetAnimationInspectorPane::buildPane(%this)
+{
+ %grid = %this.makeCellGrid(0, $AssetAnimationInspectorPane::cellWidth,
+ $AssetAnimationInspectorPane::cellCount);
+ %this.add(%grid);
+ %this.contentGrid = %grid;
+
+ %this.buildIdentityCell(%grid);
+ %this.buildPlaybackCell(%grid);
+ %this.buildDescriptionCell(%grid);
+
+ %this.buildWarning();
+
+ %this.nameRow.setEnabled(false, "Renaming an asset changes its id and every file that refers to it, " @
+ "so it is not something the inspector can do safely on its own.");
+}
+
+// addFieldRow takes the label and the kind as arguments rather than asking the
+// tables for them, so every call here would otherwise repeat the same two
+// lookups. One place to go through them is also one place to be wrong.
+function AssetAnimationInspectorPane::addField(%this, %container, %field)
+{
+ return %this.addFieldRow(%container, %field, %this.labelFor(%field),
+ %this.kindFor(%field), %this.enumItemsFor(%field));
+}
+
+function AssetAnimationInspectorPane::buildIdentityCell(%this, %grid)
+{
+ %chain = %this.makeCell(%grid, 4);
+ %this.identityChain = %chain;
+
+ %this.nameRow = %this.addField(%chain, "AssetName");
+ %this.addField(%chain, "AssetCategory");
+
+ // Kind "asset": EditorFieldRow's Find button opens the picker already filtered
+ // to image assets, which is the only asset an animation can name.
+ %this.addField(%chain, "Image");
+}
+
+function AssetAnimationInspectorPane::buildPlaybackCell(%this, %grid)
+{
+ %chain = %this.makeCell(%grid, 4);
+ %this.playbackChain = %chain;
+
+ %this.addField(%chain, "AnimationTime");
+ %this.addField(%chain, "AnimationCycle");
+ %this.addField(%chain, "RandomStart");
+
+ // Wrapping and extending: makeInfoLabel gives a label one line of 20 pixels,
+ // which is right for a short readout and not for a sentence about frame
+ // counts, sizes and rates. Without them the line is simply not drawn.
+ %this.infoLabel = %this.makeInfoLabel(%chain, "labelProfile");
+ %this.infoLabel.textWrap = true;
+ %this.infoLabel.textExtend = true;
+}
+
+function AssetAnimationInspectorPane::buildDescriptionCell(%this, %grid)
+{
+ %chain = %this.makeCell(%grid, 4);
+ %this.descriptionChain = %chain;
+
+ %this.addField(%chain, "AssetDescription");
+}
+
+function AssetAnimationInspectorPane::buildWarning(%this)
+{
+ %this.warningLabel = %this.makeInfoLabel(%this, "overrideLabelProfile");
+ %this.warningLabel.textWrap = true;
+ %this.warningLabel.textExtend = true;
+ %this.warningLabel.setVisible(false);
+}
+
+//-----------------------------------------------------------------------------
+// The field tables.
+//-----------------------------------------------------------------------------
+
+function AssetAnimationInspectorPane::labelFor(%this, %field)
+{
+ switch$(%field)
+ {
+ case "AssetName": return "Asset Name";
+ case "AssetCategory": return "Category";
+ case "Image": return "Image Asset";
+ case "AnimationTime": return "Animation Time (seconds)";
+ case "AnimationCycle": return "Loop";
+ case "RandomStart": return "Start On A Random Frame";
+ case "AssetDescription": return "Description";
+ }
+
+ return %field;
+}
+
+function AssetAnimationInspectorPane::kindFor(%this, %field)
+{
+ switch$(%field)
+ {
+ case "Image": return "asset";
+ case "AnimationTime": return "decimal";
+ case "AnimationCycle": return "bool";
+ case "RandomStart": return "bool";
+ case "AssetDescription": return "multiline";
+ }
+
+ return "text";
+}
+
+function AssetAnimationInspectorPane::editorHeightFor(%this, %field)
+{
+ if(%field $= "AssetDescription")
+ {
+ return $AssetAnimationInspectorPane::descriptionHeight;
+ }
+
+ return 0;
+}
+
+//-----------------------------------------------------------------------------
+// Reading and writing.
+//
+// RandomStart has no script accessor at all -- it is field-only -- which is
+// exactly what the base's readField and writeField already do through
+// getFieldValue and setFieldValue. No override; the note is here because the
+// missing accessor looks like an oversight rather than a decision.
+//-----------------------------------------------------------------------------
+
+function AssetAnimationInspectorPane::writeField(%this, %field, %value)
+{
+ if(%field $= "AnimationTime")
+ {
+ // Floored rather than refused, because the number a person is halfway
+ // through typing passes through here on its way to a sensible one.
+ if(%value < $AssetAnimationInspectorPane::minimumTime)
+ {
+ %value = $AssetAnimationInspectorPane::minimumTime;
+ }
+ %this.target.setAnimationTime(%value);
+ return;
+ }
+
+ %this.target.setFieldValue(%field, %value);
+}
+
+function AssetAnimationInspectorPane::refreshExtras(%this)
+{
+ %this.infoLabel.setText(%this.describeAnimation(%this.target));
+ %this.showWarning(%this.warningFor(%this.target));
+}
+
+// Everything the numbers add up to, which is the question the fields cannot
+// answer separately: how long, how many, and therefore how fast.
+function AssetAnimationInspectorPane::describeAnimation(%this, %asset)
+{
+ // getFrameCount, not getAnimationFrameCount: that one refuses to answer for an
+ // animation using named cells and returns -1, which read as "-1 frames" on
+ // this very line.
+ %count = %asset.getFrameCount();
+ %time = %asset.getAnimationTime();
+
+ %line = %count SPC ((%count == 1) ? "frame," : "frames,") SPC %time SPC "s";
+
+ if(%count > 0 && %time > 0)
+ {
+ %line = %line @ "," SPC mFloatLength(%count / %time, 1) SPC "per second";
+ }
+ %line = %line @ ".";
+
+ %image = AssetDatabase.acquireAsset(%asset.getImage());
+ if(isObject(%image))
+ {
+ %frameSize = %image.getFrameSize(0);
+ %line = %line SPC "Frames are" SPC getWord(%frameSize, 0) SPC "x" SPC getWord(%frameSize, 1) @
+ ", from" SPC %asset.getImage() SPC "(" @ %image.getImageWidth() SPC "x" SPC
+ %image.getImageHeight() @ "," SPC %image.getFrameCount() SPC "frames).";
+
+ AssetDatabase.releaseAsset(%asset.getImage());
+ }
+
+ return %line;
+}
+
+// In the order they matter. Only the first is shown, because the first is the one
+// that has to be fixed before any of the others can be judged.
+function AssetAnimationInspectorPane::warningFor(%this, %asset)
+{
+ %imageId = %asset.getImage();
+ if(%imageId $= "")
+ {
+ return "This animation has no image asset, so there is nothing to play.";
+ }
+
+ %image = AssetDatabase.acquireAsset(%imageId);
+ %valid = isObject(%image) && %image.getFrameCount() > 0;
+ if(isObject(%image))
+ {
+ AssetDatabase.releaseAsset(%imageId);
+ }
+
+ if(!%valid)
+ {
+ return "The image asset" SPC %imageId SPC "did not load, so there is nothing to play.";
+ }
+
+ // The two spaces fail differently, so they are reported differently.
+ //
+ // A named frame that no cell answers to is simply not drawn, and the timeline
+ // keeps it as an outlined gap -- so the useful thing to say is WHICH names,
+ // because the fix is either to put the cell back or to take the frame out.
+ if(%asset.getNamedCellsMode())
+ {
+ %missing = trim(%asset.getMissingFrames());
+ if(%missing !$= "")
+ {
+ %plural = (getWordCount(%missing) == 1);
+ return (%plural ? "The frame" : "The frames") SPC "\"" @ %missing @ "\"" SPC
+ (%plural ? "names a cell" : "name cells") SPC "the image no longer has, so" SPC
+ (%plural ? "it draws" : "they draw") SPC "nothing. Put the cell back on the " @
+ "Explicit Frames tab, or take the frame out of the timeline.";
+ }
+ }
+ // A numbered frame out of range is CLAMPED to the last one rather than
+ // dropped, so the animation keeps playing and quietly shows the wrong art.
+ // Specified against validated is the only comparison script can make, and it
+ // is exactly the right one.
+ else if(trim(%asset.getAnimationFrames()) !$= trim(%asset.getAnimationFrames(true)))
+ {
+ return "Some frames are outside the image's" SPC %image.getFrameCount() SPC
+ "and are being clamped to the nearest one. The timeline shows what was asked for; " @
+ "the preview shows what is being drawn.";
+ }
+
+ if(%asset.getFrameCount() == 0)
+ {
+ return "This animation has no frames yet. Drag one in from the palette, or use Frame Range.";
+ }
+
+ if(%asset.getAnimationTime() <= 0)
+ {
+ return "An animation time of zero has no length, so nothing plays.";
+ }
+
+ return "";
+}
+
+// forceLayout only when the visibility actually changed: a chain skips hidden
+// children when it lays out, and nothing re-lays it out on setVisible.
+function AssetAnimationInspectorPane::showWarning(%this, %text)
+{
+ %wanted = (%text !$= "");
+ %changed = (%wanted != %this.warningLabel.isVisible());
+
+ %this.warningLabel.setText(%text);
+ %this.warningLabel.setVisible(%wanted);
+
+ if(%changed)
+ {
+ %this.forceLayout();
+ }
+}
+
+// The timeline is showing the same list, so it has to hear about a change made
+// here -- picking a different image asset moves every frame's meaning.
+function AssetAnimationInspectorPane::afterCommit(%this)
+{
+ if(isObject(AssetAdmin.animationStage))
+ {
+ AssetAdmin.animationStage.onInspectorCommit();
+ }
+}
diff --git a/editor/AssetAdmin/Inspector/AssetEmitterInspectorPane.cs b/editor/AssetAdmin/Inspector/AssetEmitterInspectorPane.cs
new file mode 100644
index 000000000..53a1fda75
--- /dev/null
+++ b/editor/AssetAdmin/Inspector/AssetEmitterInspectorPane.cs
@@ -0,0 +1,710 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2013 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+
+//-----------------------------------------------------------------------------
+// The inspector for one emitter of a particle asset. The dropdown in the title
+// bar chooses which; index 0 is the asset itself and gets
+// AssetParticleInspectorPane instead.
+//
+// This is the pane the whole exercise was for. An emitter registers about thirty
+// persistent fields, and which of them are connected to anything depends on four
+// or five of the others -- a single-particle emitter ignores ten, a POINT emitter
+// ignores its size and its angle, a fixed-aspect emitter ignores every Size-Y
+// curve. The generic inspector listed all thirty flat, so the one thing it could
+// not tell you was which knobs were live.
+//
+// A header, then five blocks laid out by the same reflowing grid as every other
+// pane:
+//
+// Emission where particles are born
+// Aim & Orientation which way they are sent and which way they face
+// Particle Image what one looks like
+// Particle the switches that change what the other blocks mean
+// Render how it is drawn and in what order
+//
+// THE BLOCKS ARE SIZED BEFORE THEY ARE THEMED. A grid row is as tall as its
+// tallest cell, so blocks of unequal length do not make a short column and a long
+// one -- they make columns of the SAME height with the short ones mostly empty,
+// and the whole pane reads as badly laid out rather than as densely packed. Five
+// rows each is therefore a constraint on the grouping, not an outcome of it, and
+// the first cut (7 / 4 / 2 / 6 / 6) failed it badly.
+//
+// Two of the placements follow from that constraint and are worth defending on
+// their own terms as well:
+//
+// PivotPoint sits with Orientation, not with the particle switches, because it
+// is the point a particle is positioned and ROTATED about -- an orientation
+// concern wherever it is filed.
+//
+// AlphaTest sits with Particle Image, not with Render, because it is a
+// threshold on the image's own alpha channel. It belongs beside the image whose
+// transparency it reads.
+//
+// Aiming (IsTargeting / TargetPosition) moved out of Emission and in beside
+// Orientation, because both answer "which way", where Emission answers "where".
+//
+// The name and the "Emitter 1 of 2" line are the HEADER, above the grid and
+// across the full width, rather than a sixth block -- a two-row Identity block
+// beside a five-row one is the same failure in miniature. The same reasoning puts
+// the image pane's warning line outside its grid.
+//
+// TWO WAYS OF SAYING "this does not apply", and the difference is deliberate.
+//
+// SWAP when the fields are alternatives -- an emitter draws an image OR an
+// animation, never both, and an orientation has exactly one offset. The
+// arm that is not in use is hidden outright, because showing an
+// Animation row beside an Image row invites you to fill in both and one
+// of them would silently win.
+//
+// GREY when the field is real, holds a value, and this mode simply does not
+// read it. Hiding those would lose the value from sight and make the
+// pane jump around as you tried modes; greying keeps it where it was
+// and puts the reason in the tooltip.
+//
+// The 32 graph fields (Quantity, SizeX, Speed, Spin, the colour channels, and
+// each one's Variation and Life curves) are not here. Every one of them is a
+// curve over time rather than a number, and the Emitter Graph tab beside this one
+// is where a curve is drawn.
+//
+// Absent, each for a checkable reason:
+// PhysicsParticle, PhysicsParticleType their addProtectedField calls are
+// commented out in ParticleAssetEmitter.cc,
+// so they are not fields at all -- the
+// members and the enum table exist, and
+// nothing reads them
+// hidden, locked SimObject bookkeeping, as everywhere
+//-----------------------------------------------------------------------------
+
+$AssetEmitterInspectorPane::cellWidth = 300;
+$AssetEmitterInspectorPane::cellCount = 5;
+$AssetEmitterInspectorPane::headerWidth = 300;
+
+function AssetEmitterInspectorPane::onAdd(%this)
+{
+ // onAdd does not chain, so the shared setup runs from here.
+ %this.init();
+}
+
+//-----------------------------------------------------------------------------
+// Construction.
+//-----------------------------------------------------------------------------
+
+function AssetEmitterInspectorPane::buildPane(%this)
+{
+ %this.buildHeader();
+
+ %grid = %this.makeCellGrid(0, $AssetEmitterInspectorPane::cellWidth,
+ $AssetEmitterInspectorPane::cellCount);
+ %this.add(%grid);
+ %this.contentGrid = %grid;
+
+ // Reading order across a wide screen: where they are born, which way they go,
+ // what they look like, how they behave, how they are drawn.
+ %this.buildEmissionCell(%grid);
+ %this.buildOrientationCell(%grid);
+ %this.buildImageCell(%grid);
+ %this.buildBehaviorCell(%grid);
+ %this.buildRenderCell(%grid);
+}
+
+// The name and the line placing this emitter within the effect, above the grid
+// and outside it. The name box keeps a column's width rather than the pane's --
+// a name is a short thing, and a text box a metre wide invites a sentence.
+function AssetEmitterInspectorPane::buildHeader(%this)
+{
+ %chain = %this.makeChain(0, 4);
+ %this.add(%chain);
+ %this.headerChain = %chain;
+
+ %row = %this.addFieldRow(%chain, "EmitterName", %this.labelFor("EmitterName"), "text", "");
+ %row.HorizSizing = "right";
+ %row.setExtent($AssetEmitterInspectorPane::headerWidth, %row.rowHeight);
+
+ %this.infoLabel = %this.makeInfoLabel(%chain, "labelProfile");
+ %this.infoLabel.textWrap = true;
+ %this.infoLabel.textExtend = true;
+ %this.infoLabel.vAlign = "top";
+}
+
+// addFieldRow takes the label and the kind as arguments rather than asking the
+// tables for them, so every call here would otherwise repeat the same lookups.
+function AssetEmitterInspectorPane::addField(%this, %container, %field)
+{
+ return %this.addFieldRow(%container, %field, %this.labelFor(%field),
+ %this.kindFor(%field), %this.enumItemsFor(%field));
+}
+
+function AssetEmitterInspectorPane::buildEmissionCell(%this, %grid)
+{
+ %chain = %this.makeCell(%grid, 4);
+ %this.emissionChain = %chain;
+
+ // Type first: it decides whether the two rows under it are read.
+ %this.addField(%chain, "EmitterType");
+ %this.addField(%chain, "EmitterSize");
+ %this.addField(%chain, "EmitterAngle");
+ %this.addField(%chain, "EmitterOffset");
+ %this.addField(%chain, "LinkEmissionRotation");
+}
+
+function AssetEmitterInspectorPane::buildImageCell(%this, %grid)
+{
+ %chain = %this.makeCell(%grid, 4);
+ %this.imageChain = %chain;
+
+ // Not a field. There is no StaticMode on an emitter: the mode is a side
+ // effect of whichever of Image and Animation was written last, so the picker
+ // exists to make that choice sayable rather than accidental. It is built
+ // WITHOUT registering, so the refresh loop never tries to read a field of this
+ // name off the emitter -- refreshExtras sets it instead, and writeField below
+ // turns a change of it into the write that actually moves the mode.
+ %this.sourceRow = %this.makeFieldRow(%chain, "Source", "Drawn With", "dropdown", "");
+ %this.sourceRow.fillItems("Static Image" TAB "Animation");
+
+ %this.addField(%chain, "Image");
+ %this.addField(%chain, "RandomImageFrame");
+ %this.addField(%chain, "Frame");
+ %this.addField(%chain, "NamedFrame");
+ %this.addField(%chain, "Animation");
+
+ // A threshold on this image's own alpha channel, so it belongs beside the
+ // image rather than in Render with the blend rows.
+ %this.addField(%chain, "AlphaTest");
+}
+
+// Aim and orientation: which way particles are sent, and which way they face
+// once they are out. Only one of the three orientation arms is ever on show, so
+// the block is five rows in the default state and six for the two arms that
+// carry a second control.
+function AssetEmitterInspectorPane::buildOrientationCell(%this, %grid)
+{
+ %chain = %this.makeCell(%grid, 4);
+ %this.orientationChain = %chain;
+
+ %this.addField(%chain, "IsTargeting");
+ %this.addField(%chain, "TargetPosition");
+
+ %this.addField(%chain, "OrientationType");
+ %this.addField(%chain, "FixedAngleOffset");
+ %this.addField(%chain, "AlignedAngleOffset");
+ %this.addField(%chain, "KeepAligned");
+ %this.addField(%chain, "RandomAngleOffset");
+ %this.addField(%chain, "RandomArc");
+
+ %this.addField(%chain, "PivotPoint");
+}
+
+function AssetEmitterInspectorPane::buildBehaviorCell(%this, %grid)
+{
+ %chain = %this.makeCell(%grid, 4);
+ %this.behaviorChain = %chain;
+
+ %this.addField(%chain, "SingleParticle");
+ %this.addField(%chain, "FixedAspect");
+ %this.addField(%chain, "FixedForceAngle");
+ %this.addField(%chain, "AttachPositionToEmitter");
+ %this.addField(%chain, "AttachRotationToEmitter");
+}
+
+function AssetEmitterInspectorPane::buildRenderCell(%this, %grid)
+{
+ %chain = %this.makeCell(%grid, 4);
+ %this.renderChain = %chain;
+
+ %this.addField(%chain, "OldestInFront");
+ %this.addField(%chain, "IntenseParticles");
+ %this.addField(%chain, "BlendMode");
+ %this.addField(%chain, "SrcBlendFactor");
+ %this.addField(%chain, "DstBlendFactor");
+}
+
+//-----------------------------------------------------------------------------
+// The field tables.
+//-----------------------------------------------------------------------------
+
+function AssetEmitterInspectorPane::labelFor(%this, %field)
+{
+ switch$(%field)
+ {
+ case "EmitterName": return "Emitter Name";
+
+ case "EmitterType": return "Shape";
+ case "EmitterSize": return "Shape Size";
+ case "EmitterAngle": return "Shape Angle";
+ case "EmitterOffset": return "Offset";
+ case "LinkEmissionRotation": return "Follow Player Rotation";
+ case "IsTargeting": return "Aim At A Point";
+ case "TargetPosition": return "Target Position";
+
+ case "Image": return "Image";
+ case "RandomImageFrame": return "Random Frame";
+ case "Frame": return "Frame";
+ case "NamedFrame": return "Named Frame";
+ case "Animation": return "Animation";
+
+ case "OrientationType": return "Orientation";
+ case "FixedAngleOffset": return "Angle";
+ case "AlignedAngleOffset": return "Angle From Travel";
+ case "KeepAligned": return "Keep Aligned";
+ case "RandomAngleOffset": return "Centre Angle";
+ case "RandomArc": return "Arc";
+
+ case "SingleParticle": return "Single Particle";
+ case "FixedAspect": return "Fixed Aspect";
+ case "FixedForceAngle": return "Fixed Force Angle";
+ case "AttachPositionToEmitter": return "Attach Position";
+ case "AttachRotationToEmitter": return "Attach Rotation";
+ case "PivotPoint": return "Pivot Point";
+
+ case "OldestInFront": return "Oldest In Front";
+ case "IntenseParticles": return "Intense (Additive)";
+ case "BlendMode": return "Blending";
+ case "SrcBlendFactor": return "Source Factor";
+ case "DstBlendFactor": return "Destination Factor";
+ case "AlphaTest": return "Alpha Test";
+ }
+
+ return %field;
+}
+
+function AssetEmitterInspectorPane::kindFor(%this, %field)
+{
+ switch$(%field)
+ {
+ case "EmitterType" or "OrientationType" or "SrcBlendFactor" or "DstBlendFactor":
+ return "enum";
+
+ case "EmitterSize" or "EmitterOffset" or "TargetPosition" or "PivotPoint":
+ return "pointf";
+
+ case "EmitterAngle" or "FixedForceAngle" or "FixedAngleOffset" or "AlignedAngleOffset" or
+ "RandomAngleOffset" or "RandomArc" or "AlphaTest":
+ return "decimal";
+
+ case "Frame":
+ return "number";
+
+ case "Image" or "Animation":
+ return "asset";
+
+ case "LinkEmissionRotation" or "IsTargeting" or "RandomImageFrame" or "KeepAligned" or
+ "SingleParticle" or "FixedAspect" or "AttachPositionToEmitter" or
+ "AttachRotationToEmitter" or "OldestInFront" or "IntenseParticles" or "BlendMode":
+ return "bool";
+ }
+
+ return "text";
+}
+
+function AssetEmitterInspectorPane::enumItemsFor(%this, %field)
+{
+ // The engine's own labels, from the enum tables the fields are registered
+ // with. The drop-down's search ignores case, so the readable ones can be
+ // written as words; the GL blend factors are left exactly as they are,
+ // because they are names rather than English and an editor that renamed them
+ // would not match anything written about them.
+ switch$(%field)
+ {
+ // EmitterTypeTable, ParticleAssetEmitter.cc
+ case "EmitterType": return "Point" TAB "Line" TAB "Box" TAB "Disk" TAB "Ellipse" TAB "Torus";
+
+ // OrientationTypeTable, ParticleAssetEmitter.cc
+ case "OrientationType": return "Fixed" TAB "Aligned" TAB "Random";
+
+ // srcBlendFactorTable, SceneObject.cc
+ case "SrcBlendFactor": return "ZERO" TAB "ONE" TAB "DST_COLOR" TAB "ONE_MINUS_DST_COLOR" TAB
+ "SRC_ALPHA" TAB "ONE_MINUS_SRC_ALPHA" TAB "DST_ALPHA" TAB "ONE_MINUS_DST_ALPHA" TAB
+ "SRC_ALPHA_SATURATE";
+
+ // dstBlendFactorTable, SceneObject.cc
+ case "DstBlendFactor": return "ZERO" TAB "ONE" TAB "SRC_COLOR" TAB "ONE_MINUS_SRC_COLOR" TAB
+ "SRC_ALPHA" TAB "ONE_MINUS_SRC_ALPHA" TAB "DST_ALPHA" TAB "ONE_MINUS_DST_ALPHA";
+ }
+
+ return "";
+}
+
+// The emitter's two asset rows want different pickers, which is the whole reason
+// EditorFieldRow learned to be told.
+function AssetEmitterInspectorPane::assetTypeFor(%this, %field)
+{
+ switch$(%field)
+ {
+ case "Image": return "ImageAsset";
+ case "Animation": return "AnimationAsset";
+ }
+
+ return "";
+}
+
+// None of these fields carries a doc string in the engine, so the pane is where
+// the explanation lives. The ones whose names already say it are left out.
+function AssetEmitterInspectorPane::tipFor(%this, %field)
+{
+ switch$(%field)
+ {
+ case "Source": return "Whether each particle draws a still frame of an image asset or plays an " @
+ "animation. An emitter is one or the other -- choosing here clears the other one.";
+
+ case "EmitterType": return "The shape particles are born on. Point emits from a single spot. Line " @
+ "uses the X size as its half-length. Box and Disk fill their area; Ellipse and Torus emit on " @
+ "the edge, Torus between an outer and an inner radius.";
+
+ case "EmitterSize": return "The shape's dimensions, in world units.";
+
+ case "EmitterAngle": return "How far the shape itself is turned, in degrees. This rotates where " @
+ "particles are born, not which way they travel.";
+
+ case "EmitterOffset": return "Where the shape sits relative to the player's position.";
+
+ case "LinkEmissionRotation": return "Add the player's own rotation to the emission angle, so " @
+ "turning the player turns the spray with it.";
+
+ case "IsTargeting": return "Aim particles at a fixed point instead of using the Emission Angle " @
+ "graph. The Emission Arc graph still spreads them around that aim.";
+
+ case "TargetPosition": return "The point particles are aimed at while Aim At A Point is on.";
+
+ case "Image": return "The image asset each particle draws a frame of.";
+
+ case "RandomImageFrame": return "Give every particle a random frame of the image instead of the " @
+ "one chosen below. A sheet of different sparks or leaves is what this is for.";
+
+ case "Frame": return "Which frame of the image each particle draws, counting from zero.";
+
+ case "NamedFrame": return "Which named cell of the image each particle draws. Only an image with " @
+ "explicit named cells has these.";
+
+ case "Animation": return "The animation asset each particle plays. Every particle starts its own " @
+ "copy from the beginning.";
+
+ case "OrientationType": return "Which way a particle faces. Fixed points them all one way. Aligned " @
+ "points them along their direction of travel. Random gives each one its own angle.";
+
+ case "FixedAngleOffset": return "The angle every particle faces, in degrees.";
+
+ case "AlignedAngleOffset": return "Added to the direction of travel, in degrees -- so art drawn " @
+ "pointing up rather than right can be corrected here.";
+
+ case "KeepAligned": return "Keep turning particles as they change direction, instead of aiming " @
+ "them once when they are born.";
+
+ case "RandomAngleOffset": return "The middle of the range random angles are drawn from, in degrees.";
+
+ case "RandomArc": return "How wide that range is, in degrees. 360 is a completely free angle.";
+
+ case "SingleParticle": return "Emit one immortal particle at the offset instead of a stream. It " @
+ "never moves and never expires, so the whole Emission block, and the Lifetime and Quantity " @
+ "graphs, stop being read.";
+
+ case "FixedAspect": return "Keep particles square: the Size-Y graphs are ignored and height " @
+ "follows width.";
+
+ case "FixedForceAngle": return "Which way the Fixed Force graph pushes, in degrees. 90 is up, " @
+ "which is what a rising flame or smoke wants.";
+
+ case "AttachPositionToEmitter": return "Carry particles with the player as it moves, instead of " @
+ "leaving them behind in the world where they were born.";
+
+ case "AttachRotationToEmitter": return "Turn those carried particles with the player as well. " @
+ "Only read while Attach Position is on.";
+
+ case "PivotPoint": return "The point within a particle that it is positioned and rotated about, " @
+ "as a fraction of its size from the centre.";
+
+ case "OldestInFront": return "Draw the oldest particles on top of the newest, rather than the " @
+ "other way round.";
+
+ case "IntenseParticles": return "Force additive blending, which makes overlapping particles glow. " @
+ "Overrides the three blending rows below.";
+
+ case "BlendMode": return "Blend particles with what is behind them. Turning this off draws them " @
+ "opaque, edges and all.";
+
+ case "SrcBlendFactor": return "How much of the particle's own colour goes into the blend. With " @
+ "Destination Factor, these are the two halves of the OpenGL blend equation.";
+
+ case "DstBlendFactor": return "How much of what is already on screen survives the blend. " @
+ "ONE_MINUS_SRC_ALPHA is ordinary transparency; ONE is additive.";
+
+ case "AlphaTest": return "Discard any pixel less opaque than this, before blending. Below zero " @
+ "turns the test off, which is the default.";
+ }
+
+ return "";
+}
+
+//-----------------------------------------------------------------------------
+// Reading and writing.
+//-----------------------------------------------------------------------------
+
+// The mode is not a persistent field -- it is a side effect of which of setImage
+// and setAnimation ran last. Ask the engine rather than inferring it from which
+// asset is set: an emitter switched to animation before an animation has been
+// chosen is in animated mode holding nothing, and "no animation asset" would
+// read that as static. isStaticMode exists for this.
+function AssetEmitterInspectorPane::isAnimated(%this)
+{
+ return isObject(%this.target) && !%this.target.isStaticMode();
+}
+
+function AssetEmitterInspectorPane::writeField(%this, %field, %value)
+{
+ switch$(%field)
+ {
+ // The picker's own row. Writing whichever field defines the wanted mode IS
+ // the mode switch: both setters set the mode and clear the other asset.
+ // Writing the arm's current value rather than an empty one keeps a choice
+ // that was made earlier and then switched away from.
+ case "Source":
+ if(%value $= "Animation")
+ {
+ %this.target.setFieldValue("Animation", %this.target.getAnimation());
+ }
+ else
+ {
+ %this.target.setFieldValue("Image", %this.target.getImage());
+ }
+ return;
+
+ // The one emitter setter with no refreshAsset of its own, and deliberately
+ // so -- a target position is aimed at something that moves, and AngleToy
+ // writes it on every mouse move. See the comment on
+ // ParticleAssetEmitter::setTargetPosition. An editor writing it once has to
+ // ask for the refresh itself, or nothing is marked dirty and the preview
+ // does not move.
+ case "TargetPosition":
+ %this.target.setFieldValue(%field, %value);
+ %this.target.refreshAsset();
+ return;
+ }
+
+ %this.target.setFieldValue(%field, %value);
+}
+
+// The refresh chain announces the ASSET. This pane's target is one of its
+// emitters, so the base class's "is this mine?" test has to be asked one step up
+// -- otherwise every emitter edit would bounce off and the pane would show
+// whatever the engine had clamped the value to only after the next selection.
+function AssetEmitterInspectorPane::onAssetRefreshed(%this, %asset)
+{
+ if(%this.committing || !isObject(%this.target))
+ {
+ return;
+ }
+ if(%this.target.getOwner() != %asset)
+ {
+ return;
+ }
+
+ %this.refresh();
+}
+
+//-----------------------------------------------------------------------------
+// Loading. Everything the row loop does not reach.
+//-----------------------------------------------------------------------------
+
+function AssetEmitterInspectorPane::refreshExtras(%this)
+{
+ %this.sourceRow.setValue(%this.isAnimated() ? "Animation" : "Static Image");
+
+ %this.infoLabel.setText(%this.describeEmitter(%this.target));
+ %this.applyGating();
+}
+
+// Where this emitter sits in the effect and what it draws. The dropdown above
+// says which one is selected but not how many there are, and render order is the
+// reason the order matters.
+function AssetEmitterInspectorPane::describeEmitter(%this, %emitter)
+{
+ %asset = %emitter.getOwner();
+ if(!isObject(%asset))
+ {
+ return "";
+ }
+
+ %count = %asset.getEmitterCount();
+ %index = -1;
+ for(%i = 0; %i < %count; %i++)
+ {
+ if(%asset.getEmitter(%i) == %emitter)
+ {
+ %index = %i;
+ break;
+ }
+ }
+
+ %line = "Emitter" SPC (%index + 1) SPC "of" SPC %count;
+
+ // Drawn in list order, so the last one is on top of the rest.
+ if(%count > 1)
+ {
+ if(%index == %count - 1)
+ {
+ %line = %line @ ", drawn last (on top)";
+ }
+ else if(%index == 0)
+ {
+ %line = %line @ ", drawn first (behind)";
+ }
+ }
+
+ return %line @ ".";
+}
+
+//-----------------------------------------------------------------------------
+// The gating. One method, run on every refresh, because every rule here reads a
+// field the row loop has just reloaded.
+//-----------------------------------------------------------------------------
+
+function AssetEmitterInspectorPane::applyGating(%this)
+{
+ if(!isObject(%this.target))
+ {
+ return;
+ }
+
+ %this.gateSource();
+ %this.gateEmission();
+ %this.gateOrientation();
+ %this.gateBehavior();
+ %this.gateRender();
+
+ // Swapping arms changes how tall the blocks are, and a chain lays out only
+ // the children it can see -- nothing re-lays it out on setVisible.
+ %this.forceLayout();
+}
+
+// SWAP. An emitter draws one or the other, never both.
+function AssetEmitterInspectorPane::gateSource(%this)
+{
+ %animated = %this.isAnimated();
+
+ %this.row["Animation"].setVisible(%animated);
+
+ %this.row["Image"].setVisible(!%animated);
+ %this.row["RandomImageFrame"].setVisible(!%animated);
+
+ // SWAP again, one level down: an image is addressed by number or by name, and
+ // which one is in play is decided by whichever was written last. Only an image
+ // with explicit named cells can be addressed by name at all.
+ %named = !%animated && %this.target.isUsingNamedImageFrame();
+ %this.row["Frame"].setVisible(!%animated && !%named);
+ %this.row["NamedFrame"].setVisible(%named);
+
+ // GREY. A random frame is still a frame of the same image, so the row below
+ // keeps its value -- it is simply not the one used.
+ if(!%animated)
+ {
+ %this.setRowsEnabled("Frame NamedFrame", !%this.target.getRandomImageFrame(),
+ "Random Frame is on, so each particle picks its own and this one is not used.");
+ }
+}
+
+function AssetEmitterInspectorPane::gateEmission(%this)
+{
+ // GREY, the widest rule on the pane. A single particle sits at the offset and
+ // never moves, so nothing about the shape or the direction is read.
+ if(%this.target.getSingleParticle())
+ {
+ %this.setRowsEnabled("EmitterType EmitterSize EmitterAngle LinkEmissionRotation IsTargeting TargetPosition",
+ false, "Single Particle is on: one particle sits at the offset and never moves, so nothing " @
+ "about the emitter's shape or direction is read.");
+ return;
+ }
+
+ %this.setRowsEnabled("EmitterType LinkEmissionRotation IsTargeting", true, "");
+
+ // GREY. A point has no size and no angle; a torus has a size but its branch in
+ // ParticlePlayer never applies the rotation.
+ %type = %this.target.getEmitterType();
+
+ %this.setRowsEnabled("EmitterSize", %type !$= "POINT",
+ "A Point emitter emits from one spot, so it has no size. Choose another shape to use this.");
+
+ %this.setRowsEnabled("EmitterAngle", %type !$= "POINT" && %type !$= "TORUS",
+ (%type $= "POINT") ?
+ "A Point emitter emits from one spot, so turning it changes nothing." :
+ "A Torus is the same shape whichever way it is turned, and the engine does not apply this to it.");
+
+ // GREY. Aiming replaces the Emission Angle graph rather than this row, so what
+ // goes inert here is the target when aiming is off.
+ %this.setRowsEnabled("TargetPosition", %this.target.getIsTargeting(),
+ "Only read while Aim At A Point is on.");
+}
+
+// SWAP. Each orientation has exactly one set of controls; the other two would be
+// three more rows that do nothing.
+function AssetEmitterInspectorPane::gateOrientation(%this)
+{
+ %type = %this.target.getOrientationType();
+
+ %this.row["FixedAngleOffset"].setVisible(%type $= "FIXED");
+
+ %this.row["AlignedAngleOffset"].setVisible(%type $= "ALIGNED");
+ %this.row["KeepAligned"].setVisible(%type $= "ALIGNED");
+
+ %this.row["RandomAngleOffset"].setVisible(%type $= "RANDOM");
+ %this.row["RandomArc"].setVisible(%type $= "RANDOM");
+}
+
+function AssetEmitterInspectorPane::gateBehavior(%this)
+{
+ // GREY. Attaching rotation is read only from inside the position-attach test,
+ // so on its own it does nothing at all.
+ %this.setRowsEnabled("AttachRotationToEmitter", %this.target.getAttachPositionToEmitter(),
+ "Only read while Attach Position is on -- particles have to be carried with the player before " @
+ "they can be turned with it.");
+}
+
+function AssetEmitterInspectorPane::gateRender(%this)
+{
+ // GREY. Intense particles force additive blending before the blend rows are
+ // consulted at all.
+ if(%this.target.getIntenseParticles())
+ {
+ %this.setRowsEnabled("BlendMode SrcBlendFactor DstBlendFactor", false,
+ "Intense (Additive) is on, which forces additive blending and overrides these.");
+ return;
+ }
+
+ %this.setRowsEnabled("BlendMode", true, "");
+
+ // GREY. With blending off there is nothing for the two factors to weigh.
+ %this.setRowsEnabled("SrcBlendFactor DstBlendFactor", %this.target.getBlendMode(),
+ "Blending is off, so these are not used.");
+}
+
+// The commit rebuilt the preview player's emitter nodes, and it may have renamed
+// this emitter -- which the dropdown in the title bar is showing.
+function AssetEmitterInspectorPane::afterCommit(%this)
+{
+ if(isObject(AssetAdmin.inspector))
+ {
+ AssetAdmin.inspector.refreshEmitterLabels();
+ }
+
+ if(isObject(AssetAdmin.particleTransportBar))
+ {
+ AssetAdmin.particleTransportBar.refresh();
+ }
+}
diff --git a/editor/AssetAdmin/Inspector/AssetFontInspectorPane.cs b/editor/AssetAdmin/Inspector/AssetFontInspectorPane.cs
new file mode 100644
index 000000000..b791b3bc1
--- /dev/null
+++ b/editor/AssetAdmin/Inspector/AssetFontInspectorPane.cs
@@ -0,0 +1,312 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2013 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+
+//-----------------------------------------------------------------------------
+// The inspector for a font asset, in place of the generic one.
+//
+// A FontAsset is the thinnest asset in the library: it registers exactly one
+// field of its own, the .fnt file, and everything else about it is inherited
+// from AssetBase. So there is very little to arrange, and what makes this worth
+// a pane at all is the line that is NOT a field -- what the .fnt turned out to
+// contain, and whether it loaded.
+//
+// Three blocks, the shape AssetAnimationInspectorPane uses:
+//
+// Identity the name and the category
+// Font the file, what came out of it, and when it lets go
+// Description the prose the library is searched by
+//
+// The file row is in the Font block rather than in Identity, which is where the
+// image and animation panes put theirs. The rule both of those follow is that
+// the readout sits in the block holding the thing it answers, and here the
+// readout answers the file: "128 px, 97 glyphs, 2 pages of 512 x 512" is a
+// remark about the .fnt, not about the asset's name.
+//
+// A bitmap font is a .fnt descriptor plus one or more page images, and the pages
+// are named INSIDE the .fnt rather than in the asset file. That is why a missing
+// page is its own warning: nothing in the asset refers to it, so there is
+// nowhere else the loss could show up.
+//
+// AssetName is shown but not editable, for the reason the other panes give:
+// AssetBase::setAssetName does nothing once the asset manager owns the asset, so
+// a box that accepted typing would silently do nothing. A real rename is
+// AssetDatabase.renameDeclaredAsset.
+//
+// Absent, each for a checkable reason:
+// AssetInternal, AssetPrivate they exist to keep an asset OUT of the editor
+// asset id, asset file the module and the name are on show, and the
+// file is where the manager put it
+//-----------------------------------------------------------------------------
+
+$AssetFontInspectorPane::cellWidth = 300;
+$AssetFontInspectorPane::cellCount = 3;
+$AssetFontInspectorPane::descriptionHeight = 150;
+
+function AssetFontInspectorPane::onAdd(%this)
+{
+ // onAdd does not chain, so the shared setup runs from here.
+ %this.init();
+
+ // What the file row's Find button offers. A "file" row was a bitmap
+ // everywhere until this pane, and an image filter offers nothing a font
+ // asset can use.
+ %this.fileFilters = "Bitmap Font (*.fnt)|*.fnt|All Files (*.*)|*.*";
+ %this.fileTitle = "Choose a Bitmap Font File";
+}
+
+//-----------------------------------------------------------------------------
+// Construction.
+//-----------------------------------------------------------------------------
+
+function AssetFontInspectorPane::buildPane(%this)
+{
+ %grid = %this.makeCellGrid(0, $AssetFontInspectorPane::cellWidth,
+ $AssetFontInspectorPane::cellCount);
+ %this.add(%grid);
+ %this.contentGrid = %grid;
+
+ %this.buildIdentityCell(%grid);
+ %this.buildFontCell(%grid);
+ %this.buildDescriptionCell(%grid);
+
+ %this.buildWarning();
+
+ %this.nameRow.setEnabled(false, "Renaming an asset changes its id and every file that refers to it, " @
+ "so it is not something the inspector can do safely on its own.");
+}
+
+// addFieldRow takes the label and the kind as arguments rather than asking the
+// tables for them, so every call here would otherwise repeat the same lookups.
+function AssetFontInspectorPane::addField(%this, %container, %field)
+{
+ return %this.addFieldRow(%container, %field, %this.labelFor(%field),
+ %this.kindFor(%field), %this.enumItemsFor(%field));
+}
+
+function AssetFontInspectorPane::buildIdentityCell(%this, %grid)
+{
+ %chain = %this.makeCell(%grid, 4);
+ %this.identityChain = %chain;
+
+ %this.nameRow = %this.addField(%chain, "AssetName");
+ %this.addField(%chain, "AssetCategory");
+}
+
+function AssetFontInspectorPane::buildFontCell(%this, %grid)
+{
+ %chain = %this.makeCell(%grid, 4);
+ %this.fontChain = %chain;
+
+ %this.fileRow = %this.addField(%chain, "FontFile");
+
+ // Read-only, because none of it is a value the asset holds: it is what the
+ // last parse of the .fnt produced. Wrapped and extending, or a sentence in a
+ // block a third of the pane wide is simply not drawn.
+ %this.infoLabel = %this.makeInfoLabel(%chain, "labelProfile");
+ %this.infoLabel.textWrap = true;
+ %this.infoLabel.textExtend = true;
+ %this.infoLabel.vAlign = "top";
+
+ %this.addField(%chain, "AssetAutoUnload");
+}
+
+function AssetFontInspectorPane::buildDescriptionCell(%this, %grid)
+{
+ %chain = %this.makeCell(%grid, 4);
+ %this.descriptionChain = %chain;
+
+ %this.addField(%chain, "AssetDescription");
+}
+
+// Below the grid rather than in a block. A warning is a sentence, and a sentence
+// read across the whole pane is one or two lines where the same sentence in a
+// third of it is five.
+function AssetFontInspectorPane::buildWarning(%this)
+{
+ %this.warningLabel = %this.makeInfoLabel(%this, "overrideLabelProfile");
+ %this.warningLabel.textWrap = true;
+ %this.warningLabel.textExtend = true;
+ %this.warningLabel.vAlign = "top";
+ %this.warningLabel.setVisible(false);
+}
+
+//-----------------------------------------------------------------------------
+// The field tables.
+//-----------------------------------------------------------------------------
+
+function AssetFontInspectorPane::labelFor(%this, %field)
+{
+ switch$(%field)
+ {
+ case "AssetName": return "Asset Name";
+ case "AssetCategory": return "Category";
+ case "AssetDescription": return "Description";
+ case "AssetAutoUnload": return "Auto Unload";
+ case "FontFile": return "Font File";
+ }
+
+ return %field;
+}
+
+function AssetFontInspectorPane::kindFor(%this, %field)
+{
+ switch$(%field)
+ {
+ case "FontFile": return "file";
+ case "AssetAutoUnload": return "bool";
+ case "AssetDescription": return "multiline";
+ }
+
+ return "text";
+}
+
+function AssetFontInspectorPane::tipFor(%this, %field)
+{
+ switch$(%field)
+ {
+ case "FontFile": return "The .fnt descriptor written by AngelCode BMFont, in its text format. " @
+ "The page images are named inside it, and are loaded from beside it.";
+ case "AssetAutoUnload": return "Let go of the font's textures when nothing is using it any more.";
+ }
+
+ return "";
+}
+
+function AssetFontInspectorPane::editorHeightFor(%this, %field)
+{
+ if(%field $= "AssetDescription")
+ {
+ return $AssetFontInspectorPane::descriptionHeight;
+ }
+
+ return 0;
+}
+
+// The stored path is absolute -- setFontFile expands whatever it is given against
+// the asset's own folder -- and an absolute path is neither readable nor
+// portable. Show the collapsed form, which is what the file on disk says.
+function AssetFontInspectorPane::readField(%this, %field)
+{
+ if(%field $= "FontFile")
+ {
+ return %this.target.getRelativeFontFile();
+ }
+
+ return %this.target.getFieldValue(%field);
+}
+
+//-----------------------------------------------------------------------------
+// Loading. Everything the row loop does not reach.
+//-----------------------------------------------------------------------------
+
+function AssetFontInspectorPane::refreshExtras(%this)
+{
+ %this.infoLabel.setText(%this.describeFont(%this.target));
+ %this.showWarning(%this.warningFor(%this.target));
+}
+
+// What the .fnt held. Everything here is asked of the asset and none of it is
+// stored on it, which is why it is a line of text and not a set of rows.
+//
+// The page size is the size of the texture that actually loaded, not the scaleW
+// and scaleH the .fnt declares -- the same choice the image pane makes when it
+// reports the picture it got rather than the one it asked for.
+function AssetFontInspectorPane::describeFont(%this, %asset)
+{
+ %glyphs = %asset.getGlyphCount();
+
+ if(%glyphs == 0)
+ {
+ return "No font loaded.";
+ }
+
+ %line = %asset.getFontSize() SPC "px";
+
+ // Only when it says something the size did not. These are equal in every font
+ // shipped with the engine, and a line that repeats itself reads as a mistake.
+ if(%asset.getLineHeight() != %asset.getFontSize())
+ {
+ %line = %line @ "," SPC %asset.getLineHeight() SPC "line height";
+ }
+
+ %line = %line @ "," SPC %glyphs SPC ((%glyphs == 1) ? "glyph" : "glyphs");
+
+ %pages = %asset.getPageCount();
+ %line = %line @ "," SPC %pages SPC ((%pages == 1) ? "page" : "pages");
+
+ // Nothing to say about the size of a page that did not load; the warning
+ // below covers that case instead.
+ %pageWidth = %asset.getPageWidth(0);
+ if(%pageWidth > 0)
+ {
+ %line = %line SPC "of" SPC %pageWidth SPC "x" SPC %asset.getPageHeight(0);
+ }
+
+ return %line @ ".";
+}
+
+// In the order they matter. Only the first is shown, because the first is the
+// one that has to be fixed before any of the others can be judged.
+//
+// Both of these are currently a line in the console log and nothing on screen.
+function AssetFontInspectorPane::warningFor(%this, %asset)
+{
+ if(%asset.getGlyphCount() == 0)
+ {
+ return "This font did not load. Check that the file is where the path says, and that it is the " @
+ "TEXT format AngelCode BMFont writes -- the binary and XML variants are not read.";
+ }
+
+ %declared = %asset.getPageCount();
+ %loaded = %asset.getLoadedPageCount();
+
+ if(%loaded < %declared)
+ {
+ return (%declared - %loaded) SPC "of this font's" SPC %declared SPC "page images did not load, so " @
+ "some characters will be missing. The pages are named inside the .fnt file rather than here, " @
+ "and are loaded from the folder beside it.";
+ }
+
+ return "";
+}
+
+// forceLayout only when the visibility actually changed: a chain skips hidden
+// children when it lays out, and nothing re-lays it out on setVisible.
+function AssetFontInspectorPane::showWarning(%this, %text)
+{
+ %wanted = (%text !$= "");
+ %changed = (%wanted != %this.warningLabel.isVisible());
+
+ %this.warningLabel.setText(%text);
+ %this.warningLabel.setVisible(%wanted);
+
+ if(%changed)
+ {
+ %this.forceLayout();
+ }
+}
+
+// No afterCommit. The preview is a TextSprite wearing this font and it does have
+// to be rebuilt when the file changes -- but that already happens: the commit
+// ends in refreshAsset, and AssetAdmin::refreshPreview re-clicks the selected
+// tile, which is what built the TextSprite in the first place. Doing it here as
+// well would build it twice.
diff --git a/editor/AssetAdmin/Inspector/AssetImageCellGrid.cs b/editor/AssetAdmin/Inspector/AssetImageCellGrid.cs
new file mode 100644
index 000000000..37a08cb5f
--- /dev/null
+++ b/editor/AssetAdmin/Inspector/AssetImageCellGrid.cs
@@ -0,0 +1,292 @@
+
+//-----------------------------------------------------------------------------
+// The eight values that cut an image into frames, as one small table:
+//
+// X Y
+// Count [ 4 ] [ 2 ]
+// Size [128 ] [128 ]
+// Offset [ 0 ] [ 0 ]
+// Stride [ 0 ] [ 0 ]
+//
+// [x] Row order
+//
+// Eight captioned field rows would say the same thing, and the stock inspector
+// tries: it reflects them into an "X Values" group and a "Y Values" group, so a
+// cell's width is four fields away from its height and the pairs that have to be
+// read together never appear together. They are two columns of one table, and
+// this draws them as one -- which also keeps them one cell of the pane's grid,
+// so no reflow can ever split a pair across a column break.
+//
+// The table never writes the asset. It hands each edit to its owner --
+// owner.onCellGridCommit(%field, %value) -- so the pane stays the only thing
+// that touches the asset, and the only thing that has to know that every write
+// saves the file. The same reason GuiProfileEditorBorderGrid reports to its
+// host rather than editing through it.
+//
+// The creator sets owner inline, then calls build() once after adding the table
+// to its container -- the container decides how wide the cell is, so build() has
+// to run after the add. It records the laid-out height in .gridHeight.
+//-----------------------------------------------------------------------------
+
+function AssetImageCellGrid::onAdd(%this)
+{
+ ThemeManager.setProfile(%this, "emptyProfile");
+}
+
+// Row title TAB X field TAB Y field TAB X tip TAB Y tip.
+function AssetImageCellGrid::rowTable(%this)
+{
+ return
+ "Count" TAB "CellCountX" TAB "CellCountY" TAB
+ "How many cells across the image." TAB
+ "How many cells down the image." NL
+ "Size" TAB "CellWidth" TAB "CellHeight" TAB
+ "The width of one cell, in pixels." TAB
+ "The height of one cell, in pixels." NL
+ "Offset" TAB "CellOffsetX" TAB "CellOffsetY" TAB
+ "The gap between the left edge of the image and the first cell." TAB
+ "The gap between the top edge of the image and the first cell." NL
+ "Stride" TAB "CellStrideX" TAB "CellStrideY" TAB
+ "From one cell's left edge to the next one's. Leave at 0 to use the cell width." TAB
+ "From one cell's top edge to the next one's. Leave at 0 to use the cell height.";
+}
+
+// Every field the table edits, in one list, for the pane's greying.
+function AssetImageCellGrid::fields(%this)
+{
+ return "CellCountX CellCountY CellWidth CellHeight CellOffsetX CellOffsetY CellStrideX CellStrideY CellRowOrder";
+}
+
+function AssetImageCellGrid::build(%this)
+{
+ // The width available, which is not the width taken: the table is eight boxes
+ // of two or three digits, and past a certain point a wider block only buys
+ // whitespace between them. So it measures what it is given, sizes its boxes
+ // to fit, and then shrinks to what it actually used.
+ //
+ // Which is also why it carries no sizing flag. A block that grows leaves the
+ // table where it is rather than stretching it into a row of very wide number
+ // boxes, and a block can never get narrower than the table needs -- the
+ // blocks have a 300-pixel floor and the table wants 216.
+ %avail = getWord(%this.getExtent(), 0);
+ %x0 = 4;
+ %labelW = 52;
+ %gap = 6;
+ %rowH = 26;
+ %boxH = 22;
+ %headerH = 16;
+
+ // The boxes hold two or three digits, so they are sized for that -- a number
+ // box the width of half a pane reads as a text field with a number lost in
+ // it. Narrow enough and they give ground instead of clipping.
+ %room = %avail - %x0 - %labelW - %gap - 4;
+ %boxW = mGetMin(72, mGetMax(34, (%room - %gap) / 2));
+ %w = %x0 + %labelW + %gap + %boxW + %gap + %boxW + 4;
+
+ %colX[0] = %x0 + %labelW + %gap;
+ %colX[1] = %colX[0] + %boxW + %gap;
+
+ // The two column captions. Nothing else says which box is which axis.
+ %axis = "X" TAB "Y";
+ for(%c = 0; %c < 2; %c++)
+ {
+ %cap = new GuiControl()
+ {
+ Position = %colX[%c] SPC 2;
+ Extent = %boxW SPC %headerH;
+ Text = getField(%axis, %c);
+ align = "center";
+ vAlign = "middle";
+ UseInput = false;
+ };
+ ThemeManager.setProfile(%cap, "labelProfile");
+ %this.add(%cap);
+ }
+
+ %rows = %this.rowTable();
+ %count = getRecordCount(%rows);
+ for(%r = 0; %r < %count; %r++)
+ {
+ %rec = getRecord(%rows, %r);
+ %y = %headerH + 4 + (%r * %rowH);
+
+ %label = new GuiControl()
+ {
+ Position = %x0 SPC %y;
+ Extent = %labelW SPC %boxH;
+ Text = getField(%rec, 0);
+ align = "left";
+ vAlign = "middle";
+ UseInput = false;
+ };
+ ThemeManager.setProfile(%label, "labelProfile");
+ %this.add(%label);
+
+ for(%c = 0; %c < 2; %c++)
+ {
+ %this.makeBox(%colX[%c], %y, %boxW, %boxH,
+ getField(%rec, 1 + %c), getField(%rec, 3 + %c));
+ }
+ }
+
+ // Which way the frame numbers run. It only means anything once the image is
+ // cut both ways, but it is the last thing about the cut, so it belongs here
+ // rather than in a section of its own.
+ %uy = %headerH + 8 + (%count * %rowH);
+ %cbW = %w - %x0 - 4;
+ %this.rowOrderBox = new GuiCheckBoxCtrl()
+ {
+ Position = %x0 SPC %uy;
+ Extent = %cbW SPC 26;
+ Text = "Row order";
+ boxOffset = "0 4";
+ boxExtent = "18 18";
+ textOffset = "26 4";
+ textExtent = (%cbW - 26) SPC 18;
+ Tooltip = "Number the frames left to right, then top to bottom. Turn it off to number them down each column instead.";
+ Command = %this.getID() @ ".commitRowOrder();";
+ };
+ ThemeManager.setProfile(%this.rowOrderBox, "checkboxProfile");
+ ThemeManager.setProfile(%this.rowOrderBox, "tipProfile", "TooltipProfile");
+ %this.add(%this.rowOrderBox);
+
+ %this.gridHeight = %uy + 30;
+ %this.setExtent(%w, %this.gridHeight);
+}
+
+// One numeric box. The class is what gives it the arrow keys; the tip is kept on
+// the box as well as set on it, because greying the table replaces every tooltip
+// with the reason and has to be able to put them back.
+function AssetImageCellGrid::makeBox(%this, %x, %y, %w, %h, %field, %tip)
+{
+ %box = new GuiTextEditCtrl()
+ {
+ class = "AssetImageCellInput";
+ Position = %x SPC %y;
+ Extent = %w SPC %h;
+ inputMode = "Number";
+ align = "center";
+ Tooltip = %tip;
+ tipText = %tip;
+ cellField = %field;
+ grid = %this;
+ };
+ ThemeManager.setProfile(%box, "textEditProfile");
+ ThemeManager.setProfile(%box, "tipProfile", "TooltipProfile");
+ %box.AltCommand = %this.getID() @ ".commitBox(" @ %box.getID() @ ");";
+ %box.ReturnCommand = %this.getID() @ ".commitBox(" @ %box.getID() @ ");";
+ %this.add(%box);
+ %this.box[%field] = %box;
+ return %box;
+}
+
+//-----------------------------------------------------------------------------
+// Values.
+//-----------------------------------------------------------------------------
+
+// Load the asset's nine values. The populating guard keeps setText and
+// setStateOn from echoing straight back through the commits.
+function AssetImageCellGrid::load(%this, %asset)
+{
+ if(!isObject(%asset))
+ {
+ return;
+ }
+
+ %this.populating = true;
+
+ %rows = %this.rowTable();
+ %count = getRecordCount(%rows);
+ for(%r = 0; %r < %count; %r++)
+ {
+ %rec = getRecord(%rows, %r);
+ for(%c = 0; %c < 2; %c++)
+ {
+ %field = getField(%rec, 1 + %c);
+ %this.box[%field].setText(%asset.getFieldValue(%field));
+ }
+ }
+
+ %this.rowOrderBox.setStateOn(%asset.getCellRowOrder());
+
+ %this.populating = false;
+}
+
+// Inert but still readable, which is what the table becomes in explicit frame
+// mode: the values are still the ones the asset holds, they are simply not the
+// ones it cuts by. Blanking them would lose what a user is about to go back to.
+function AssetImageCellGrid::setEnabled(%this, %enabled, %reason)
+{
+ %rows = %this.rowTable();
+ %count = getRecordCount(%rows);
+ for(%r = 0; %r < %count; %r++)
+ {
+ %rec = getRecord(%rows, %r);
+ for(%c = 0; %c < 2; %c++)
+ {
+ %box = %this.box[getField(%rec, 1 + %c)];
+ %box.setActive(%enabled);
+ %box.Tooltip = %enabled ? %box.tipText : %reason;
+ }
+ }
+
+ %this.rowOrderBox.setActive(%enabled);
+ %this.enabled = %enabled;
+}
+
+//-----------------------------------------------------------------------------
+// Commit. Nothing here writes the asset -- see the header.
+//-----------------------------------------------------------------------------
+
+function AssetImageCellGrid::commitBox(%this, %box)
+{
+ // mFloor, not the text: a box left holding "12.0" would write "12.0" into a
+ // field the engine reads as a whole number of pixels.
+ %this.notify(%box.cellField, mFloor(%box.getText()));
+}
+
+function AssetImageCellGrid::commitRowOrder(%this)
+{
+ %this.notify("CellRowOrder", %this.rowOrderBox.getStateOn());
+}
+
+function AssetImageCellGrid::notify(%this, %field, %value)
+{
+ if(%this.populating || !isObject(%this.owner))
+ {
+ return;
+ }
+
+ %this.owner.onCellGridCommit(%field, %value);
+}
+
+//-----------------------------------------------------------------------------
+// The numeric boxes: up and down nudge by one. A second class in this file, for
+// the reason EditorFieldRow keeps its two -- it exists only to route one engine
+// callback back to the widget that owns the box, and a file of its own would say
+// nothing this one does not.
+//
+// Clicking places the caret, as it does in every other box in the editor; see
+// the note in EditorFieldRow for why nothing re-selects here.
+//-----------------------------------------------------------------------------
+
+function AssetImageCellInput::onUpArrow(%this)
+{
+ %this.nudge(1);
+}
+
+function AssetImageCellInput::onDownArrow(%this)
+{
+ %this.nudge(-1);
+}
+
+function AssetImageCellInput::nudge(%this, %delta)
+{
+ // None of these nine can be negative, and a cell count of zero is what the
+ // asset already means by "one row" -- so the floor is where the field's own
+ // validation is, not somewhere this decides.
+ %value = %this.getText() + %delta;
+ %this.setText(mGetMax(0, %value));
+ %this.selectAllText();
+ %this.grid.commitBox(%this);
+}
diff --git a/editor/AssetAdmin/Inspector/AssetImageInspectorPane.cs b/editor/AssetAdmin/Inspector/AssetImageInspectorPane.cs
new file mode 100644
index 000000000..0105d31f2
--- /dev/null
+++ b/editor/AssetAdmin/Inspector/AssetImageInspectorPane.cs
@@ -0,0 +1,332 @@
+
+//-----------------------------------------------------------------------------
+// The Asset Manager's Inspector tab for an image asset, in place of the generic
+// C++ GuiInspector. Everything about laying fields out and moving values between
+// them and the asset is in AssetInspectorPane; this is only what an ImageAsset
+// holds and how it should read.
+//
+// There are no collapsible sections. The Gui Editor has them because a control
+// carries dozens of fields; an image asset carries eleven, and eleven fit. So
+// everything is in the open, as four blocks of roughly equal size in one grid:
+//
+// Identity name, category, the file
+// Frames the eight cell values as one table, the row order, and a line
+// saying what actually loaded and how it cut up
+// Settings filter, color depth, and when it unloads
+// Description the prose the library is searched by
+//
+// Four blocks and not four columns: the grid decides how many columns it can
+// afford and the blocks flow into them, so the same pane is 1x4 in a tall narrow
+// frame, 2x2 at the size the inspector opens at, and 4x1 across the foot of a
+// wide screen. That last one is the case that made this a grid -- laid out as a
+// stack, a 1600-pixel inspector was a column of fields down the left and a
+// description box a yard wide, with most of the panel empty.
+//
+// AssetName is shown but not editable. AssetBase::setAssetName does nothing once
+// the asset manager owns the asset (assetBase.h), so the stock inspector has
+// always offered a box that silently did not work. A real rename is
+// AssetDatabase.renameDeclaredAsset, which changes the asset id and rewrites
+// every file referring to it -- its own piece of work, not a side effect of
+// typing in a box.
+//
+// Five of the asset's values are deliberately absent. AssetInternal and
+// AssetPrivate exist to keep an asset OUT of the editor, so an editor is the one
+// place they are no use. The asset id is the module and the name, both of which
+// are on show; the asset file is where the manager put it, and wanting a
+// different one means wanting a different asset.
+//
+// BlendColor is the fifth, and it is absent because it is not really a property
+// of the image: it tints the BASE LAYER that an asset's layers are composed
+// onto, does nothing at all when there are no layers, and is edited in the row
+// it belongs to on the Image Layers tab -- where the layer it tints is on screen
+// beside it. Offered here it was a color picker that did nothing on nearly every
+// asset in the library.
+//
+// ExplicitMode belongs to another tab and is only read here, to grey the cell
+// table: the frames then come from the Explicit Frames tab. It reaches the pane
+// the way any other change does -- the setter calls refreshAsset, which fires
+// AssetBase::onRefresh, which tells the inspector.
+//-----------------------------------------------------------------------------
+
+// The narrowest a block may get, and how many of them there are.
+//
+// 300 is chosen against the three widths that matter. The identity block has to
+// hold a path and a Find button, which sets the floor; and with 4-pixel spacing
+// the grid takes 1 column below 608, 2 up to 912, and 4 once it has 1216 -- so a
+// tall narrow frame stacks, the size the inspector opens at is 2x2, and the foot
+// of a wide screen is a single row of four.
+$AssetImageInspectorPane::cellWidth = 300;
+$AssetImageInspectorPane::cellCount = 4;
+
+// Deep enough that an empty description box reads as "the prose goes here" and
+// stands about as tall as the blocks beside it.
+$AssetImageInspectorPane::descriptionHeight = 150;
+
+function AssetImageInspectorPane::onAdd(%this)
+{
+ // onAdd does not chain, so the shared setup runs from here.
+ %this.init();
+}
+
+//-----------------------------------------------------------------------------
+// Construction. Four blocks in one grid, then the warning beneath it.
+//-----------------------------------------------------------------------------
+
+function AssetImageInspectorPane::buildPane(%this)
+{
+ %grid = %this.makeCellGrid(0, $AssetImageInspectorPane::cellWidth,
+ $AssetImageInspectorPane::cellCount);
+ %this.add(%grid);
+ %this.contentGrid = %grid;
+
+ %this.buildIdentityCell(%grid);
+ %this.buildFramesCell(%grid);
+ %this.buildSettingsCell(%grid);
+ %this.buildDescriptionCell(%grid);
+
+ %this.buildWarning();
+
+ // AssetName is the only row that is there to be read rather than changed, and
+ // nothing about a selection can make it editable, so it is said once.
+ %this.nameRow.setEnabled(false,
+ "Renaming an asset changes its id and every file that refers to it, so it is not done from here yet.");
+}
+
+// What the asset is: its name, the category the library groups it under, and the
+// picture itself.
+function AssetImageInspectorPane::buildIdentityCell(%this, %grid)
+{
+ %cell = %this.makeCell(%grid);
+ %this.identityChain = %cell;
+
+ %this.nameRow = %this.addFieldRow(%cell, "AssetName", "Asset Name", "text", "");
+ %this.categoryRow = %this.addFieldRow(%cell, "AssetCategory", "Category", "text", "");
+ %this.fileRow = %this.addFieldRow(%cell, "ImageFile", "Image File", "file", "");
+}
+
+// How it is cut into frames, and what that produced. The readout is in this
+// block rather than across the pane because it is an answer to the table above
+// it -- "512 x 512 pixels, 64 frames" is only interesting beside the numbers
+// that decided it.
+function AssetImageInspectorPane::buildFramesCell(%this, %grid)
+{
+ %cell = %this.makeCell(%grid);
+ %this.framesChain = %cell;
+
+ // One widget rather than eight rows -- see AssetImageCellGrid for why.
+ %this.cellGrid = new GuiControl()
+ {
+ class = "AssetImageCellGrid";
+ Position = "0 0";
+ Extent = getWord(%cell.getExtent(), 0) SPC 160;
+ owner = %this;
+ };
+ %cell.add(%this.cellGrid);
+ %this.cellGrid.build();
+
+ // Read-only, because none of it is a value the asset holds: it is the size of
+ // the picture that loaded and the number of frames the cell values actually
+ // produced. Wrapped, because a block is a quarter of the pane at its widest.
+ %this.infoLabel = %this.makeInfoLabel(%cell, "labelProfile");
+ %this.infoLabel.textWrap = true;
+ %this.infoLabel.textExtend = true;
+ %this.infoLabel.vAlign = "top";
+}
+
+// How it is drawn, and when it lets go of its texture.
+function AssetImageInspectorPane::buildSettingsCell(%this, %grid)
+{
+ %cell = %this.makeCell(%grid);
+ %this.settingsChain = %cell;
+
+ %this.addFieldRow(%cell, "FilterMode", %this.labelFor("FilterMode"),
+ "enum", %this.enumItemsFor("FilterMode"));
+ %this.addFieldRow(%cell, "Force16bit", %this.labelFor("Force16bit"), "bool", "");
+ %this.addFieldRow(%cell, "AssetAutoUnload", %this.labelFor("AssetAutoUnload"), "bool", "");
+}
+
+// What it is for. One field, and it fills the block: the library searches by
+// this, so it is worth writing more than a few words in.
+function AssetImageInspectorPane::buildDescriptionCell(%this, %grid)
+{
+ %cell = %this.makeCell(%grid);
+ %this.descriptionChain = %cell;
+
+ %this.descriptionRow = %this.addFieldRow(%cell, "AssetDescription",
+ %this.labelFor("AssetDescription"), "multiline", "");
+}
+
+// Below the grid rather than in a block, and the only thing that is. A warning
+// is a sentence, and a sentence read across the whole pane is one or two lines
+// where the same sentence in a quarter of it is six.
+//
+// Hidden while there is nothing to say, so it costs no height -- a chain skips
+// its hidden children. textWrap with textExtend so it takes the lines it needs;
+// rendering is where that is measured, being the only place a font can be asked
+// how wide a word is.
+function AssetImageInspectorPane::buildWarning(%this)
+{
+ %this.warningLabel = %this.makeInfoLabel(%this, "overrideLabelProfile");
+ %this.warningLabel.textWrap = true;
+ %this.warningLabel.textExtend = true;
+ %this.warningLabel.vAlign = "top";
+ %this.warningLabel.setVisible(false);
+}
+
+//-----------------------------------------------------------------------------
+// Field presentation.
+//-----------------------------------------------------------------------------
+
+function AssetImageInspectorPane::labelFor(%this, %field)
+{
+ switch$(%field)
+ {
+ case "AssetName": return "Asset Name";
+ case "AssetCategory": return "Category";
+ case "AssetDescription": return "Description";
+ case "AssetAutoUnload": return "Auto Unload";
+ case "ImageFile": return "Image File";
+ case "FilterMode": return "Filter";
+ case "Force16bit": return "16 Bit Color";
+ }
+ return %field;
+}
+
+function AssetImageInspectorPane::kindFor(%this, %field)
+{
+ switch$(%field)
+ {
+ case "AssetAutoUnload" or "Force16bit": return "bool";
+ case "AssetDescription": return "multiline";
+ case "ImageFile": return "file";
+ case "FilterMode": return "enum";
+ }
+ return "text";
+}
+
+function AssetImageInspectorPane::enumItemsFor(%this, %field)
+{
+ // The engine's own labels are NEAREST, BILINEAR and DEFAULT (textureFilterLookup
+ // in ImageAsset.cc). Both the lookup on the way in and the drop-down's own
+ // search ignore case, so these can read as words.
+ if(%field $= "FilterMode")
+ {
+ return "Default" TAB "Nearest" TAB "Bilinear";
+ }
+ return "";
+}
+
+// The description has a block to itself, so it is not three lines deep.
+function AssetImageInspectorPane::editorHeightFor(%this, %field)
+{
+ return (%field $= "AssetDescription")
+ ? $AssetImageInspectorPane::descriptionHeight : 0;
+}
+
+// The stored path is absolute -- setImageFile expands whatever it is given
+// against the asset's own folder -- and an absolute path is neither readable nor
+// portable. Show the collapsed form, which is what the file on disk says.
+function AssetImageInspectorPane::readField(%this, %field)
+{
+ if(%field $= "ImageFile")
+ {
+ return %this.target.getRelativeImageFile();
+ }
+ return %this.target.getFieldValue(%field);
+}
+
+//-----------------------------------------------------------------------------
+// Loading. Everything the row loop does not reach.
+//-----------------------------------------------------------------------------
+
+function AssetImageInspectorPane::refreshExtras(%this)
+{
+ %asset = %this.target;
+
+ %this.cellGrid.load(%asset);
+ %this.cellGrid.setEnabled(!%asset.getExplicitMode(),
+ "Explicit frame mode is on, so the frames come from the Explicit Frames tab and these values are not used.");
+
+ %this.infoLabel.setText(%this.describeImage(%asset));
+ %this.showWarning(%this.warningFor(%asset));
+}
+
+// What loaded and what it cut into. Everything here is asked of the asset, never
+// stored on it, which is why it is a line of text and not a row.
+function AssetImageInspectorPane::describeImage(%this, %asset)
+{
+ %w = %asset.getImageWidth();
+ %h = %asset.getImageHeight();
+
+ if(%w <= 0 || %h <= 0)
+ {
+ return "No image loaded.";
+ }
+
+ // No "pixels" after the size. It is the one word here that says nothing a
+ // reader of an image asset did not already know, and without it the whole
+ // line fits on one line in a block a quarter of the pane wide.
+ %frames = %asset.getFrameCount();
+ %text = %w @ " x " @ %h @ ", " @ %frames SPC ((%frames == 1) ? "frame" : "frames");
+
+ // Worth saying either way. A texture whose sides are not powers of two is
+ // legal here but is the first thing to look at when one will not load on a
+ // phone or in a browser.
+ return %text @ ", " @ (%asset.getIsImagePOT() ? "power of two" : "not a power of two");
+}
+
+// The three ways an image asset ends up looking wrong, in the order they matter.
+// Each of them is currently a line in the console log and nothing on screen.
+function AssetImageInspectorPane::warningFor(%this, %asset)
+{
+ if(%asset.getImageWidth() <= 0 || %asset.getImageHeight() <= 0)
+ {
+ // The size cap is the likeliest cause and the least discoverable one:
+ // TextureManager refuses a bitmap over 2048 on either side and says so
+ // only in the log, leaving a sprite that draws nothing at all.
+ return "This image did not load. Check that the file is where the path says, and that neither side is over 2048 pixels -- the engine refuses anything larger.";
+ }
+
+ if(%asset.getExplicitMode())
+ {
+ return "Explicit frame mode is on. The frames are the ones listed on the Explicit Frames tab, and the cell values above are not being used.";
+ }
+
+ // The cut did not survive calculateImage, which warns to the console and
+ // falls back to treating the whole image as one frame.
+ %cx = %asset.getCellCountX();
+ %cy = %asset.getCellCountY();
+ if(%cx > 0 && %cy > 0 && %asset.getFrameCount() != (%cx * %cy))
+ {
+ return "These cell values do not fit the image, so it is being used as a single frame. Check the sizes and offsets against the image size above.";
+ }
+
+ return "";
+}
+
+// A chain skips hidden children when it lays out, but nothing re-lays it out on
+// setVisible -- so the pane has to ask, and only when the answer changed.
+function AssetImageInspectorPane::showWarning(%this, %text)
+{
+ %show = (%text !$= "");
+ %this.warningLabel.setText(%text);
+
+ if(%this.warningLabel.isVisible() == %show)
+ {
+ return;
+ }
+
+ %this.warningLabel.setVisible(%show);
+ %this.forceLayout();
+}
+
+//-----------------------------------------------------------------------------
+// Commits.
+//-----------------------------------------------------------------------------
+
+// One of the cell table's nine values. It goes through commitValue like a row's
+// would, so the pane stays the only thing that writes to the asset.
+function AssetImageInspectorPane::onCellGridCommit(%this, %field, %value)
+{
+ %this.commitValue(%field, %value);
+}
diff --git a/editor/AssetAdmin/Inspector/AssetInspectorPane.cs b/editor/AssetAdmin/Inspector/AssetInspectorPane.cs
new file mode 100644
index 000000000..c35d68beb
--- /dev/null
+++ b/editor/AssetAdmin/Inspector/AssetInspectorPane.cs
@@ -0,0 +1,493 @@
+
+//-----------------------------------------------------------------------------
+// The shared half of an Asset Manager inspector pane: everything about laying
+// fields out and moving values between them and an asset, and nothing about
+// which fields an asset has.
+//
+// It exists because the stock C++ GuiInspector reflects every registered field
+// into flat alphabetical groups, which for an ImageAsset means the eight cell
+// values arrive split into "X Values" and "Y Values" -- so a cell's width never
+// sits beside its height -- with nothing pinned open and nothing said about the
+// image itself. Image, animation, font and audio assets have panes; particle and
+// spine assets still use the inspector, which is why the pane's knowledge of a
+// particular asset lives in the subclass rather than here.
+//
+// Layout is the arrangement GuiEditorInspectorPane and GuiProfileEditorProfileForm
+// both use, and for the same reason: a vertical chain of blocks, each laying its
+// fields out in a GuiGridCtrl, so widening the inspector frame reflows the cells
+// into more columns instead of leaving dead space. The Asset Manager needs that
+// more than either of them -- its inspector is the BOTTOM frame of the frame set
+// (AssetAdmin::createFrameSet), so it opens about 700 wide and 360 tall and is
+// dragged to whatever shape suits the work.
+//
+// The pane owns every write to the asset; its rows only marshal values.
+//
+// Subclassing. onAdd does not chain in TorqueScript (TORQUE_SCRIPT.md rule 8),
+// so a subclass's onAdd calls init() first and the base drives from there:
+//
+// init() call from the subclass's onAdd
+// build() call once after adding the pane to its scroller; it calls
+// buildPane() which the subclass defines -- its whole layout
+// labelFor() \ the subclass's field tables. The defaults are the field
+// kindFor() } name, a text box and no tooltip, which is what an
+// enumItemsFor() } unlisted field degrades to rather than vanishing.
+// tipFor() /
+// refreshExtras() anything the row loop does not reach
+// afterCommit() anything the rest of the editor has to be told
+//
+// A subclass may also override readField/writeField, which is how a field whose
+// stored form is not its editable form (an asset's loose file path) is handled
+// without the row or the loop knowing.
+//-----------------------------------------------------------------------------
+
+function AssetInspectorPane::init(%this)
+{
+ ThemeManager.setProfile(%this, "emptyProfile");
+
+ // The narrowest a cell column may get. The grids run in Variable mode, so
+ // they fit as many columns as the pane can hold at this width and share the
+ // remainder evenly -- which is what makes dragging the frame wider add
+ // columns rather than whitespace.
+ %this.rowWidth = 220;
+
+ // Half of that, for the sections whose fields are read in pairs.
+ %this.pairWidth = 152;
+
+ %this.rowFields = "";
+ %this.panelList = "";
+ %this.target = "";
+ %this.assetId = "";
+}
+
+//-----------------------------------------------------------------------------
+// Construction.
+//-----------------------------------------------------------------------------
+
+function AssetInspectorPane::build(%this)
+{
+ %this.buildPane();
+ %this.forceLayout();
+ %this.setVisible(false);
+}
+
+// A GuiPanelCtrl learns its collapsed height only from parentResized -- its
+// constructor defaults to 64x64 whatever Extent it was given, and a chain
+// positions its children without ever resizing them. Nudging the width by a
+// pixel and back forces exactly one parentResized through every child and
+// leaves the widths where they started.
+//
+// Measured from the pane's CURRENT width rather than the width it was built at:
+// the pane is HorizSizing "width" inside a filling scroller, so by the time
+// anything calls this it is as wide as the frame, and nudging from the build
+// width would snap it narrow until the next parent resize pushed it back.
+function AssetInspectorPane::forceLayout(%this)
+{
+ %w = getWord(%this.getExtent(), 0);
+ %h = getWord(%this.getExtent(), 1);
+ %this.resize(0, 0, %w + 1, %h);
+ %this.resize(0, 0, %w, %h);
+}
+
+// The grid configuration every block here uses. A hidden cell is skipped rather
+// than left as a hole, so filtering closes the gap (GuiGridCtrl::resize).
+//
+// %cellW is the NARROWEST a column may be, not its width: the grid fits as many
+// columns as the pane can hold at that size and shares the remainder evenly.
+// Omit it for the ordinary one-field-per-row width.
+//
+// %maxCols caps the count, and a grid whose cells are blocks rather than single
+// fields wants it. GuiGridCtrl works out its chain length from the width alone
+// and never asks how many children it has (GetGridItemWidth), so a four-block
+// grid on a wide screen computes six columns, fills four and leaves two empty --
+// and the blocks come out narrower than the width they were given. Capping at
+// the number of blocks makes the last step 4-across rather than 4-of-6.
+function AssetInspectorPane::makeCellGrid(%this, %y, %cellW, %maxCols)
+{
+ %grid = new GuiGridCtrl()
+ {
+ HorizSizing = "width";
+ Position = "0" SPC %y;
+ Extent = %this.paneWidth SPC 4;
+ CellModeX = "variable";
+ CellModeY = "variable";
+ CellSizeX = (%cellW $= "") ? %this.rowWidth : %cellW;
+ CellSizeY = 48;
+ CellSpacingX = 4;
+ CellSpacingY = 4;
+ MaxColCount = (%maxCols $= "") ? 0 : %maxCols;
+ MaxRowCount = 0;
+ OrderMode = "lrtb";
+ IsExtentDynamic = true;
+ };
+ ThemeManager.setProfile(%grid, "emptyProfile");
+ return %grid;
+}
+
+// One cell of such a grid: a vertical chain that measures itself from what is
+// put in it, so the grid's row grows to the tallest block rather than to a
+// number written here. Added to the grid before anything goes in it, because the
+// grid sizes a cell as it arrives and everything inside lays out to that width.
+function AssetInspectorPane::makeCell(%this, %grid, %spacing)
+{
+ %cell = %this.makeChain(0, (%spacing $= "") ? 2 : %spacing);
+ %grid.add(%cell);
+ return %cell;
+}
+
+// A plain vertical chain, for the places that stack full-width blocks rather
+// than flowing cells.
+function AssetInspectorPane::makeChain(%this, %y, %spacing)
+{
+ %chain = new GuiChainCtrl()
+ {
+ HorizSizing = "width";
+ Position = "0" SPC %y;
+ Extent = %this.paneWidth SPC 4;
+ IsVertical = true;
+ ChildSpacing = %spacing;
+ };
+ ThemeManager.setProfile(%chain, "emptyProfile");
+ return %chain;
+}
+
+// A collapsible section. Its cells sit in an inner grid rather than directly on
+// the panel: GuiExpandCtrl::toggleHiddenChildren force-writes mVisible on every
+// direct child whenever it expands, collapses or resizes, which would undo any
+// filtering. Grandchildren are left alone and the grid skips the hidden ones.
+function AssetInspectorPane::makeSectionPanel(%this, %title)
+{
+ %headerH = 24;
+
+ %panel = new GuiPanelCtrl()
+ {
+ HorizSizing = "width";
+ Text = %title;
+ Position = "0 0";
+ Extent = %this.paneWidth SPC %headerH;
+ MinExtent = "80" SPC %headerH;
+ };
+ ThemeManager.setProfile(%panel, "panelProfile");
+ return %panel;
+}
+
+// A read-only line of text. Used for the things an asset can only be asked,
+// never told -- its size, how many frames it cuts into, why it did not load.
+//
+// Sized to the container it is going into rather than to the pane, and added
+// here rather than by the caller, because a GuiChainCtrl positions its children
+// without resizing them: a label authored at the pane's width and dropped into a
+// block a quarter that wide does not wrap, it hangs off the end and is clipped.
+// The sizing flag takes it from there.
+function AssetInspectorPane::makeInfoLabel(%this, %container, %profile)
+{
+ %label = new GuiControl()
+ {
+ HorizSizing = "width";
+ Position = "0 0";
+ Extent = getWord(%container.getExtent(), 0) SPC 20;
+ MinExtent = "0 0";
+ Text = "";
+ align = "left";
+ vAlign = "middle";
+ UseInput = false;
+ };
+ ThemeManager.setProfile(%label, %profile);
+ %container.add(%label);
+ return %label;
+}
+
+//-----------------------------------------------------------------------------
+// Rows.
+//-----------------------------------------------------------------------------
+
+// Build a row without claiming a name for it, for the rare widget that reads
+// like a field but is not one -- it must stay out of the registry the refresh
+// loop walks.
+function AssetInspectorPane::makeFieldRow(%this, %container, %field, %label, %kind, %enumItems)
+{
+ %row = new GuiControl()
+ {
+ class = "EditorFieldRow";
+
+ // A grid resizes every cell it lays out, which makes the flag moot there;
+ // a chain does not, so a row in one follows the pane's width from here.
+ HorizSizing = "width";
+ Position = "0 0";
+ Extent = getWord(%container.getExtent(), 0) SPC 48;
+ fieldName = %field;
+ labelText = %label;
+ kind = %kind;
+ enumItems = %enumItems;
+ editorHeight = %this.editorHeightFor(%field);
+ assetType = %this.assetTypeFor(%field);
+ owner = %this;
+ };
+ %container.add(%row);
+ %row.build();
+
+ // The row's reset button means "back to the theme's stamped value", which has
+ // no analogue for an asset: there is no layer under it to fall back to.
+ %row.resetButton.setVisible(false);
+
+ // After build(), because the widgets the tooltip goes on do not exist until
+ // then. Empty for most fields, which is the same as not having one.
+ %row.setTooltip(%this.tipFor(%field));
+
+ return %row;
+}
+
+function AssetInspectorPane::addFieldRow(%this, %container, %field, %label, %kind, %enumItems)
+{
+ %row = %this.makeFieldRow(%container, %field, %label, %kind, %enumItems);
+
+ %this.row[%field] = %row;
+ %this.rowFields = (%this.rowFields $= "") ? %field : (%this.rowFields SPC %field);
+ return %row;
+}
+
+// A collapsible section holding one row per named field, with the labels and
+// kinds coming from the subclass's tables.
+function AssetInspectorPane::buildSection(%this, %key, %title, %fields, %cellW)
+{
+ %panel = %this.makeSectionPanel(%title);
+ %this.add(%panel);
+
+ %grid = %this.makeCellGrid(24, %cellW);
+ %panel.add(%grid);
+
+ %this.panel[%key] = %panel;
+ %this.panelFields[%key] = %fields;
+ %this.panelList = (%this.panelList $= "") ? %key : (%this.panelList SPC %key);
+
+ %count = getWordCount(%fields);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %field = getWord(%fields, %i);
+ %this.addFieldRow(%grid, %field, %this.labelFor(%field),
+ %this.kindFor(%field), %this.enumItemsFor(%field));
+ }
+ return %panel;
+}
+
+//-----------------------------------------------------------------------------
+// Field presentation. The defaults answer for any field at all, so a subclass
+// that forgets one gets a text box named after it rather than nothing.
+//-----------------------------------------------------------------------------
+
+function AssetInspectorPane::labelFor(%this, %field)
+{
+ return %field;
+}
+
+function AssetInspectorPane::kindFor(%this, %field)
+{
+ return "text";
+}
+
+function AssetInspectorPane::enumItemsFor(%this, %field)
+{
+ return "";
+}
+
+// What a field means, for the ones whose name does not say it.
+//
+// The engine would be the obvious place to get this from and is not one: a
+// field's registered doc string is empty on all six of AudioAsset's and on most
+// of everything else, so the stock inspector has nothing to show either. Left
+// empty here, which is the same as having no tooltip; a pane answers for the
+// handful of its own fields that need explaining.
+function AssetInspectorPane::tipFor(%this, %field)
+{
+ return "";
+}
+
+// How deep a paragraph box should be. Zero takes the row's own three lines,
+// which is right for a field sharing a block with others and wrong for one that
+// has a whole cell of the grid to fill.
+function AssetInspectorPane::editorHeightFor(%this, %field)
+{
+ return 0;
+}
+
+// What kind of asset an "asset" row offers to pick. Empty leaves the row on its
+// own default of ImageAsset, which is what every asset row on every pane wanted
+// until an emitter turned out to reference an animation as readily as an image.
+function AssetInspectorPane::assetTypeFor(%this, %field)
+{
+ return "";
+}
+
+//-----------------------------------------------------------------------------
+// Binding. There is no rebuild here at all: the pane is built once for the kind
+// of asset it edits, and binding only ever loads values -- so a selection change
+// can never free a control the engine is mid-dispatch on.
+//-----------------------------------------------------------------------------
+
+function AssetInspectorPane::bind(%this, %asset, %assetId)
+{
+ if(!isObject(%asset))
+ {
+ %this.unbind();
+ return;
+ }
+
+ %this.target = %asset;
+ %this.assetId = %assetId;
+
+ // What a "file" row's Find button measures its answer against. An asset's
+ // loose files are stored relative to the folder the asset itself lives in,
+ // not to the game root -- see EditorFieldRow::pathBase.
+ %this.findBase = AssetDatabase.getAssetPath(%assetId);
+
+ %this.refresh();
+ %this.forceLayout();
+ %this.setVisible(true);
+}
+
+// Nothing selected. The rows keep their values and the whole pane stops drawing,
+// so nothing stale is left on show.
+function AssetInspectorPane::unbind(%this)
+{
+ %this.target = "";
+ %this.assetId = "";
+ %this.setVisible(false);
+}
+
+//-----------------------------------------------------------------------------
+// Loading values. The populating guard keeps every setText / setColorF /
+// setStateOn from echoing straight back through the commits.
+//-----------------------------------------------------------------------------
+
+function AssetInspectorPane::refresh(%this)
+{
+ if(!isObject(%this.target))
+ {
+ return;
+ }
+
+ %this.populating = true;
+
+ %count = getWordCount(%this.rowFields);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %field = getWord(%this.rowFields, %i);
+ %row = %this.row[%field];
+ if(isObject(%row))
+ {
+ %row.setValue(%this.readField(%field));
+ }
+ }
+
+ %this.refreshExtras();
+
+ %this.populating = false;
+}
+
+// Whatever the row loop does not reach: read-only readouts, widgets that are not
+// field rows, and any greying that depends on the asset's current state.
+function AssetInspectorPane::refreshExtras(%this)
+{
+}
+
+// An asset changed underneath us -- possibly this pane's own commit coming back
+// round, because every asset setter ends in refreshAsset(), and possibly a
+// change made on one of the other tabs.
+//
+// The committing guard is what stops the bounce. Nothing here rebuilds, so the
+// re-entry is not dangerous, but reloading every row in the middle of a commit
+// would overwrite the box the user is still in.
+function AssetInspectorPane::onAssetRefreshed(%this, %asset)
+{
+ if(%this.committing || !isObject(%this.target) || %this.target != %asset)
+ {
+ return;
+ }
+
+ %this.refresh();
+}
+
+function AssetInspectorPane::readField(%this, %field)
+{
+ return %this.target.getFieldValue(%field);
+}
+
+function AssetInspectorPane::writeField(%this, %field, %value)
+{
+ %this.target.setFieldValue(%field, %value);
+}
+
+//-----------------------------------------------------------------------------
+// Commits. Every write to the asset goes through here.
+//
+// Each of them also writes the asset's file: every setter on AssetBase and
+// ImageAsset ends in refreshAsset(), which saves the .asset.taml then and there
+// and cascades to anything depending on it. That is why a row commits on blur
+// and on Enter and never per keystroke -- EditorFieldRow's AltCommand and
+// ReturnCommand -- and why an unchanged row is left alone rather than written
+// back over itself.
+//-----------------------------------------------------------------------------
+
+function AssetInspectorPane::onFieldRowCommit(%this, %row)
+{
+ if(%this.populating || !isObject(%this.target) || !%row.hasChanged())
+ {
+ return;
+ }
+
+ %this.commitValue(%row.fieldName, %row.getValue());
+ %row.markClean();
+}
+
+// One value, written and announced. Kept apart from the row handler so the
+// widgets that are not rows -- a cell table, a toggle -- reach the asset the
+// same way, through the same guard.
+function AssetInspectorPane::commitValue(%this, %field, %value)
+{
+ if(!isObject(%this.target))
+ {
+ return;
+ }
+
+ // Name the undo step after the field being changed, so the tooltip reads
+ // "Undo Cell Width" rather than "Undo Edit". Only the paths that know what
+ // they did can do this; a particle graph drag happens entirely in C++ and gets
+ // the generic name.
+ AssetAdmin.undoRecorder.setLabel(%field);
+
+ // The change lands in refreshAsset, which comes straight back at
+ // onAssetRefreshed. The values here are already what the asset holds, so the
+ // bounce has nothing to say; what follows it does.
+ %this.committing = true;
+ %this.writeField(%field, %value);
+ %this.committing = false;
+
+ %this.refresh();
+ %this.afterCommit();
+}
+
+function AssetInspectorPane::onFieldRowReset(%this, %row)
+{
+}
+
+// Everything a write has to tell the rest of the editor.
+function AssetInspectorPane::afterCommit(%this)
+{
+}
+
+//-----------------------------------------------------------------------------
+// Enabling. A field an asset is currently ignoring stays visible but inert, with
+// a tooltip saying why -- blanking it would leave no way to see what it holds.
+//-----------------------------------------------------------------------------
+
+function AssetInspectorPane::setRowsEnabled(%this, %fields, %enabled, %reason)
+{
+ %count = getWordCount(%fields);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %row = %this.row[getWord(%fields, %i)];
+ if(isObject(%row))
+ {
+ %row.setEnabled(%enabled, %reason);
+ }
+ }
+}
diff --git a/editor/AssetAdmin/Inspector/AssetParticleInspectorPane.cs b/editor/AssetAdmin/Inspector/AssetParticleInspectorPane.cs
new file mode 100644
index 000000000..c56316dc8
--- /dev/null
+++ b/editor/AssetAdmin/Inspector/AssetParticleInspectorPane.cs
@@ -0,0 +1,339 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2013 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+
+//-----------------------------------------------------------------------------
+// The inspector for a particle asset, in place of the generic one. This is the
+// ASSET half; the emitter dropdown in the title bar swaps to
+// AssetEmitterInspectorPane for everything below index 0.
+//
+// A ParticleAsset registers two persistent fields of its own -- Lifetime and
+// LifeMode -- and that is genuinely all of it. Everything else an effect is made
+// of lives either on its emitters or in the nine scale graphs, and neither is a
+// value a text box can hold. So this pane is small on purpose, and what it adds
+// over a list of two fields is the pair of readouts: what the effect is made of,
+// and whether any of it will actually draw.
+//
+// Three blocks, the shape AssetFontInspectorPane uses:
+//
+// Identity the name and the category
+// Effect how long it runs and what it does when it gets there
+// Description the prose the library is searched by
+//
+// The nine *Scale fields (LifetimeScale, QuantityScale, SizeXScale ... ) are not
+// here and cannot be: each is a curve over the effect's age rather than a number,
+// and the Scale Graph tab beside this one is where a curve is drawn. Same for the
+// emitters, which are objects rather than values.
+//
+// AssetName is shown but not editable, for the reason the other panes give:
+// AssetBase::setAssetName does nothing once the asset manager owns the asset, so
+// a box that accepted typing would silently do nothing. A real rename is
+// AssetDatabase.renameDeclaredAsset.
+//
+// Absent, each for a checkable reason:
+// AssetInternal, AssetPrivate they exist to keep an asset OUT of the editor
+// asset id, asset file the module and the name are on show, and the
+// file is where the manager put it
+//-----------------------------------------------------------------------------
+
+$AssetParticleInspectorPane::cellWidth = 300;
+$AssetParticleInspectorPane::cellCount = 3;
+$AssetParticleInspectorPane::descriptionHeight = 150;
+
+function AssetParticleInspectorPane::onAdd(%this)
+{
+ // onAdd does not chain, so the shared setup runs from here.
+ %this.init();
+}
+
+//-----------------------------------------------------------------------------
+// Construction.
+//-----------------------------------------------------------------------------
+
+function AssetParticleInspectorPane::buildPane(%this)
+{
+ %grid = %this.makeCellGrid(0, $AssetParticleInspectorPane::cellWidth,
+ $AssetParticleInspectorPane::cellCount);
+ %this.add(%grid);
+ %this.contentGrid = %grid;
+
+ %this.buildIdentityCell(%grid);
+ %this.buildEffectCell(%grid);
+ %this.buildDescriptionCell(%grid);
+
+ %this.buildWarning();
+
+ %this.nameRow.setEnabled(false, "Renaming an asset changes its id and every file that refers to it, " @
+ "so it is not something the inspector can do safely on its own.");
+}
+
+// addFieldRow takes the label and the kind as arguments rather than asking the
+// tables for them, so every call here would otherwise repeat the same lookups.
+function AssetParticleInspectorPane::addField(%this, %container, %field)
+{
+ return %this.addFieldRow(%container, %field, %this.labelFor(%field),
+ %this.kindFor(%field), %this.enumItemsFor(%field));
+}
+
+function AssetParticleInspectorPane::buildIdentityCell(%this, %grid)
+{
+ %chain = %this.makeCell(%grid, 4);
+ %this.identityChain = %chain;
+
+ %this.nameRow = %this.addField(%chain, "AssetName");
+ %this.addField(%chain, "AssetCategory");
+}
+
+function AssetParticleInspectorPane::buildEffectCell(%this, %grid)
+{
+ %chain = %this.makeCell(%grid, 4);
+ %this.effectChain = %chain;
+
+ // LifeMode first, because it decides whether the Lifetime under it means
+ // anything at all.
+ %this.lifeModeRow = %this.addField(%chain, "LifeMode");
+ %this.lifetimeRow = %this.addField(%chain, "Lifetime");
+
+ // What the effect is made of. Read-only: an emitter is not a value, and the
+ // dropdown in the title bar is where one is chosen.
+ %this.infoLabel = %this.makeInfoLabel(%chain, "labelProfile");
+ %this.infoLabel.textWrap = true;
+ %this.infoLabel.textExtend = true;
+ %this.infoLabel.vAlign = "top";
+}
+
+function AssetParticleInspectorPane::buildDescriptionCell(%this, %grid)
+{
+ %chain = %this.makeCell(%grid, 4);
+ %this.descriptionChain = %chain;
+
+ %this.addField(%chain, "AssetDescription");
+}
+
+// Below the grid rather than in a block. A warning is a sentence, and a sentence
+// read across the whole pane is one or two lines where the same sentence in a
+// third of it is five.
+function AssetParticleInspectorPane::buildWarning(%this)
+{
+ %this.warningLabel = %this.makeInfoLabel(%this, "overrideLabelProfile");
+ %this.warningLabel.textWrap = true;
+ %this.warningLabel.textExtend = true;
+ %this.warningLabel.vAlign = "top";
+ %this.warningLabel.setVisible(false);
+}
+
+//-----------------------------------------------------------------------------
+// The field tables.
+//-----------------------------------------------------------------------------
+
+function AssetParticleInspectorPane::labelFor(%this, %field)
+{
+ switch$(%field)
+ {
+ case "AssetName": return "Asset Name";
+ case "AssetCategory": return "Category";
+ case "AssetDescription": return "Description";
+ case "LifeMode": return "Life Mode";
+ case "Lifetime": return "Lifetime (seconds)";
+ }
+
+ return %field;
+}
+
+function AssetParticleInspectorPane::kindFor(%this, %field)
+{
+ switch$(%field)
+ {
+ case "LifeMode": return "enum";
+ case "Lifetime": return "decimal";
+ case "AssetDescription": return "multiline";
+ }
+
+ return "text";
+}
+
+function AssetParticleInspectorPane::enumItemsFor(%this, %field)
+{
+ // The engine's labels are INFINITE, CYCLE, STOP and KILL (LifeModeTable in
+ // ParticleAsset.cc). Both the lookup on the way in and the drop-down's own
+ // search ignore case, so these can read as words.
+ if(%field $= "LifeMode")
+ {
+ return "Infinite" TAB "Cycle" TAB "Stop" TAB "Kill";
+ }
+
+ return "";
+}
+
+function AssetParticleInspectorPane::tipFor(%this, %field)
+{
+ switch$(%field)
+ {
+ case "LifeMode": return "What happens when the effect reaches its lifetime. Infinite never gets " @
+ "there. Cycle starts it again from the beginning. Stop stops emitting and lets the particles " @
+ "already out live their own lifetimes. Kill deletes the player outright, which is what a " @
+ "one-shot explosion wants.";
+
+ case "Lifetime": return "How long the effect runs before its life mode takes over, in seconds. " @
+ "This is the EFFECT's clock -- the one the scale graphs are drawn against -- not how long an " @
+ "individual particle lasts, which is the emitter's Lifetime.";
+
+ case "AssetCategory": return "A word of your own for grouping assets in the library. Nothing in " @
+ "the engine reads it.";
+
+ case "AssetDescription": return "What this effect is for. The library's search box reads it.";
+ }
+
+ return "";
+}
+
+function AssetParticleInspectorPane::editorHeightFor(%this, %field)
+{
+ if(%field $= "AssetDescription")
+ {
+ return $AssetParticleInspectorPane::descriptionHeight;
+ }
+
+ return 0;
+}
+
+//-----------------------------------------------------------------------------
+// Loading. Everything the row loop does not reach.
+//-----------------------------------------------------------------------------
+
+function AssetParticleInspectorPane::refreshExtras(%this)
+{
+ %this.infoLabel.setText(%this.describeEffect(%this.target));
+ %this.applyGating();
+ %this.showWarning(%this.warningFor(%this.target));
+}
+
+// The pane's one gating rule. An infinite effect never reaches its lifetime, so
+// the number is not read -- greyed rather than hidden, because it still holds a
+// value that comes back the moment the mode changes.
+function AssetParticleInspectorPane::applyGating(%this)
+{
+ if(!isObject(%this.target))
+ {
+ return;
+ }
+
+ %infinite = (%this.target.getLifeMode() $= "INFINITE");
+
+ %this.setRowsEnabled("Lifetime", !%infinite,
+ "An infinite effect never reaches its lifetime, so this is not read. Choose another life mode " @
+ "to use it.");
+}
+
+// What the effect is made of. Asked of the asset, stored nowhere on it, which is
+// why it is a line of text rather than a set of rows.
+function AssetParticleInspectorPane::describeEffect(%this, %asset)
+{
+ %count = %asset.getEmitterCount();
+
+ if(%count == 0)
+ {
+ return "No emitters.";
+ }
+
+ %names = "";
+ for(%i = 0; %i < %count; %i++)
+ {
+ %name = %asset.getEmitter(%i).getEmitterName();
+ if(%name $= "")
+ {
+ %name = "(unnamed)";
+ }
+
+ %names = (%names $= "") ? %name : (%names @ ", " @ %name);
+ }
+
+ return %count SPC ((%count == 1) ? "emitter" : "emitters") @ ":" SPC %names @ ".";
+}
+
+// In the order they matter. Only the first is shown, because the first is the one
+// that has to be fixed before any of the others can be judged.
+function AssetParticleInspectorPane::warningFor(%this, %asset)
+{
+ // ParticleAsset::isAssetValid is exactly this test, and an invalid particle
+ // asset draws nothing at all.
+ if(%asset.getEmitterCount() == 0)
+ {
+ return "This effect has no emitters, so nothing will be drawn. Add one with the + button beside " @
+ "the dropdown above.";
+ }
+
+ // An emitter with neither an image nor an animation is skipped outright by
+ // ParticlePlayer, both when it builds its emitter nodes and when it renders --
+ // silently, so without this there is nothing to see and nothing said.
+ %blank = 0;
+ %count = %asset.getEmitterCount();
+ for(%i = 0; %i < %count; %i++)
+ {
+ %emitter = %asset.getEmitter(%i);
+ if(%emitter.getImage() $= "" && %emitter.getAnimation() $= "")
+ {
+ %blank++;
+ }
+ }
+
+ if(%blank > 0)
+ {
+ if(%blank == %count)
+ {
+ return ((%count == 1) ? "This effect's emitter has" : "None of this effect's emitters have") SPC
+ "an image or an animation, so nothing will be drawn. Choose one on the emitter's page.";
+ }
+
+ return %blank SPC "of this effect's" SPC %count SPC "emitters have no image or animation and will " @
+ "not be drawn. Choose one on each emitter's page.";
+ }
+
+ return "";
+}
+
+// forceLayout only when the visibility actually changed: a chain skips hidden
+// children when it lays out, and nothing re-lays it out on setVisible.
+function AssetParticleInspectorPane::showWarning(%this, %text)
+{
+ %wanted = (%text !$= "");
+ %changed = (%wanted != %this.warningLabel.isVisible());
+
+ %this.warningLabel.setText(%text);
+ %this.warningLabel.setVisible(%wanted);
+
+ if(%changed)
+ {
+ %this.forceLayout();
+ }
+}
+
+// The commit ends in refreshAsset, which rebuilds the preview player's emitter
+// nodes -- so the transport's play/stop state is no longer whatever it was, and
+// the solo it was holding has been rebuilt away. The bar is the only thing that
+// knows either, so it is the only thing that has to be told.
+function AssetParticleInspectorPane::afterCommit(%this)
+{
+ if(isObject(AssetAdmin.particleTransportBar))
+ {
+ AssetAdmin.particleTransportBar.refresh();
+ }
+}
diff --git a/editor/AssetAdmin/Inspector/AssetSoundInspectorPane.cs b/editor/AssetAdmin/Inspector/AssetSoundInspectorPane.cs
new file mode 100644
index 000000000..06d132991
--- /dev/null
+++ b/editor/AssetAdmin/Inspector/AssetSoundInspectorPane.cs
@@ -0,0 +1,378 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2013 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+
+//-----------------------------------------------------------------------------
+// The inspector for an audio asset, in place of the generic one. Three blocks,
+// the shape AssetAnimationInspectorPane uses:
+//
+// Identity the name, the category, the sound file, and how long that file
+// is -- the one thing about a sound that no field says
+// Playback how loud, on which channel, and the three flags
+// Description the prose the library is searched by
+//
+// An AudioAsset registers six fields of its own and not one of them carries a
+// doc string, so the generic inspector showed six labels and explained none of
+// them. Half the value of this pane is tipFor below.
+//
+// The preview is unchanged: selecting a sound in the library auditions it
+// through the Play button overlaid on the preview area
+// (AssetAdmin::buildAudioPlayButton, AssetWindow::displayAudioAsset). This pane
+// deliberately does not grow a transport of its own.
+//
+// Absent, each for a checkable reason:
+//
+// AssetAutoUnload NOT hidden because it is uninteresting -- it is
+// hidden because it does not work here.
+// AudioAsset::initializeAsset calls
+// setAssetAutoUnload(false) unconditionally, so
+// every audio asset reads false whatever its file
+// says, and a tick would silently come back off
+// the next time the asset was loaded. A checkbox
+// that cannot be changed is worse than no
+// checkbox: it invites the attempt.
+// AssetInternal, AssetPrivate they exist to keep an asset OUT of the editor
+// asset id, asset file the module and the name are on show, and the
+// file is where the manager put it
+// the eight 3D fields is3D, referenceDistance, maxDistance, the cone
+// family and environmentLevel are commented out
+// of AudioAsset::initPersistFields, and mIs3D is
+// hard-set false in the constructor. A sound
+// played from an asset id is never positional;
+// the positional path is SceneObject::playSound
+// with an AudioDescription datablock, which is a
+// different object with its own fields.
+//-----------------------------------------------------------------------------
+
+$AssetSoundInspectorPane::cellWidth = 300;
+$AssetSoundInspectorPane::cellCount = 3;
+$AssetSoundInspectorPane::descriptionHeight = 150;
+
+// The gain at or below which alxCreateSource refuses to make a source at all
+// rather than making a quiet one (MIN_GAIN, audio.cc). A sound this quiet is not
+// faint, it is absent, which is worth saying out loud.
+$AssetSoundInspectorPane::minimumGain = 0.05;
+
+// Audio::AudioVolumeChannels - 1. Every channel has its own volume and there is
+// no engine-side naming for any of them.
+$AssetSoundInspectorPane::maxChannel = 31;
+
+function AssetSoundInspectorPane::onAdd(%this)
+{
+ // onAdd does not chain, so the shared setup runs from here.
+ %this.init();
+
+ // What the file row's Find button offers. These are the formats the engine
+ // actually registers with the resource manager (OpenALInitDriver) -- the same
+ // list NewAudioAssetDialog uses.
+ %this.fileFilters = "Audio Files (*.wav;*.ogg)|*.wav;*.ogg|All Files (*.*)|*.*";
+ %this.fileTitle = "Choose an Audio File";
+}
+
+//-----------------------------------------------------------------------------
+// Construction.
+//-----------------------------------------------------------------------------
+
+function AssetSoundInspectorPane::buildPane(%this)
+{
+ %grid = %this.makeCellGrid(0, $AssetSoundInspectorPane::cellWidth,
+ $AssetSoundInspectorPane::cellCount);
+ %this.add(%grid);
+ %this.contentGrid = %grid;
+
+ %this.buildIdentityCell(%grid);
+ %this.buildPlaybackCell(%grid);
+ %this.buildDescriptionCell(%grid);
+
+ %this.buildWarning();
+
+ %this.nameRow.setEnabled(false, "Renaming an asset changes its id and every file that refers to it, " @
+ "so it is not something the inspector can do safely on its own.");
+}
+
+function AssetSoundInspectorPane::addField(%this, %container, %field)
+{
+ return %this.addFieldRow(%container, %field, %this.labelFor(%field),
+ %this.kindFor(%field), %this.enumItemsFor(%field));
+}
+
+function AssetSoundInspectorPane::buildIdentityCell(%this, %grid)
+{
+ %chain = %this.makeCell(%grid, 4);
+ %this.identityChain = %chain;
+
+ %this.nameRow = %this.addField(%chain, "AssetName");
+ %this.addField(%chain, "AssetCategory");
+ %this.fileRow = %this.addField(%chain, "AudioFile");
+
+ // Under the file, which is what it describes -- the length and the format are
+ // both facts about that .wav, not about how loudly it is played. It also has
+ // room to breathe here: the playback block is five rows deep and this block is
+ // three, so the space at the bottom of this one was going spare.
+ %this.infoLabel = %this.makeInfoLabel(%chain, "labelProfile");
+ %this.infoLabel.textWrap = true;
+ %this.infoLabel.textExtend = true;
+ %this.infoLabel.vAlign = "top";
+}
+
+function AssetSoundInspectorPane::buildPlaybackCell(%this, %grid)
+{
+ %chain = %this.makeCell(%grid, 4);
+ %this.playbackChain = %chain;
+
+ %this.addField(%chain, "Volume");
+ %this.addField(%chain, "VolumeChannel");
+ %this.addField(%chain, "Looping");
+ %this.addField(%chain, "Streaming");
+ %this.addField(%chain, "Priority");
+}
+
+function AssetSoundInspectorPane::buildDescriptionCell(%this, %grid)
+{
+ %chain = %this.makeCell(%grid, 4);
+ %this.descriptionChain = %chain;
+
+ %this.addField(%chain, "AssetDescription");
+}
+
+function AssetSoundInspectorPane::buildWarning(%this)
+{
+ %this.warningLabel = %this.makeInfoLabel(%this, "overrideLabelProfile");
+ %this.warningLabel.textWrap = true;
+ %this.warningLabel.textExtend = true;
+ %this.warningLabel.vAlign = "top";
+ %this.warningLabel.setVisible(false);
+}
+
+//-----------------------------------------------------------------------------
+// The field tables.
+//-----------------------------------------------------------------------------
+
+function AssetSoundInspectorPane::labelFor(%this, %field)
+{
+ switch$(%field)
+ {
+ case "AssetName": return "Asset Name";
+ case "AssetCategory": return "Category";
+ case "AssetDescription": return "Description";
+ case "AudioFile": return "Audio File";
+ case "VolumeChannel": return "Volume Channel";
+ }
+
+ return %field;
+}
+
+function AssetSoundInspectorPane::kindFor(%this, %field)
+{
+ switch$(%field)
+ {
+ case "AudioFile": return "file";
+ case "Volume": return "decimal";
+ case "VolumeChannel": return "number";
+ case "Looping" or "Streaming" or "Priority": return "bool";
+ case "AssetDescription": return "multiline";
+ }
+
+ return "text";
+}
+
+// The engine's own doc strings for all six of these fields are the empty string,
+// so there is nothing to inherit and nowhere else a reader could find this out.
+function AssetSoundInspectorPane::tipFor(%this, %field)
+{
+ switch$(%field)
+ {
+ case "AudioFile": return "A .wav or .ogg file. Those are the only two formats the engine reads.";
+
+ case "Volume": return "How loud this sound is, from 0 to 1, before the channel volume is applied.";
+
+ case "VolumeChannel": return "Which of the 32 mixer channels (0 to" SPC
+ $AssetSoundInspectorPane::maxChannel @ ") this sound plays on. Each channel has its own volume, " @
+ "so a game can fade music without touching effects. This project's scripts use 0 for music and " @
+ "1 for effects, but that is a convention -- the engine attaches no meaning to any channel.";
+
+ case "Looping": return "Repeat until something stops it. Looping sounds are also never dropped " @
+ "when the mixer runs short of voices.";
+
+ case "Streaming": return "Decode a piece at a time while it plays rather than loading the whole " @
+ "file up front. Worth it for music, wasteful for a short effect.";
+
+ case "Priority": return "Keep this sound when the mixer runs out of voices and has to drop one.";
+ }
+
+ return "";
+}
+
+function AssetSoundInspectorPane::editorHeightFor(%this, %field)
+{
+ if(%field $= "AssetDescription")
+ {
+ return $AssetSoundInspectorPane::descriptionHeight;
+ }
+
+ return 0;
+}
+
+// The stored path is absolute -- setAudioFile expands whatever it is given
+// against the asset's own folder -- and an absolute path is neither readable nor
+// portable. Show the collapsed form, which is what the file on disk says.
+function AssetSoundInspectorPane::readField(%this, %field)
+{
+ if(%field $= "AudioFile")
+ {
+ return %this.target.getRelativeAudioFile();
+ }
+
+ return %this.target.getFieldValue(%field);
+}
+
+// Both of these are clamped by the engine as well. Clamping here too is not
+// belt and braces: setVolume and setVolumeChannel compare the value they were
+// given against the one they hold and clamp only afterwards, so handing them
+// something out of range reads as a change every single time and marks the asset
+// unsaved for an edit that moves nothing. Sending only values that are already
+// in range means that never comes up.
+function AssetSoundInspectorPane::writeField(%this, %field, %value)
+{
+ // mClamp, not mClampF. The latter is the C++ name and is not bound to script
+ // at all, so it would return an empty string here and quietly write a zero.
+ if(%field $= "Volume")
+ {
+ %this.target.Volume = mClamp(%value, 0, 1);
+ return;
+ }
+
+ if(%field $= "VolumeChannel")
+ {
+ %this.target.VolumeChannel = mFloor(mClamp(%value, 0, $AssetSoundInspectorPane::maxChannel));
+ return;
+ }
+
+ %this.target.setFieldValue(%field, %value);
+}
+
+//-----------------------------------------------------------------------------
+// Loading. Everything the row loop does not reach.
+//-----------------------------------------------------------------------------
+
+function AssetSoundInspectorPane::refreshExtras(%this)
+{
+ %this.infoLabel.setText(%this.describeSound(%this.target));
+ %this.showWarning(%this.warningFor(%this.target));
+}
+
+// How long the sound is, in milliseconds, measured at most once per file.
+//
+// Not measured in refreshExtras, which runs on every bind, every commit and
+// every refresh an asset announces: alxGetAudioLength decodes the file to find
+// out, and doing that to a music track each time somebody types a letter in the
+// description box is not free. The answer only changes when the file does.
+function AssetSoundInspectorPane::lengthOf(%this, %asset)
+{
+ %file = %asset.getRelativeAudioFile();
+
+ if(%this.measuredId $= %this.assetId && %this.measuredFile $= %file)
+ {
+ return %this.measuredLength;
+ }
+
+ %this.measuredId = %this.assetId;
+ %this.measuredFile = %file;
+ %this.measuredLength = alxGetAudioLength(%this.assetId);
+
+ return %this.measuredLength;
+}
+
+// The one thing about a sound that is not on show as a field: how long it is.
+//
+// A length of zero is reported as unknown rather than as an error. It means the
+// buffer could not be read, and the reasons for that are mostly not the asset's
+// fault -- no audio device on the machine, or a driver that would not start.
+//
+// The channel's current volume is deliberately NOT reported here, though it is
+// the best answer to "why can I not hear it". alxGetChannelVolume reads a plain
+// global array that is only filled in when the audio driver starts, so before
+// that every channel answers zero -- and an editor that said "channel 0 is at 0%"
+// about every sound in the library would be worse than saying nothing.
+function AssetSoundInspectorPane::describeSound(%this, %asset)
+{
+ %length = %this.lengthOf(%asset);
+ %format = strupr(getSubStr(fileExt(%asset.getRelativeAudioFile()), 1, 8));
+
+ if(%length > 0)
+ {
+ %line = mFloatLength(%length / 1000, 2) SPC "s";
+ %line = (%format $= "") ? %line : (%line @ "," SPC %format);
+ }
+ else
+ {
+ %line = (%format $= "") ? "Length unknown" : (%format @ ", length unknown");
+ }
+
+ return %line @ ".";
+}
+
+// In the order they matter. Only the first is shown, because the first is the one
+// that has to be fixed before any of the others can be judged.
+function AssetSoundInspectorPane::warningFor(%this, %asset)
+{
+ %ext = fileExt(%asset.getRelativeAudioFile());
+
+ // The stream factory answers with nothing at all for any other extension, so
+ // the sound simply never plays and says nothing about why.
+ if(%asset.Streaming && %ext !$= ".wav" && %ext !$= ".ogg")
+ {
+ return "Streaming only works for .wav and .ogg files. This one is" SPC
+ (%ext $= "" ? "not either" : %ext) @ ", so it will not play at all while Streaming is on.";
+ }
+
+ // Not "quiet" -- absent. alxCreateSource refuses to make a source whose gain
+ // has fallen this low, and it makes the exception for looping and streaming
+ // sounds, which are never dropped this way.
+ if(%asset.Volume <= $AssetSoundInspectorPane::minimumGain && !%asset.Looping && !%asset.Streaming)
+ {
+ return "A volume of" SPC $AssetSoundInspectorPane::minimumGain SPC "or below means this sound is " @
+ "not created at all rather than played quietly. Raise it, or turn on Looping or Streaming, " @
+ "which are exempt.";
+ }
+
+ // A muted channel is not warned about, for the reason describeSound gives:
+ // the channel volumes are zero until the audio driver starts, so the check
+ // cannot tell "somebody muted this" from "nothing has made a sound yet".
+
+ return "";
+}
+
+// forceLayout only when the visibility actually changed: a chain skips hidden
+// children when it lays out, and nothing re-lays it out on setVisible.
+function AssetSoundInspectorPane::showWarning(%this, %text)
+{
+ %wanted = (%text !$= "");
+ %changed = (%wanted != %this.warningLabel.isVisible());
+
+ %this.warningLabel.setText(%text);
+ %this.warningLabel.setVisible(%wanted);
+
+ if(%changed)
+ {
+ %this.forceLayout();
+ }
+}
diff --git a/editor/AssetAdmin/Inspector/exec.cs b/editor/AssetAdmin/Inspector/exec.cs
new file mode 100644
index 000000000..66a465399
--- /dev/null
+++ b/editor/AssetAdmin/Inspector/exec.cs
@@ -0,0 +1,8 @@
+exec("./AssetInspectorPane.cs");
+exec("./AssetImageCellGrid.cs");
+exec("./AssetAnimationInspectorPane.cs");
+exec("./AssetImageInspectorPane.cs");
+exec("./AssetFontInspectorPane.cs");
+exec("./AssetSoundInspectorPane.cs");
+exec("./AssetParticleInspectorPane.cs");
+exec("./AssetEmitterInspectorPane.cs");
diff --git a/editor/AssetAdmin/NewAssetButton.cs b/editor/AssetAdmin/NewAssetButton.cs
index b97ba37be..252b633b0 100644
--- a/editor/AssetAdmin/NewAssetButton.cs
+++ b/editor/AssetAdmin/NewAssetButton.cs
@@ -1,91 +1,10 @@
//NewAssetButton.cs
+// The library's per-group New button. The dialogs it opens are also on the File
+// menu, so the bodies live on AssetAdmin and this is only the button half - .type
+// is the asset class name the group holds ("ImageAsset"), which is exactly the
+// back half of AssetAdmin::newImageAsset.
function NewAssetButton::onClick(%this)
{
- %this.call("onNew" @ %this.type);
-}
-
-function NewAssetButton::onNewImageAsset(%this)
-{
- %width = 700;
- %height = 340;
- %dialog = new GuiControl()
- {
- class = "NewImageAssetDialog";
- superclass = "EditorDialog";
- dialogSize = (%width + 8) SPC (%height + 8);
- dialogCanClose = true;
- dialogText = "New Image Asset";
- };
- %dialog.init(%width, %height);
-
- Canvas.pushDialog(%dialog);
-}
-
-function NewAssetButton::onNewAnimationAsset(%this)
-{
- %width = 700;
- %height = 390;
- %dialog = new GuiControl()
- {
- class = "NewAnimationAssetDialog";
- superclass = "EditorDialog";
- dialogSize = (%width + 8) SPC (%height + 8);
- dialogCanClose = true;
- dialogText = "New Animation Asset";
- };
- %dialog.init(%width, %height);
-
- Canvas.pushDialog(%dialog);
-}
-
-function NewAssetButton::onNewParticleAsset(%this)
-{
- %width = 700;
- %height = 440;
- %dialog = new GuiControl()
- {
- class = "NewParticleAssetDialog";
- superclass = "EditorDialog";
- dialogSize = (%width + 8) SPC (%height + 8);
- dialogCanClose = true;
- dialogText = "New Particle Asset";
- };
- %dialog.init(%width, %height);
-
- Canvas.pushDialog(%dialog);
-}
-
-function NewAssetButton::onNewFontAsset(%this)
-{
- %width = 700;
- %height = 340;
- %dialog = new GuiControl()
- {
- class = "NewFontAssetDialog";
- superclass = "EditorDialog";
- dialogSize = (%width + 8) SPC (%height + 8);
- dialogCanClose = true;
- dialogText = "New Bitmap Font Asset";
- };
- %dialog.init(%width, %height);
-
- Canvas.pushDialog(%dialog);
-}
-
-function NewAssetButton::onNewAudioAsset(%this)
-{
- %width = 700;
- %height = 340;
- %dialog = new GuiControl()
- {
- class = "NewAudioAssetDialog";
- superclass = "EditorDialog";
- dialogSize = (%width + 8) SPC (%height + 8);
- dialogCanClose = true;
- dialogText = "New Audio Asset";
- };
- %dialog.init(%width, %height);
-
- Canvas.pushDialog(%dialog);
+ AssetAdmin.call("new" @ %this.type);
}
diff --git a/editor/AssetAdmin/NewAudioAssetDialog.cs b/editor/AssetAdmin/NewAudioAssetDialog.cs
index ae7ea6b18..5f82565bd 100644
--- a/editor/AssetAdmin/NewAudioAssetDialog.cs
+++ b/editor/AssetAdmin/NewAudioAssetDialog.cs
@@ -169,8 +169,10 @@ class = "EditorForm";
%moduleDef = ModuleDatabase.findModule(%moduleName, %moduleVersion);
AssetDatabase.addDeclaredAsset(%moduleDef, %assetPath);
- //Refresh the asset so that the loose file will be a path relative to the asset file.
- AssetDatabase.refreshAsset(%assetID);
+ //Save the asset so that the loose file will be a path relative to the asset
+ //file. That collapse happens in onTamlPreWrite, so it only happens on a
+ //write -- and refreshAsset no longer writes.
+ AssetDatabase.saveAsset(%assetID);
//Do we already have this button?
%button = AssetAdmin.Dictionary["AudioAsset"].getButton(%assetID);
diff --git a/editor/AssetAdmin/NewFontAssetDialog.cs b/editor/AssetAdmin/NewFontAssetDialog.cs
index 54f0483e5..a4384e1f1 100644
--- a/editor/AssetAdmin/NewFontAssetDialog.cs
+++ b/editor/AssetAdmin/NewFontAssetDialog.cs
@@ -169,8 +169,10 @@ class = "EditorForm";
%moduleDef = ModuleDatabase.findModule(%moduleName, %moduleVersion);
AssetDatabase.addDeclaredAsset(%moduleDef, %assetPath);
- //Refresh the asset so that the loose file will be a path relative to the asset file.
- AssetDatabase.refreshAsset(%assetID);
+ //Save the asset so that the loose file will be a path relative to the asset
+ //file. That collapse happens in onTamlPreWrite, so it only happens on a
+ //write -- and refreshAsset no longer writes.
+ AssetDatabase.saveAsset(%assetID);
//Do we already have this button?
%button = AssetAdmin.Dictionary["FontAsset"].getButton(%assetID);
diff --git a/editor/AssetAdmin/NewImageAssetDialog.cs b/editor/AssetAdmin/NewImageAssetDialog.cs
index 3e25a0287..a7497f01d 100644
--- a/editor/AssetAdmin/NewImageAssetDialog.cs
+++ b/editor/AssetAdmin/NewImageAssetDialog.cs
@@ -176,8 +176,10 @@ class = "EditorForm";
%moduleDef = ModuleDatabase.findModule(%moduleName, %moduleVersion);
AssetDatabase.addDeclaredAsset(%moduleDef, %assetPath);
- //Refresh the asset so that the loose file will be a path relative to the asset file.
- AssetDatabase.refreshAsset(%assetID);
+ //Save the asset so that the loose file will be a path relative to the asset
+ //file. That collapse happens in onTamlPreWrite, so it only happens on a
+ //write -- and refreshAsset no longer writes.
+ AssetDatabase.saveAsset(%assetID);
//Do we already have this button?
%button = AssetAdmin.Dictionary["ImageAsset"].getButton(%assetID);
diff --git a/editor/AssetAdmin/ParticleEditor/AssetParticleChannelToggle.cs b/editor/AssetAdmin/ParticleEditor/AssetParticleChannelToggle.cs
new file mode 100644
index 000000000..4afa0bf28
--- /dev/null
+++ b/editor/AssetAdmin/ParticleEditor/AssetParticleChannelToggle.cs
@@ -0,0 +1,46 @@
+
+//-----------------------------------------------------------------------------
+// One of the color graph's three channel buttons: a swatch that stays pressed.
+//
+// An EditorToggleIcon with one thing changed. The stock toggle tints its icon
+// with the editor's own inks, bright for on and dim for off, which is right for a
+// switch -- but these three buttons stand for red, green and blue, so the color
+// IS the label. A themed ink would say which button is pressed and nothing about
+// which curve it belongs to.
+//
+// So the tint is the channel's own hue, matched to what GuiEditParticleColorGraph
+// draws that channel's curve in: full strength when the channel is live, faded
+// when it is not, exactly as the curve is. The button and the line it controls
+// are then visibly the same thing.
+//
+// Radio behavior belongs to the owner, not here: a checkbox flips itself, so
+// clicking the live channel would switch it off. AssetParticleColorGraphUnit
+// puts it back.
+//
+// The creator sets channel, owner, and tipOff inline.
+//-----------------------------------------------------------------------------
+
+function AssetParticleChannelToggle::getIconTint(%this, %on)
+{
+ if(!%this.isActive())
+ {
+ return ThemeManager.activeTheme.iconButtonProfile.fontColorNA;
+ }
+
+ // Lifted off the primaries for the same reason the curves are: a pure blue
+ // swatch on a dark panel is close to unreadable. These stay unmistakably red,
+ // green and blue on every editor theme.
+ switch$(%this.channel)
+ {
+ case "Green":
+ %color = %on ? "90 220 110 255" : "90 220 110 130";
+
+ case "Blue":
+ %color = %on ? "105 155 255 255" : "105 155 255 130";
+
+ default:
+ %color = %on ? "255 95 95 255" : "255 95 95 130";
+ }
+
+ return %color;
+}
diff --git a/editor/AssetAdmin/ParticleEditor/AssetParticleColorGraphUnit.cs b/editor/AssetAdmin/ParticleEditor/AssetParticleColorGraphUnit.cs
new file mode 100644
index 000000000..f1955b849
--- /dev/null
+++ b/editor/AssetAdmin/ParticleEditor/AssetParticleColorGraphUnit.cs
@@ -0,0 +1,93 @@
+
+//-----------------------------------------------------------------------------
+// The graph unit for an emitter's color, the one selection that shows something
+// other than a single curve.
+//
+// It is an AssetParticleGraphUnit with two differences: the graph it builds is a
+// GuiEditParticleColorGraph rather than a plain inspector, and it buys itself a
+// second column down the left side for the three channel toggles. Everything
+// else -- the zoom and pan buttons, the cameras, attaching to and detaching from
+// the tool's grid -- is the superclass's and unchanged.
+//
+// Exactly one channel is live. The toggles are a radio group, and which one is
+// pressed is asked of the graph rather than remembered here, so the buttons
+// cannot drift out of step with what a click on the plot will edit.
+//-----------------------------------------------------------------------------
+
+function AssetParticleColorGraphUnit::createGraph(%this)
+{
+ return new GuiEditParticleColorGraph();
+}
+
+function AssetParticleColorGraphUnit::getLeftInset(%this)
+{
+ // 30 for the zoom and pan column the superclass places, and 30 more for the
+ // channel toggles this unit adds outside it.
+ return 60;
+}
+
+function AssetParticleColorGraphUnit::addExtraControls(%this)
+{
+ %this.channelCount = 3;
+ %this.channel[0] = "Red";
+ %this.channel[1] = "Green";
+ %this.channel[2] = "Blue";
+
+ for(%i = 0; %i < %this.channelCount; %i++)
+ {
+ %channel = %this.channel[%i];
+
+ %toggle = new GuiCheckBoxCtrl()
+ {
+ Class = "AssetParticleChannelToggle";
+ superclass = "EditorToggleIcon";
+ channel = %channel;
+ owner = %this;
+ frameOff = $EditorIcon::square_shape;
+ tipOff = "Edit the " @ %channel @ " channel";
+ Position = "2" SPC (18 + (%i * 26));
+ Extent = "24 24";
+ };
+ ThemeManager.setProfile(%toggle, "iconButtonProfile");
+ %this.add(%toggle);
+
+ %this.toggle[%channel] = %toggle;
+ }
+}
+
+// Point the unit at an emitter and show it. The labels say Color rather than the
+// Base Value the other units use: these are life curves, whatever the field
+// collection files them under.
+//
+// The channel is whichever one was already live, so switching emitters leaves you
+// looking at the same channel you were editing. Setting the field is what carries
+// the emitter index, and the graph keeps its live channel in step with it.
+function AssetParticleColorGraphUnit::setToColor(%this, %emitterID)
+{
+ %this.attach();
+ %this.graph.setDisplayLabels("Time", "Color");
+ %this.graph.setDisplayField(%this.graph.getActiveChannel() @ "Channel", %emitterID);
+ %this.refreshToggles();
+}
+
+// A checkbox has already flipped itself by the time this runs, so the live
+// channel is put back on rather than being allowed to switch off, and the other
+// two are cleared.
+function AssetParticleColorGraphUnit::onToggleIconChanged(%this, %toggle)
+{
+ %this.graph.setActiveChannel(%toggle.channel);
+ %this.refreshToggles();
+}
+
+// The graph is asked which channel is live rather than told, so a channel set
+// any other way still lights the right button.
+function AssetParticleColorGraphUnit::refreshToggles(%this)
+{
+ %active = %this.graph.getActiveChannel();
+
+ for(%i = 0; %i < %this.channelCount; %i++)
+ {
+ %channel = %this.channel[%i];
+ %this.toggle[%channel].setValue(%channel $= %active);
+ }
+}
diff --git a/editor/AssetAdmin/ParticleEditor/AssetParticleGraphTool.cs b/editor/AssetAdmin/ParticleEditor/AssetParticleGraphTool.cs
index 2e5608dde..107908620 100644
--- a/editor/AssetAdmin/ParticleEditor/AssetParticleGraphTool.cs
+++ b/editor/AssetAdmin/ParticleEditor/AssetParticleGraphTool.cs
@@ -49,9 +49,11 @@
%this.addItem("Emission Force");
%this.addItem("Emission Angle");
%this.addItem("Emission Arc");
- %this.addItem("Red Channel");
- %this.addItem("Green Channel");
- %this.addItem("Blue Channel");
+
+ // One entry for red, green and blue together. They were three, and three
+ // pictures cannot answer what color a particle is at half its life -- which is
+ // the only thing anyone opens them to find out.
+ %this.addItem("Color Channel");
%this.addItem("Alpha Channel");
}
@@ -107,14 +109,17 @@
ThemeManager.setProfile(%this.toolScroll, "scrollingPanelArrowProfile", "ArrowProfile");
%this.add(%this.toolScroll);
- %itemWidth = 360;
+ // A field rather than a local: initEmitter builds three more units from it, and
+ // as a local it was simply empty there -- which made their authored Extent a
+ // malformed point, and every button in them was placed against it.
+ %this.itemWidth = 360;
%this.toolGrid = new GuiGridCtrl()
{
HorizSizing="width";
VertSizing="height";
Position="0 0";
Extent = (getWord(%this.toolScroll.Extent, 0) - 14) SPC getWord(%this.extent, 1);
- CellSizeX = %itemWidth;
+ CellSizeX = %this.itemWidth;
CellSizeY = 0;
CellModeX = variable;
CellModeY = variable;
@@ -131,8 +136,9 @@
HorizSizing="right";
VertSizing="bottom";
Position="0 0";
- Extent= %itemWidth SPC (getWord(%this.extent, 1) - 30);
+ Extent= %this.itemWidth SPC (getWord(%this.extent, 1) - 30);
Text = "Base Value";
+ Tool = %this.toolGrid;
};
ThemeManager.setProfile(%this.baseGraph, "labelProfile");
%this.toolGrid.add(%this.baseGraph);
@@ -146,7 +152,7 @@
HorizSizing="right";
VertSizing="bottom";
Position="0 0";
- Extent= %itemWidth SPC (getWord(%this.extent, 1) - 30);
+ Extent= %this.itemWidth SPC (getWord(%this.extent, 1) - 30);
Text = "Variation";
Tool = %this.toolGrid;
};
@@ -160,12 +166,47 @@
HorizSizing="right";
VertSizing="bottom";
Position="0 0";
- Extent= %itemWidth SPC (getWord(%this.extent, 1) - 30);
+ Extent= %this.itemWidth SPC (getWord(%this.extent, 1) - 30);
Text = "Scale Over Particle Lifetime";
Tool = %this.toolGrid;
};
ThemeManager.setProfile(%this.lifeGraph, "labelProfile");
%this.toolGrid.add(%this.lifeGraph);
+
+ // The color unit is wider than the others: it is the only one on screen when
+ // it is showing, and the strip under its plot reads better with the room. It
+ // starts out of the grid, since the tool opens on Lifetime.
+ %this.colorGraph = new GuiControl()
+ {
+ Class = "AssetParticleColorGraphUnit";
+ superclass = "AssetParticleGraphUnit";
+ HorizSizing="right";
+ VertSizing="bottom";
+ Position="0 0";
+ Extent= (%this.itemWidth * 2) SPC (getWord(%this.extent, 1) - 30);
+ Text = "Color Over Particle Lifetime";
+ Tool = %this.toolGrid;
+ };
+ ThemeManager.setProfile(%this.colorGraph, "labelProfile");
+ %this.colorGraph.detach();
+}
+
+// The grid deletes the units inside it. A unit that is currently detached is not
+// inside anything, so it is this tool's to delete.
+function AssetParticleGraphEmitterTool::onRemove(%this)
+{
+ %this.deleteDetachedUnit(%this.baseGraph);
+ %this.deleteDetachedUnit(%this.variGraph);
+ %this.deleteDetachedUnit(%this.lifeGraph);
+ %this.deleteDetachedUnit(%this.colorGraph);
+}
+
+function AssetParticleGraphEmitterTool::deleteDetachedUnit(%this, %unit)
+{
+ if(isObject(%unit) && !%this.toolGrid.isMember(%unit))
+ {
+ %unit.delete();
+ }
}
function AssetParticleGraphTool::addItem(%this, %item, %color)
@@ -192,6 +233,10 @@
{
%this.lifeGraph.graph.inspect(%asset);
}
+ if(isObject(%this.colorGraph))
+ {
+ %this.colorGraph.graph.inspect(%asset);
+ }
%this.baseList.clearSelection();
%this.emitterID = %emitterID;
%this.baseList.setCurSel(0);
@@ -265,9 +310,7 @@ class = ParticleGraphCameraController;
%graphTable[%i] = "EmissionForce"; %i++;
%graphTable[%i] = "EmissionAngle"; %i++;
%graphTable[%i] = "EmissionArc"; %i++;
- %graphTable[%i] = "RedChannel"; %i++;
- %graphTable[%i] = "GreenChannel"; %i++;
- %graphTable[%i] = "BlueChannel"; %i++;
+ %graphTable[%i] = "ColorChannel"; %i++;
%graphTable[%i] = "AlphaChannel";
for(%i = 0; %i < 11; %i++)
@@ -281,6 +324,26 @@ class = ParticleGraphCameraController;
}
%name = %graphTable[%index];
+
+ // Color is the one selection that shows a different graph rather than a
+ // different field, so it swaps the whole set of units in the grid.
+ if(%name $= "ColorChannel")
+ {
+ %this.baseGraph.detach();
+ %this.variGraph.detach();
+ %this.lifeGraph.detach();
+
+ %this.colorGraph.setToColor(%this.emitterID);
+
+ // Any of the three channels gives the same window: they are registered with
+ // identical bounds, 0 to 1 over a lifetime of 0 to 1.
+ %this.colorGraph.setValueController(%this.getValueController("RedChannel"));
+ %this.colorGraph.setTimeController(%this.getTimeController("RedChannel"));
+ return;
+ }
+
+ %this.colorGraph.detach();
+ %this.baseGraph.attach();
%this.baseGraph.setToBase(%name, %varTable[%index], %this.emitterID);
%this.baseGraph.setValueController(%this.getValueController(%name));
%this.baseGraph.setTimeController(%this.getTimeController(%name));
diff --git a/editor/AssetAdmin/ParticleEditor/AssetParticleGraphUnit.cs b/editor/AssetAdmin/ParticleEditor/AssetParticleGraphUnit.cs
index 634917e0d..bb1d43ccd 100644
--- a/editor/AssetAdmin/ParticleEditor/AssetParticleGraphUnit.cs
+++ b/editor/AssetAdmin/ParticleEditor/AssetParticleGraphUnit.cs
@@ -1,23 +1,33 @@
function AssetParticleGraphUnit::onAdd(%this)
{
- %this.graph = new GuiParticleGraphInspector()
- {
- HorizSizing="width";
- VertSizing="height";
- Position="30 18";
- Extent= (getWord(%this.extent, 0) - 40) SPC (getWord(%this.extent, 1) - 60);
- };
+ // Everything here is placed against the left inset rather than a literal 30,
+ // so a subclass wanting another column of its own down the left side moves the
+ // whole arrangement by overriding one number.
+ %inset = %this.getLeftInset();
+
+ %this.graph = %this.createGraph();
+ %this.graph.HorizSizing = "width";
+ %this.graph.VertSizing = "height";
+ %this.graph.Position = %inset SPC 18;
+ %this.graph.Extent = (getWord(%this.extent, 0) - %inset - 10) SPC (getWord(%this.extent, 1) - 60);
ThemeManager.setProfile(%this.graph, "graphProfile");
%this.add(%this.graph);
- //Value zoom buttons
+ // The value buttons sit in the column immediately left of the graph, whatever
+ // the inset is.
+ %valueX = %inset - 28;
+
+ // Value zoom buttons. A plus and minus in a square rather than the magnifier
+ // pair these used to wear: the icon set has a magnifier but no +/- variants
+ // of it, and the squared pair stays distinct from the round plus and minus,
+ // which mean add and remove everywhere else in the editor.
%center = 6 + mRound(getWord(%this.graph.extent, 1) / 2);
%this.valueZoomInButton = new GuiButtonCtrl()
{
Class = "EditorIconButton";
- Frame = 0;
- Position = "2" SPC (%center + 13);
+ Frame = $EditorIcon::sq_plus;
+ Position = %valueX SPC (%center + 13);
Command = %this.getId() @ ".valueZoomIn();";
Tooltip = "Zoom In";
};
@@ -27,8 +37,8 @@
%this.valueZoomOutButton = new GuiButtonCtrl()
{
Class = "EditorIconButton";
- Frame = 1;
- Position = "2" SPC (%center - 13);
+ Frame = $EditorIcon::sq_minus;
+ Position = %valueX SPC (%center - 13);
Command = %this.getId() @ ".valueZoomOut();";
Tooltip = "Zoom Out";
};
@@ -39,8 +49,8 @@
%this.valueMoveUpButton = new GuiButtonCtrl()
{
Class = "EditorIconButton";
- Frame = 2;
- Position = "2 18";
+ Frame = $EditorIcon::arrow_top;
+ Position = %valueX SPC 18;
Command = %this.getId() @ ".valueMoveUp();";
Tooltip = "Move Graph Up";
};
@@ -50,8 +60,8 @@
%this.valueMoveDownButton = new GuiButtonCtrl()
{
Class = "EditorIconButton";
- Frame = 6;
- Position = "2" SPC (getWord(%this.extent, 1) - 66);
+ Frame = $EditorIcon::arrow_bottom;
+ Position = %valueX SPC (getWord(%this.extent, 1) - 66);
Command = %this.getId() @ ".valueMoveDown();";
Tooltip = "Move Graph Down";
};
@@ -59,7 +69,6 @@
%this.add(%this.valueMoveDownButton);
//time zoom buttons
- %center = 18 + mRound(getWord(%this.graph.extent, 0));
%bottom = getWord(%this.extent, 1) - 38;
%this.timeZoomContainer = new GuiControl()
{
@@ -73,7 +82,7 @@
%this.timeZoomInButton = new GuiButtonCtrl()
{
Class = "EditorIconButton";
- Frame = 0;
+ Frame = $EditorIcon::sq_plus;
Position = "0 0";
Command = %this.getId() @ ".timeZoomIn();";
Tooltip = "Zoom In";
@@ -84,7 +93,7 @@
%this.timeZoomOutButton = new GuiButtonCtrl()
{
Class = "EditorIconButton";
- Frame = 1;
+ Frame = $EditorIcon::sq_minus;
Position = "26 0";
Command = %this.getId() @ ".timeZoomOut();";
Tooltip = "Zoom Out";
@@ -96,9 +105,9 @@
%this.timeMoveBackButton = new GuiButtonCtrl()
{
Class = "EditorIconButton";
- Frame = 8;
+ Frame = $EditorIcon::arrow_left;
HorizSizing = "right";
- Position = "30" SPC %bottom;
+ Position = %inset SPC %bottom;
Command = %this.getId() @ ".timeMoveBack();";
Tooltip = "Move Graph Back";
};
@@ -108,7 +117,7 @@
%this.timeMoveForwardButton = new GuiButtonCtrl()
{
Class = "EditorIconButton";
- Frame = 4;
+ Frame = $EditorIcon::arrow_right;
HorizSizing = "left";
Position = (getWord(%this.graph.extent, 0) + 6) SPC %bottom;
Command = %this.getId() @ ".timeMoveForward();";
@@ -116,6 +125,44 @@
};
ThemeManager.setProfile(%this.timeMoveForwardButton, "iconButtonProfile");
%this.add(%this.timeMoveForwardButton);
+
+ %this.addExtraControls();
+}
+
+// The graph this unit wraps. A subclass showing something other than one curve
+// answers with its own control and inherits every button above unchanged.
+function AssetParticleGraphUnit::createGraph(%this)
+{
+ return new GuiParticleGraphInspector();
+}
+
+// How much of the unit's left edge belongs to buttons rather than to the graph.
+function AssetParticleGraphUnit::getLeftInset(%this)
+{
+ return 30;
+}
+
+// Anything a subclass wants in the room its inset bought. Nothing, here.
+function AssetParticleGraphUnit::addExtraControls(%this)
+{
+}
+
+// A unit that has nothing to show is taken out of the grid rather than emptied,
+// so the cells that remain close up over it.
+function AssetParticleGraphUnit::attach(%this)
+{
+ if(!%this.Tool.isMember(%this))
+ {
+ %this.Tool.add(%this);
+ }
+}
+
+function AssetParticleGraphUnit::detach(%this)
+{
+ if(%this.Tool.isMember(%this))
+ {
+ %this.Tool.remove(%this);
+ }
}
function AssetParticleGraphUnit::setToScale(%this, %scaleName)
@@ -134,17 +181,11 @@
{
if(%variName $= "")
{
- if(%this.Tool.isMember(%this))
- {
- %this.Tool.remove(%this);
- }
+ %this.detach();
return;
}
- if(!%this.Tool.isMember(%this))
- {
- %this.Tool.add(%this);
- }
+ %this.attach();
%this.graph.setDisplayLabels("Time", "Variation");
%this.graph.setDisplayField(%variName, %emitterID);
}
@@ -153,17 +194,11 @@
{
if(%lifeName $= "")
{
- if(%this.Tool.isMember(%this))
- {
- %this.Tool.remove(%this);
- }
+ %this.detach();
return;
}
- if(!%this.Tool.isMember(%this))
- {
- %this.Tool.add(%this);
- }
+ %this.attach();
%this.graph.setDisplayLabels("Time", "Scale");
%this.graph.setDisplayField(%lifeName, %emitterID);
}
diff --git a/editor/AssetAdmin/ParticleEditor/AssetParticleTransportBar.cs b/editor/AssetAdmin/ParticleEditor/AssetParticleTransportBar.cs
new file mode 100644
index 000000000..8afd16b36
--- /dev/null
+++ b/editor/AssetAdmin/ParticleEditor/AssetParticleTransportBar.cs
@@ -0,0 +1,306 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2013 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+
+//-----------------------------------------------------------------------------
+// The bar over the particle preview: restart, play/pause, stop, a speed, and the
+// two switches that reduce the effect to the one emitter being tuned.
+//
+// The chrome is EditorTransportBar in EditorCore, shared with the animation
+// preview's bar; what is here is what these buttons do.
+//
+// Play and Pause are two buttons with one hidden rather than one toggle, for the
+// reason the animation bar gives: a toggle says "this setting is on", a transport
+// says "here is what will happen if you press me", and two buttons cannot get
+// stuck showing the wrong state because there is no state to get stuck -- only
+// whichever button is on show.
+//
+// SOLO AND EMITTER-OFF ARE NOT ASSET STATE. They are ParticlePlayer::setEmitterVisible
+// and setEmitterPaused on the preview's own player, so nothing they do can dirty
+// the effect or reach its file. That also means they do not survive: every edit
+// ends in refreshAsset, ParticlePlayer::onAssetRefreshed rebuilds every emitter
+// node, and the rebuilt nodes are all visible and running again. reapply() is
+// called from the panes' afterCommit for exactly that reason.
+//
+// Both act on the emitter the title dropdown has selected, which is the one whose
+// fields are on show -- so "solo" always means "the one I am looking at". On the
+// effect itself (index 0) there is no emitter to isolate and both stand down.
+//-----------------------------------------------------------------------------
+
+$AssetParticleTransportBar::speeds = "0.1 0.25 0.5 1 2";
+$AssetParticleTransportBar::defaultSpeedIndex = 3;
+
+function AssetParticleTransportBar::onAdd(%this)
+{
+ %this.init();
+
+ %this.addButton("restart", $EditorIcon::playback_rew, "Play the effect again from the beginning",
+ $EditorTransportBar::buttonSize);
+
+ // The one you reach for, so it is half again the size of the rest. They sit in
+ // the same place, and exactly one of them is ever visible.
+ %this.playButton = %this.addButton("play", $EditorIcon::playback_play, "Play the preview",
+ $EditorTransportBar::playSize);
+ %this.pauseButton = %this.addButton("pause", $EditorIcon::playback_pause, "Pause the preview",
+ $EditorTransportBar::playSize);
+ %this.pauseButton.setVisible(false);
+
+ %this.addButton("stop", $EditorIcon::playback_stop, "Stop the preview and clear its particles",
+ $EditorTransportBar::buttonSize);
+
+ %this.addSpacer($EditorTransportBar::gap);
+
+ // Not a toggle: five speeds cycled by pressing, with the current one in the
+ // tooltip. A slider would need a label to be readable and a label needs room
+ // the bar does not have over the art.
+ %this.speedIndex = $AssetParticleTransportBar::defaultSpeedIndex;
+ %this.speedButton = %this.addButton("cycleSpeed", $EditorIcon::stop_watch, "",
+ $EditorTransportBar::buttonSize);
+
+ %this.addSpacer($EditorTransportBar::gap);
+
+ %this.soloButton = %this.addToggle("Solo", $EditorIcon::eye, $EditorIcon::eye_inv,
+ "Showing this emitter only. Click to show them all again.",
+ "Showing every emitter. Click to show only the selected one.");
+
+ // on / off rather than the speaker pair this started with. Nothing here makes
+ // a sound, and a crossed-out speaker reads as "muted audio" however it is
+ // captioned -- what the button actually does is switch one emitter off.
+ %this.emitterOffButton = %this.addToggle("PauseEmitter", $EditorIcon::off, $EditorIcon::on,
+ "This emitter is switched off. Click to let it emit again.",
+ "This emitter is emitting. Click to switch just this one off.");
+}
+
+//-----------------------------------------------------------------------------
+// What the buttons do. All of it is on the preview's player, none of it on the
+// asset.
+//-----------------------------------------------------------------------------
+
+function AssetParticleTransportBar::player(%this)
+{
+ return isObject(AssetAdmin.previewPlayer) ? AssetAdmin.previewPlayer : "";
+}
+
+function AssetParticleTransportBar::play(%this)
+{
+ %player = %this.player();
+ if(!isObject(%player))
+ {
+ return;
+ }
+
+ // Paused and stopped are different states with one button between them: a
+ // paused effect resumes where it was, a stopped one has to be started again.
+ if(%player.getIsPlaying())
+ {
+ %player.setPaused(false);
+ }
+ else
+ {
+ %player.play(true);
+ }
+
+ %this.refresh();
+}
+
+function AssetParticleTransportBar::pause(%this)
+{
+ %player = %this.player();
+ if(isObject(%player))
+ {
+ %player.setPaused(true);
+ }
+
+ %this.refresh();
+}
+
+// stop(false, false): free the particles now, and do not kill the effect.
+//
+// Not stop(TRUE, false), the "let the particles finish" form, for two reasons.
+// It leaves mPlaying set until the last particle dies, so getIsPlaying goes on
+// answering true and the bar would offer Pause over an effect that had been
+// stopped -- and Pause on a stopped effect does nothing, so the button would lie
+// twice. It also pauses every emitter to do its waiting, which is the same flag
+// solo and emitter-off are written in, so a graceful stop would quietly undo them.
+//
+// Killing is the other thing this is not: that deletes the player and leaves the
+// preview with nothing in it and nothing to restart.
+function AssetParticleTransportBar::stop(%this)
+{
+ %player = %this.player();
+ if(isObject(%player))
+ {
+ %player.stop(false, false);
+ }
+
+ %this.refresh();
+}
+
+function AssetParticleTransportBar::restart(%this)
+{
+ %player = %this.player();
+ if(isObject(%player))
+ {
+ // True clears the particles already out, so what follows is the effect
+ // from nothing rather than the effect over its own tail.
+ %player.play(true);
+ %player.setPaused(false);
+ }
+
+ %this.refresh();
+}
+
+function AssetParticleTransportBar::cycleSpeed(%this)
+{
+ %count = getWordCount($AssetParticleTransportBar::speeds);
+ %this.speedIndex = (%this.speedIndex + 1) % %count;
+
+ %this.applySpeed();
+ %this.refresh();
+}
+
+function AssetParticleTransportBar::speed(%this)
+{
+ return getWord($AssetParticleTransportBar::speeds, %this.speedIndex);
+}
+
+function AssetParticleTransportBar::applySpeed(%this)
+{
+ %player = %this.player();
+ if(isObject(%player))
+ {
+ %player.setTimeScale(%this.speed());
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Solo and emitter-off, on the emitter the dropdown has selected.
+//-----------------------------------------------------------------------------
+
+function AssetParticleTransportBar::onToggleIconChanged(%this, %button)
+{
+ switch$(%button.toggleName)
+ {
+ case "Solo":
+ %this.soloOn = %button.getValue();
+
+ case "PauseEmitter":
+ %this.emitterOff = %button.getValue();
+ }
+
+ %this.reapply();
+}
+
+// The selected emitter's index, or -1 when the dropdown is on the effect itself.
+function AssetParticleTransportBar::selectedIndex(%this)
+{
+ if(!isObject(AssetAdmin.inspector) || !AssetAdmin.inspector.titleDropDown.isVisible())
+ {
+ return -1;
+ }
+
+ return AssetAdmin.inspector.titleDropDown.getSelectedItem() - 1;
+}
+
+// Push the whole visible/paused state onto the player. Written as "say it for
+// every emitter" rather than "change the one that moved", because the player's
+// emitter nodes are rebuilt from the asset on every edit and arrive visible and
+// running -- so there is nothing to change, only something to say again.
+function AssetParticleTransportBar::reapply(%this)
+{
+ %player = %this.player();
+ %asset = isObject(AssetAdmin.inspector) ? AssetAdmin.inspector.documentAsset() : "";
+
+ if(!isObject(%player) || !isObject(%asset))
+ {
+ return;
+ }
+
+ %selected = %this.selectedIndex();
+ %count = %asset.getEmitterCount();
+
+ for(%i = 0; %i < %count; %i++)
+ {
+ %isSelected = (%i == %selected);
+
+ // Solo hides everything except the selected one. With nothing selected
+ // there is nothing to solo, so everything stays visible.
+ %visible = !%this.soloOn || %selected < 0 || %isSelected;
+
+ // Switching off pauses only the selected one -- the opposite shape to solo, and the
+ // pair is what lets you hear one emitter or everything but it.
+ %paused = %this.emitterOff && %isSelected;
+
+ %player.setEmitterVisible(%visible, %i);
+ %player.setEmitterPaused(%paused, %i);
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Reading the state back out. Called whenever something else may have moved it:
+// a new selection, an edit that rebuilt the player, or one of the buttons above.
+//-----------------------------------------------------------------------------
+
+function AssetParticleTransportBar::refresh(%this)
+{
+ %player = %this.player();
+
+ %running = isObject(%player) && %player.getIsPlaying() && !%player.getPaused();
+ %this.playButton.setVisible(!%running);
+ %this.pauseButton.setVisible(%running);
+ %this.relayout();
+
+ %this.speedButton.Tooltip = "Preview speed:" SPC %this.speed() @ "x. Click for the next one.";
+
+ // Neither switch means anything while the effect itself is selected, and a
+ // switch left on from the last emitter would be a lie about this one.
+ %onEmitter = %this.selectedIndex() >= 0;
+ %this.soloButton.setActive(%onEmitter);
+ %this.emitterOffButton.setActive(%onEmitter);
+
+ %this.applySpeed();
+ %this.reapply();
+}
+
+// A new preview player was built, for the asset named here.
+//
+// The switches are kept when it is the same effect and cleared when it is not,
+// which is the distinction that makes solo usable at all: EVERY edit rebuilds the
+// preview -- the commit ends in refreshAsset and AssetAdmin::refreshPreview
+// re-clicks the tile -- so a bar that reset on every rebuild would drop the solo
+// the moment you changed the field you were soloing to look at.
+//
+// The speed is not part of that: it is how you are watching rather than what you
+// are watching, so it carries across assets like a preference.
+function AssetParticleTransportBar::onPreviewRebuilt(%this, %assetId)
+{
+ if(%this.assetId !$= %assetId)
+ {
+ %this.assetId = %assetId;
+ %this.soloOn = false;
+ %this.emitterOff = false;
+ }
+
+ %this.soloButton.setValue(%this.soloOn);
+ %this.emitterOffButton.setValue(%this.emitterOff);
+
+ %this.refresh();
+}
diff --git a/editor/AssetAdmin/ParticleEditor/ParticleGraphCameraController.cs b/editor/AssetAdmin/ParticleEditor/ParticleGraphCameraController.cs
index 868e44ffd..c9136eb6a 100644
--- a/editor/AssetAdmin/ParticleEditor/ParticleGraphCameraController.cs
+++ b/editor/AssetAdmin/ParticleEditor/ParticleGraphCameraController.cs
@@ -60,6 +60,28 @@
{
%this.setupDegreeValue();
}
+
+ // A field that never leaves 0-1 got exactly one zoom level out of the chain
+ // above, which left all four buttons dead on every color channel and on alpha.
+ if(%this.max <= 1)
+ {
+ %this.setupUnitValue();
+ }
+}
+
+// The zoom levels are window WIDTHS, so index 1 is the tightest view and the last
+// is the whole field. That is what makes zooming out unable to go past 0-1: there
+// is no wider window to ask for.
+function ParticleGraphCameraController::setupUnitValue(%this)
+{
+ %this.currentPosition = %this.min;
+
+ %this.zoomLevel[1] = 0.1;
+ %this.zoomLevel[2] = 0.25;
+ %this.zoomLevel[3] = 0.5;
+ %this.zoomLevel[4] = 1;
+ %this.zoomCount = 4;
+ %this.currentZoomLevel = 4;
}
function ParticleGraphCameraController::setupDegreeValue(%this)
diff --git a/editor/AssetAdmin/ParticleEditor/exec.cs b/editor/AssetAdmin/ParticleEditor/exec.cs
index 412844eb8..e09346f5b 100644
--- a/editor/AssetAdmin/ParticleEditor/exec.cs
+++ b/editor/AssetAdmin/ParticleEditor/exec.cs
@@ -1,4 +1,7 @@
exec("./AssetParticleGraphTool.cs");
exec("./AssetParticleGraphUnit.cs");
+exec("./AssetParticleColorGraphUnit.cs");
+exec("./AssetParticleChannelToggle.cs");
exec("./ParticleGraphCameraController.cs");
exec("./NewParticleEmitterDialog.cs");
+exec("./AssetParticleTransportBar.cs");
diff --git a/editor/EditorCore/EditorAssetPickerDialog.cs b/editor/EditorCore/EditorAssetPickerDialog.cs
new file mode 100644
index 000000000..3533fd5c5
--- /dev/null
+++ b/editor/EditorCore/EditorAssetPickerDialog.cs
@@ -0,0 +1,373 @@
+
+//-----------------------------------------------------------------------------
+// A modal picker for choosing an asset by looking at it rather than by typing
+// its id from memory. Opened by EditorCore.openAssetPicker; used by the Gui
+// Profile Editor's Image Asset row and by the native inspector's browse button,
+// which is why it lives in EditorCore rather than in either of them.
+//
+// The spawner sets assetType, currentAsset, callbackTarget and callbackMethod.
+// Choosing calls callbackTarget.callbackMethod(assetId); cancelling calls
+// nothing at all, so a caller never has to distinguish "picked nothing" from
+// "changed my mind".
+//
+// The asset database is queried exactly once, when the dialog opens. Filtering
+// after that is pure setVisible over the buttons already built, so typing costs
+// a walk over the grid and no database work -- which also means the list cannot
+// shift under a search the way a re-query would.
+//-----------------------------------------------------------------------------
+
+function EditorAssetPickerDialog::init(%this, %width, %height)
+{
+ %window = %this.getObject(0);
+ %content = %window.getObject(0);
+
+ %pad = 10;
+ %rowHeight = 30;
+ %searchY = 10;
+ %gridTop = %searchY + %rowHeight + 8;
+ %countWidth = 110;
+ %captionWidth = 64;
+
+ // Everything below the grid is measured back from the bottom edge so the
+ // grid absorbs the slack when the window is resized -- which is worth doing
+ // here, unlike the other editor dialogs, because a wider picker shows more
+ // columns of art.
+ %detailY = %height - 96;
+ %gridHeight = (%detailY - 8) - %gridTop;
+
+ %searchLabel = new GuiControl()
+ {
+ Position = %pad SPC %searchY;
+ Extent = %captionWidth SPC %rowHeight;
+ Text = "Search";
+ align = "left";
+ vAlign = "middle";
+ };
+ ThemeManager.setProfile(%searchLabel, "labelProfile");
+ %content.add(%searchLabel);
+
+ // Command fires on every keystroke, which is what makes the list narrow as
+ // you type; AltCommand would only fire when the box lost focus.
+ %this.searchBox = new GuiTextEditCtrl()
+ {
+ HorizSizing = "width";
+ Position = (%pad + %captionWidth + 6) SPC %searchY;
+ Extent = (%width - (%pad * 2) - %captionWidth - 6 - %countWidth - 8) SPC %rowHeight;
+ align = "left";
+ };
+ ThemeManager.setProfile(%this.searchBox, "textEditProfile");
+ %this.searchBox.Command = %this.getID() @ ".onSearchChanged();";
+ %this.searchBox.ReturnCommand = %this.getID() @ ".onDone();";
+ %this.searchBox.EscapeCommand = %this.getID() @ ".onClose();";
+ %content.add(%this.searchBox);
+
+ %this.countLabel = new GuiControl()
+ {
+ HorizSizing = "left";
+ Position = (%width - %pad - %countWidth) SPC %searchY;
+ Extent = %countWidth SPC %rowHeight;
+ Text = "";
+ align = "right";
+ vAlign = "middle";
+ };
+ ThemeManager.setProfile(%this.countLabel, "labelProfile");
+ %content.add(%this.countLabel);
+
+ %this.scroller = new GuiScrollCtrl()
+ {
+ HorizSizing = "width";
+ VertSizing = "height";
+ Position = %pad SPC %gridTop;
+ Extent = (%width - (%pad * 2)) SPC %gridHeight;
+ hScrollBar = "alwaysOff";
+ vScrollBar = "dynamic";
+ constantThumbHeight = false;
+ showArrowButtons = false;
+ scrollBarThickness = 16;
+ };
+ ThemeManager.setProfile(%this.scroller, "scrollingPanelProfile");
+ ThemeManager.setProfile(%this.scroller, "scrollingPanelThumbProfile", "ThumbProfile");
+ ThemeManager.setProfile(%this.scroller, "scrollingPanelTrackProfile", "TrackProfile");
+ ThemeManager.setProfile(%this.scroller, "scrollingPanelArrowProfile", "ArrowProfile");
+ %content.add(%this.scroller);
+
+ // IsExtentDynamic is what lets the grid grow taller than the scroller and so
+ // gives the scroll bar something to scroll; without it the grid stays the
+ // height it was built at and the rows past the first screenful are unreachable.
+ %this.grid = new GuiGridCtrl()
+ {
+ HorizSizing = "width";
+ Position = "0 0";
+ Extent = (%width - (%pad * 2) - 16) SPC 60;
+ CellSizeX = 60;
+ CellSizeY = 60;
+ CellModeX = "variable";
+ CellModeY = "absolute";
+ CellSpacingX = 4;
+ CellSpacingY = 4;
+ MaxColCount = 0;
+ MaxRowCount = 0;
+ OrderMode = "lrtb";
+ IsExtentDynamic = true;
+ };
+ ThemeManager.setProfile(%this.grid, "emptyProfile");
+ %this.scroller.add(%this.grid);
+
+ %this.detailLabel = new GuiControl()
+ {
+ HorizSizing = "width";
+ VertSizing = "top";
+ Position = %pad SPC %detailY;
+ Extent = (%width - (%pad * 2)) SPC 22;
+ Text = "";
+ align = "left";
+ vAlign = "middle";
+ };
+ // labelProfile rather than infoProfile: this is a status line, and infoProfile
+ // draws a border and fill, which reads as an empty text box while nothing is
+ // selected.
+ ThemeManager.setProfile(%this.detailLabel, "labelProfile");
+ %content.add(%this.detailLabel);
+
+ // "left"/"top" sizing pins these to the bottom-right corner. The names read
+ // backwards: they name the edge whose slack absorbs the resize, not the edge
+ // the control sticks to.
+ %this.cancelButton = new GuiButtonCtrl()
+ {
+ HorizSizing = "left";
+ VertSizing = "top";
+ Position = (%width - 226) SPC (%height - 62);
+ Extent = "100 30";
+ Text = "Cancel";
+ Command = %this.getID() @ ".onClose();";
+ };
+ ThemeManager.setProfile(%this.cancelButton, "buttonProfile");
+ %content.add(%this.cancelButton);
+
+ %this.chooseButton = new GuiButtonCtrl()
+ {
+ HorizSizing = "left";
+ VertSizing = "top";
+ Position = (%width - 116) SPC (%height - 64);
+ Extent = "100 34";
+ Text = "Choose";
+ Command = %this.getID() @ ".onDone();";
+ };
+ ThemeManager.setProfile(%this.chooseButton, "primaryButtonProfile");
+ %content.add(%this.chooseButton);
+ %this.chooseButton.setActive(false);
+
+ %this.load();
+}
+
+//-----------------------------------------------------------------------------
+// Populating.
+//-----------------------------------------------------------------------------
+
+// One query, one button per asset, and that is the last the database hears from
+// this dialog. findAssetType filters the query it is handed rather than
+// searching afresh, which is what the trailing true means.
+function EditorAssetPickerDialog::load(%this)
+{
+ %query = new AssetQuery();
+ AssetDatabase.findAllAssets(%query);
+ AssetDatabase.findAssetType(%query, %this.assetType, true);
+
+ for(%i = 0; %i < %query.getCount(); %i++)
+ {
+ %assetId = %query.getAsset(%i);
+
+ // Internal assets are the engine's own bookkeeping and belong to nobody
+ // the user could sensibly point a profile at.
+ if(!AssetDatabase.isAssetInternal(%assetId))
+ {
+ %this.addItem(%assetId);
+ }
+ }
+ %query.delete();
+
+ %this.totalCount = %this.grid.getCount();
+ %this.applyFilter();
+
+ // Open showing what the field already holds, so reopening the picker reads
+ // as coming back to a choice rather than starting over.
+ %current = %this.findItem(%this.currentAsset);
+ if(isObject(%current))
+ {
+ %this.onItemClicked(%current);
+ }
+}
+
+function EditorAssetPickerDialog::addItem(%this, %assetId)
+{
+ %item = new GuiButtonCtrl()
+ {
+ class = "EditorAssetPickerItem";
+ assetId = %assetId;
+ assetType = %this.assetType;
+ owner = %this;
+ };
+ %this.grid.add(%item);
+
+ return %item;
+}
+
+function EditorAssetPickerDialog::findItem(%this, %assetId)
+{
+ if(%assetId $= "")
+ {
+ return 0;
+ }
+
+ for(%i = 0; %i < %this.grid.getCount(); %i++)
+ {
+ %item = %this.grid.getObject(%i);
+ if(%item.assetId $= %assetId)
+ {
+ return %item;
+ }
+ }
+ return 0;
+}
+
+//-----------------------------------------------------------------------------
+// Filtering.
+//-----------------------------------------------------------------------------
+
+function EditorAssetPickerDialog::onSearchChanged(%this)
+{
+ %this.applyFilter();
+}
+
+// Matching is a substring of the whole asset id, so "app" finds a module's
+// worth of assets and "rock" finds them across modules. Deliberately not
+// AssetDatabase.findAssetName: that one takes no query-as-source argument, so
+// it always re-searches the entire database and would throw away the type
+// filter this dialog was opened with.
+function EditorAssetPickerDialog::applyFilter(%this)
+{
+ %needle = strlwr(trim(%this.searchBox.getText()));
+ %shown = 0;
+
+ for(%i = 0; %i < %this.grid.getCount(); %i++)
+ {
+ %item = %this.grid.getObject(%i);
+ %match = (%needle $= "") || (strstr(%item.searchKey, %needle) != -1);
+ %item.setVisible(%match);
+
+ if(%match)
+ {
+ %shown++;
+ }
+ }
+
+ %this.countLabel.setText(%shown SPC "of" SPC %this.totalCount);
+ %this.reflowGrid();
+}
+
+// A grid re-lays out when a child is added, removed, moved or resized, and
+// setVisible is none of those -- so without this the hidden cells leave their
+// holes behind and the grid keeps its old height. Resizing it to the size it
+// already has walks the children again, and the walk skips the invisible ones.
+function EditorAssetPickerDialog::reflowGrid(%this)
+{
+ %position = %this.grid.getPosition();
+ %extent = %this.grid.getExtent();
+ %this.grid.resize(getWord(%position, 0), getWord(%position, 1),
+ getWord(%extent, 0), getWord(%extent, 1));
+}
+
+//-----------------------------------------------------------------------------
+// Selection.
+//-----------------------------------------------------------------------------
+
+function EditorAssetPickerDialog::onItemClicked(%this, %item)
+{
+ if(%this.chosenItem == %item)
+ {
+ return;
+ }
+
+ if(isObject(%this.chosenItem))
+ {
+ %this.chosenItem.setChosen(false);
+ }
+ %item.setChosen(true);
+
+ %this.chosenItem = %item;
+ %this.selectedAsset = %item.assetId;
+ %this.chooseButton.setActive(true);
+ %this.showDetail(%item.assetId);
+}
+
+// Only the selected asset is ever acquired, and the one before it is let go
+// first, so the dialog holds a single reference however much clicking around
+// happens. onRemove drops the last one.
+function EditorAssetPickerDialog::showDetail(%this, %assetId)
+{
+ %this.releaseHeldAsset();
+
+ %asset = AssetDatabase.acquireAsset(%assetId);
+ %this.heldAsset = %assetId;
+
+ %detail = %assetId;
+ if(isObject(%asset) && %this.assetType $= "ImageAsset")
+ {
+ %frames = %asset.getFrameCount();
+ %detail = %detail SPC "-" SPC %frames SPC (%frames == 1 ? "frame," : "frames,") SPC
+ %asset.getImageWidth() SPC "x" SPC %asset.getImageHeight();
+ }
+ %this.detailLabel.setText(%detail);
+}
+
+function EditorAssetPickerDialog::releaseHeldAsset(%this)
+{
+ if(%this.heldAsset !$= "")
+ {
+ AssetDatabase.releaseAsset(%this.heldAsset);
+ %this.heldAsset = "";
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Leaving.
+//-----------------------------------------------------------------------------
+
+// Pop before calling back. The caller's handler commits a field, which can
+// rebuild the pane the button that started all this lives in, and none of that
+// should run while this dialog is still on the canvas. The dialog itself
+// survives either way -- the delete is a hundred milliseconds out.
+function EditorAssetPickerDialog::onDone(%this)
+{
+ if(%this.selectedAsset $= "")
+ {
+ return;
+ }
+
+ %target = %this.callbackTarget;
+ %method = %this.callbackMethod;
+ %asset = %this.selectedAsset;
+
+ %this.onClose();
+
+ if(isObject(%target))
+ {
+ %target.call(%method, %asset);
+ }
+}
+
+// This dialog opens on top of another one, so it must not use the shared
+// EditorCore.dialog delete slot - closing both within the scheduled delay would
+// leak one of them. The parent object deletes it after a pause; scheduling
+// "delete" on the dialog itself would fire inside its own script-callback guard
+// and assert.
+function EditorAssetPickerDialog::onClose(%this)
+{
+ Canvas.popDialog(%this);
+ EditorCore.schedule(100, "deleteDialogObject", %this);
+}
+
+function EditorAssetPickerDialog::onRemove(%this)
+{
+ %this.releaseHeldAsset();
+}
diff --git a/editor/EditorCore/EditorAssetPickerItem.cs b/editor/EditorCore/EditorAssetPickerItem.cs
new file mode 100644
index 000000000..75a6a1b2f
--- /dev/null
+++ b/editor/EditorCore/EditorAssetPickerItem.cs
@@ -0,0 +1,80 @@
+
+//-----------------------------------------------------------------------------
+// One cell in the asset picker's grid: a selectable button wearing a thumbnail
+// of the asset it stands for.
+//
+// The spawner sets assetId, assetType and owner. A click goes straight back out
+// as owner.onItemClicked(%this) -- the item never learns what being chosen
+// means, only which asset it is, so the dialog stays the single place that
+// knows about selection, detail text and the callback.
+//
+// searchKey is the lowercased asset id, worked out once here rather than on
+// every keystroke, because filtering walks every button in the grid.
+//-----------------------------------------------------------------------------
+
+function EditorAssetPickerItem::onAdd(%this)
+{
+ // A cell looks the same wherever it is, so it dresses itself: the grid sizes
+ // it anyway, and the spawner is left setting only what it actually decides --
+ // which asset this is and who to tell about a click.
+ %this.HorizSizing = "center";
+ %this.VertSizing = "center";
+ %this.setExtent(56, 56);
+ %this.setText("");
+ %this.setChosen(false);
+ ThemeManager.setProfile(%this, "tipProfile", "TooltipProfile");
+
+ %this.searchKey = strlwr(%this.assetId);
+ %this.Tooltip = %this.assetId;
+
+ // Only the picture types have a picture to show. The rest name themselves,
+ // which reads better than a grid of identical placeholder icons -- and it
+ // keeps EditorCore off AssetAdmin's icon sheet, which it must not depend on.
+ if(%this.assetType $= "ImageAsset")
+ {
+ %thumbnail = %this.buildThumbnail();
+ %thumbnail.setImage(%this.assetId);
+ }
+ else if(%this.assetType $= "AnimationAsset")
+ {
+ %thumbnail = %this.buildThumbnail();
+ %thumbnail.setAnimation(%this.assetId);
+ }
+ else
+ {
+ %this.setText(getUnit(%this.assetId, 1, ":"));
+ }
+}
+
+// Centered and proportional so a wide image and a tall one both sit square in
+// the cell. UseInput is off: without it the sprite eats the click meant for the
+// button underneath it.
+function EditorAssetPickerItem::buildThumbnail(%this)
+{
+ %thumbnail = new GuiSpriteCtrl()
+ {
+ HorizSizing = "center";
+ VertSizing = "center";
+ Position = "0 0";
+ Extent = "50 50";
+ minExtent = "50 50";
+ constrainProportions = true;
+ fullSize = true;
+ UseInput = false;
+ };
+ ThemeManager.setProfile(%thumbnail, "spriteProfile");
+ %this.add(%thumbnail);
+ return %thumbnail;
+}
+
+function EditorAssetPickerItem::onClick(%this)
+{
+ %this.owner.onItemClicked(%this);
+}
+
+// Selection is shown by swapping profiles rather than by drawing anything, so a
+// theme decides what a chosen cell looks like.
+function EditorAssetPickerItem::setChosen(%this, %chosen)
+{
+ ThemeManager.setProfile(%this, %chosen ? "itemSelectedProfile" : "itemSelectProfile");
+}
diff --git a/editor/EditorCore/EditorButtonBar.cs b/editor/EditorCore/EditorButtonBar.cs
index 750eef8b0..a8f2fe5bb 100644
--- a/editor/EditorCore/EditorButtonBar.cs
+++ b/editor/EditorCore/EditorButtonBar.cs
@@ -1,5 +1,10 @@
-function EditorButtonBar::addButton(%this, %click, %frame, %tooltip, %enabled)
+// %tooltipFunction is optional and names a method on the tool that answers the
+// tip text. It exists for a button that does the same job to more than one kind
+// of thing: a "new in this category" button whose tip says "Profile" while a
+// cursor is selected is describing the wrong thing. Omit it and %tooltip is
+// simply fixed, as it is for every other button.
+function EditorButtonBar::addButton(%this, %click, %frame, %tooltip, %enabled, %tooltipFunction)
{
if(!isObject(%this.tool))
{
@@ -15,6 +20,7 @@
Command = %this.tool.getId() @ "." @ %click @ "();";
Tooltip = %tooltip;
EnabledFunction = %enabled;
+ TooltipFunction = %tooltipFunction;
};
ThemeManager.setProfile(%button, "iconButtonProfile");
%this.add(%button);
@@ -23,6 +29,8 @@
{
%button.setActive(%this.tool.call(%enabled));
}
+
+ return %button;
}
function EditorButtonBar::refreshEnabled(%this)
@@ -34,6 +42,10 @@
{
%button.setActive(%this.tool.call(%button.EnabledFunction));
}
+ if(%button.TooltipFunction !$= "")
+ {
+ %button.Tooltip = %this.tool.call(%button.TooltipFunction);
+ }
}
}
diff --git a/editor/EditorCore/EditorChoiceRow.cs b/editor/EditorCore/EditorChoiceRow.cs
new file mode 100644
index 000000000..4a9331752
--- /dev/null
+++ b/editor/EditorCore/EditorChoiceRow.cs
@@ -0,0 +1,157 @@
+
+//-----------------------------------------------------------------------------
+// A row of icon buttons where exactly one is chosen: a radio group that looks
+// like a segmented control. Built for the properties pane's two alignment rows,
+// where a drop-down was a poor fit -- there are only three or four values, they
+// each have a picture, and seeing which one is set matters more than reading
+// its name.
+//
+// The buttons are EditorToggleIcon, the same checkbox-as-button the header
+// and the anchor picker use, so a choice looks pressed and a disabled row
+// refuses to act. Exclusivity is this row's job rather than the button's: a
+// click turns the others off, and clicking the chosen one again does nothing --
+// unlike a toggle, a radio cannot be un-picked, only replaced.
+//
+// The first entry is conventionally the "unset" one. For alignment that is
+// GuiControl's "default", which getAlignmentType resolves to the profile's own
+// alignment -- so it is a real value meaning "inherit", not an absence, and it
+// gets a button with no icon rather than no button.
+//
+// The creator sets owner, fieldName and blockWidth inline, calls addChoice()
+// once per value, then build(). Changes arrive at
+// owner.onChoiceRowChanged(%row).
+//-----------------------------------------------------------------------------
+
+function EditorChoiceRow::onAdd(%this)
+{
+ ThemeManager.setProfile(%this, "emptyProfile");
+ %this.choiceCount = 0;
+ %this.value = "";
+}
+
+// %icon may be "" for the entry that means "unset" -- EditorToggleIcon draws
+// nothing when its frame is empty, leaving a plain button.
+function EditorChoiceRow::addChoice(%this, %value, %icon, %tip)
+{
+ %i = %this.choiceCount;
+ %this.choiceValue[%i] = %value;
+ %this.choiceIcon[%i] = %icon;
+ %this.choiceTip[%i] = %tip;
+ %this.choiceCount = %i + 1;
+}
+
+function EditorChoiceRow::build(%this)
+{
+ // Wide enough for a caption by default. The text block asks for a narrow one:
+ // its two rows sit side by side under the text box and are labelled "H:" and
+ // "V:", where the icons say the rest.
+ %labelW = (%this.labelWidth $= "") ? 68 : %this.labelWidth;
+ %size = 24;
+ %gap = 2;
+
+ %this.setExtent(%labelW + ((%size + %gap) * %this.choiceCount), %size + 4);
+
+ %this.label = new GuiControl()
+ {
+ Position = "0 2";
+ Extent = (%labelW - 4) SPC %size;
+ Text = %this.labelText;
+ align = "left";
+ vAlign = "middle";
+ };
+ ThemeManager.setProfile(%this.label, "labelProfile");
+ %this.add(%this.label);
+
+ for(%i = 0; %i < %this.choiceCount; %i++)
+ {
+ %button = new GuiCheckBoxCtrl()
+ {
+ class = "EditorToggleIcon";
+ Position = (%labelW + (%i * (%size + %gap))) SPC 2;
+ Extent = %size SPC %size;
+ frameOn = %this.choiceIcon[%i];
+ frameOff = %this.choiceIcon[%i];
+ tipOn = %this.choiceTip[%i];
+ tipOff = %this.choiceTip[%i];
+ toggleName = %this.choiceValue[%i];
+ owner = %this;
+ };
+ ThemeManager.setProfile(%button, "iconButtonProfile");
+ ThemeManager.setProfile(%button, "tipProfile", "TooltipProfile");
+ %this.add(%button);
+ %this.choiceButton[%i] = %button;
+ }
+}
+
+// Hide a choice that does not apply to whatever is bound. The buttons are placed
+// absolutely, so this leaves a gap where the choice was rather than reflowing the
+// row -- which is the right trade for a choice that comes and goes with the
+// selection: the ones that stay do not move under the cursor.
+function EditorChoiceRow::setChoiceVisible(%this, %value, %visible)
+{
+ for(%i = 0; %i < %this.choiceCount; %i++)
+ {
+ if(%this.choiceValue[%i] $= %value)
+ {
+ %this.choiceButton[%i].setVisible(%visible);
+ return;
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Value.
+//-----------------------------------------------------------------------------
+
+// Load a value without telling the owner. A value the row does not offer leaves
+// every button up rather than guessing, so nothing is silently rewritten.
+function EditorChoiceRow::setValue(%this, %value)
+{
+ %this.value = %value;
+ %this.populating = true;
+ for(%i = 0; %i < %this.choiceCount; %i++)
+ {
+ %this.choiceButton[%i].setValue(%this.choiceValue[%i] $= %value);
+ }
+ %this.populating = false;
+}
+
+function EditorChoiceRow::getValue(%this)
+{
+ return %this.value;
+}
+
+// A button toggled itself. Whatever it did to its own state, the row's rule is
+// that exactly one is on -- so re-assert that from the value rather than from
+// what the button happens to hold.
+function EditorChoiceRow::onToggleIconChanged(%this, %button)
+{
+ if(%this.populating)
+ {
+ return;
+ }
+
+ %chosen = %button.toggleName;
+ if(%chosen $= %this.value)
+ {
+ // Clicking the current choice cannot clear it; put the button back down.
+ %this.setValue(%chosen);
+ return;
+ }
+
+ %this.setValue(%chosen);
+
+ if(isObject(%this.owner))
+ {
+ %this.owner.onChoiceRowChanged(%this);
+ }
+}
+
+function EditorChoiceRow::setEnabled(%this, %enabled)
+{
+ %this.label.setActive(%enabled);
+ for(%i = 0; %i < %this.choiceCount; %i++)
+ {
+ %this.choiceButton[%i].setActive(%enabled);
+ }
+}
diff --git a/editor/EditorCore/EditorCore.cs b/editor/EditorCore/EditorCore.cs
index bb38e93bc..80433924c 100644
--- a/editor/EditorCore/EditorCore.cs
+++ b/editor/EditorCore/EditorCore.cs
@@ -22,6 +22,10 @@
function EditorCore::create( %this )
{
+ // First, and before any editor builds a control: every icon in every editor
+ // is a frame index into the shared sheets, and these are the names for them.
+ exec("./EditorIcons.cs");
+
%this.editorKeyMap = new ActionMap();
if(!isObject(AppCore))
{
@@ -48,8 +52,42 @@
exec("./EditorIconButton.cs");
exec("./EditorButtonBar.cs");
+ // Before initGui builds the bar, and before any editor's create runs to hang
+ // its own menus off it.
+ exec("./EditorMenu.cs");
+ exec("./EditorMenuSet.cs");
+
+ // The segmented toggle row. It lives here rather than in the Gui Editor that
+ // first grew it because editor/main.cs loads AssetAdmin FIRST, so anything the
+ // Asset Manager builds at create time cannot come out of a module loaded after
+ // it. Nothing in either file was ever Gui-Editor specific.
+ exec("./EditorToggleIcon.cs");
+ exec("./EditorChoiceRow.cs");
+
+ // The chrome a preview's transport bar is made of. Both the Asset Manager's
+ // bars build on it, and it uses EditorToggleIcon and EditorIconButton above.
+ exec("./EditorTransportBar.cs");
+
+ // The field cell, here for the same reason: the Gui Editor grew it, and the
+ // Asset Manager's inspector panes are built at create time from a module that
+ // loads before the Gui Editor does.
+ exec("./EditorFieldRow.cs");
+
+ exec("./EditorAssetPickerDialog.cs");
+ exec("./EditorAssetPickerItem.cs");
+ exec("./EditorPreferences.cs");
+
+ // Out here rather than with NewProjectDialog above, because the Project
+ // Manager renames modules too and it is loaded once a project is open.
+ exec("./ModuleStamper.cs");
+
new ScriptObject(ThemeManager);
+ // Before any editor builds a control that wants to remember how it was left.
+ new ScriptObject(EditorPreferences);
+
+ new ScriptObject(ModuleStamper);
+
%this.initGui();
%this.editorKeyMap.push();
}
@@ -58,6 +96,22 @@
function EditorCore::destroy( %this )
{
+ if(isObject(EditorPreferences))
+ {
+ EditorPreferences.delete();
+ }
+
+ if(isObject(ModuleStamper))
+ {
+ ModuleStamper.delete();
+ }
+
+ // Empty by now - every editor deletes its own menus before this runs, and
+ // each module is unloaded before the one it depends on.
+ if(isObject(%this.menuPark))
+ {
+ %this.menuPark.delete();
+ }
}
function EditorCore::initGui(%this)
@@ -81,191 +135,25 @@
Command = "EditorCore.close();";
};
+ // Both go through the guard chain, because both throw away work an
+ // editor is holding and neither is undoable. The chain names each
+ // editor in turn and tests it with isObject, which is what keeps
+ // quitting working if a module ever fails to load.
+ //
+ // These two and Close Tools are the only commands written here. Every
+ // other menu belongs to whichever editor is open and arrives with it -
+ // see setEditorMenus.
+ //
+ // The window's own X cannot be guarded this way. It posts the quit
+ // from the window procedure with no script in between.
new GuiMenuItemCtrl() {
Text = "Close Project";
- Command = "restartInstance();";
+ Command = "EditorCore.guardedCommand(\"restartInstance();\");";
};
new GuiMenuItemCtrl() {
Text = "Exit";
- Command = "quit();";
- };
- };
- new GuiMenuItemCtrl() {
- Text = "File";
- Active = "0";
-
- new GuiMenuItemCtrl() {
- Text = "New Gui";
- Command = "GuiEditor.NewGui();";
- Accelerator = "Ctrl N";
- };
- new GuiMenuItemCtrl() {
- Text = "Open Gui...";
- Command = "GuiEditor.OpenGui();";
- Accelerator = "Ctrl O";
- };
- new GuiMenuItemCtrl() { Text = "-"; };
- new GuiMenuItemCtrl() {
- Text = "Save Gui...";
- Command = "GuiEditor.SaveGui();";
- Accelerator = "Ctrl S";
- };
- new GuiMenuItemCtrl() {
- Text = "Save Gui As...";
- Command = "GuiEditor.SaveGuiAs();";
- Accelerator = "Ctrl-Shift S";
- };
- };
- new GuiMenuItemCtrl() {
- Text = "Edit";
- Active = "0";
-
- new GuiMenuItemCtrl() {
- Text = "Undo";
- Command = "GuiEditor.Undo();";
- Accelerator = "Ctrl Z";
- };
- new GuiMenuItemCtrl() {
- Text = "Redo";
- Command = "GuiEditor.Redo();";
- Accelerator = "Ctrl-Shift Z";
- };
- new GuiMenuItemCtrl() { Text = "-"; };
- new GuiMenuItemCtrl() {
- Text = "Cut";
- Command = "GuiEditor.Cut();";
- Accelerator = "Ctrl X";
- };
- new GuiMenuItemCtrl() {
- Text = "Copy";
- Command = "GuiEditor.Copy();";
- Accelerator = "Ctrl C";
- };
- new GuiMenuItemCtrl() {
- Text = "Paste";
- Command = "GuiEditor.Paste();";
- Accelerator = "Ctrl V";
- };
- };
- new GuiMenuItemCtrl() {
- Text = "Layout";
- Active = "0";
-
- new GuiMenuItemCtrl() {
- Text = "Nudge Up";
- Command = "GuiEditor.brain.moveSelection(0,-1);";
- Accelerator = "Up";
- };
- new GuiMenuItemCtrl() {
- Text = "Nudge Down";
- Command = "GuiEditor.brain.moveSelection(0,1);";
- Accelerator = "Down";
- };
- new GuiMenuItemCtrl() {
- Text = "Nudge Left";
- Command = "GuiEditor.brain.moveSelection(-1,0);";
- Accelerator = "Left";
- };
- new GuiMenuItemCtrl() {
- Text = "Nudge Right";
- Command = "GuiEditor.brain.moveSelection(1,0);";
- Accelerator = "Right";
- };
- new GuiMenuItemCtrl() { Text = "-"; };
- new GuiMenuItemCtrl() {
- Text = "Shrink Height";
- Command = "GuiEditor.changeExtent(0,-1);";
- Accelerator = "Ctrl Up";
- };
- new GuiMenuItemCtrl() {
- Text = "Expand Height";
- Command = "GuiEditor.changeExtent(0, 1);";
- Accelerator = "Ctrl Down";
- };
- new GuiMenuItemCtrl() {
- Text = "Shrink Width";
- Command = "GuiEditor.changeExtent(-1,0);";
- Accelerator = "Ctrl Left";
- };
- new GuiMenuItemCtrl() {
- Text = "Expand Width";
- Command = "GuiEditor.changeExtent(1,0);";
- Accelerator = "Ctrl Right";
- };
- new GuiMenuItemCtrl() { Text = "-"; };
- new GuiMenuItemCtrl() {
- Text = "Align Top";
- Command = "GuiEditor.brain.Justify(3);";
- Accelerator = "Ctrl T";
- };
- new GuiMenuItemCtrl() {
- Text = "Align Bottom";
- Command = "GuiEditor.brain.Justify(4);";
- Accelerator = "Ctrl B";
- };
- new GuiMenuItemCtrl() {
- Text = "Align Left";
- Command = "GuiEditor.brain.Justify(0);";
- Accelerator = "Ctrl L";
- };
- new GuiMenuItemCtrl() {
- Text = "Align Right";
- Command = "GuiEditor.brain.Justify(2);";
- Accelerator = "Ctrl R";
- };
- new GuiMenuItemCtrl() { Text = "-"; };
- new GuiMenuItemCtrl() {
- Text = "Center Horizontally";
- Command = "GuiEditor.brain.Justify(1);";
- };
- new GuiMenuItemCtrl() {
- Text = "Space Vertically";
- Command = "GuiEditor.brain.Justify(5);";
- };
- new GuiMenuItemCtrl() {
- Text = "Space Horizontally";
- Command = "GuiEditor.brain.Justify(6);";
- };
- new GuiMenuItemCtrl() { Text = "-"; };
- new GuiMenuItemCtrl() {
- Text = "Bring to Front";
- Command = "GuiEditor.brain.BringToFront();";
- Accelerator = "Ctrl-Shift Up";
- };
- new GuiMenuItemCtrl() {
- Text = "Push to Back";
- Command = "GuiEditor.brain.PushToBack();";
- Accelerator = "Ctrl-Shift Down";
- };
- new GuiMenuItemCtrl() { Text = "-"; };
- new GuiMenuItemCtrl() {
- Text = "Set Grid Size...";
- Command = "GuiEditor.SetGridSize();";
- Accelerator = "Ctrl-Shift G";
- };
- new GuiMenuItemCtrl() {
- Text = "Snap to Grid";
- Toggle = "1";
- IsOn = "1";
- Command = "GuiEditor.SnapToGrid(true);";
- AltCommand = "GuiEditor.SnapToGrid(false);";
- Accelerator = "Ctrl G";
- };
- };
- new GuiMenuItemCtrl() {
- Text = "Select";
- Active = "0";
-
- new GuiMenuItemCtrl() {
- Text = "Select All";
- Command = "GuiEditor.brain.SelectAll();";
- Accelerator = "Ctrl A";
- };
- new GuiMenuItemCtrl() {
- Text = "Deselect";
- Command = "GuiEditor.brain.clearSelection();";
- Accelerator = "Ctrl D";
+ Command = "EditorCore.guardedCommand(\"quit();\");";
};
};
new GuiMenuItemCtrl() {
@@ -309,6 +197,18 @@
ThemeManager.setProfile(%this.menuBar, "scrollingPanelArrowProfile", "ArrowProfile");
ThemeManager.setProfile(%this.menuBar, "scrollingPanelTrackProfile", "TrackProfile");
+ // The bar reads Torque2D | the open editor's menus | Theme, and the two ends
+ // are the only parts that never change. Theme has to be held onto by hand
+ // because it has to come off and go back on around every swap - see
+ // setEditorMenus - and it is taken by position rather than by name because
+ // naming it would put back exactly the by-text coupling the swap exists to
+ // remove. Last child, so this is still right once the Gui Editor's menus have
+ // moved out of the literal above.
+ %this.themeMenu = %this.menuBar.getObject(%this.menuBar.getCount() - 1);
+
+ // Where a set's menus wait while another editor has the bar.
+ %this.menuPark = new SimGroup();
+
%this.baseGui.add(%this.menuBar);
%this.tabBook = new GuiTabBookCtrl()
@@ -412,6 +312,91 @@
}
}
+// Run %command, unless an editor is holding something the command would discard
+// without being able to give it back. Used by Close Project and Exit.
+//
+// Two editors have something to lose now, and they are asked one at a time. Each
+// guard either runs what it was given or takes the decision away and hands it on
+// once the user has answered, so the chain reads:
+//
+// guardedCommand the Asset Manager's unsaved assets, then...
+// guardedCommandAfterAssets the Gui Editor's unsaved document, then...
+// eval the command itself
+//
+// A third editor with a document joins at the front of that chain, not by being
+// added to a list: the answers are not interchangeable, and each guard needs to
+// name the one that follows it.
+function EditorCore::guardedCommand(%this, %command)
+{
+ if(isObject(AssetAdmin) && AssetAdmin.hasUnsavedAssets())
+ {
+ AssetAdmin.guardAssets(%command);
+ return;
+ }
+
+ %this.guardedCommandAfterAssets(%command);
+}
+
+// The rest of the chain, once the Asset Manager has been dealt with. Discarding
+// unsaved assets comes back in here rather than at the top, because the assets
+// are still unsaved and asking again would never end.
+function EditorCore::guardedCommandAfterAssets(%this, %command)
+{
+ if(isObject(GuiEditor))
+ {
+ GuiEditor.guardDocument(%command);
+ return;
+ }
+
+ eval(%command);
+}
+
+// Put %menuSet's menus on the bar and take the last editor's off, so the bar
+// always reads Torque2D | the open editor's menus | Theme. Pass "" for an editor
+// with no menus of its own, which is what the Console and the Project Manager
+// are. Called from every editor's open() and close().
+//
+// Theme comes off and goes back on around the swap, and that is not fussiness.
+// The bar links the chain its keyboard walk follows by assuming each new menu
+// was appended - it takes the second-to-last child as the new one's neighbour -
+// and reordering afterwards repairs the layout but not the chain. Appending is
+// the only move that leaves the bar correct, so the fixed tail has to move.
+//
+// Nothing here is deleted. Each set parks its own menus, which is what keeps
+// them alive, keeps them out of the accelerator walk, and keeps the bar they
+// remember - set once, never filled in again - pointing at a real bar.
+function EditorCore::setEditorMenus(%this, %menuSet)
+{
+ if(%this.activeMenus == %menuSet)
+ {
+ return;
+ }
+
+ %this.menuPark.add(%this.themeMenu);
+
+ if(isObject(%this.activeMenus))
+ {
+ %this.activeMenus.detach();
+ }
+ %this.activeMenus = %menuSet;
+ if(isObject(%menuSet))
+ {
+ %menuSet.attach();
+ }
+
+ %this.menuBar.add(%this.themeMenu);
+
+ // The canvas keeps one flat list of accelerators and rebuilds it only when a
+ // dialog is pushed or popped. A tab change is neither, so without this the
+ // menus that just arrived have dead shortcuts and the ones that just left
+ // still have live ones - a parked item is still active and still visible, so
+ // its command would run.
+ if(isObject(Canvas))
+ {
+ Canvas.rebuildAcceleratorMap();
+ }
+}
+
function EditorCore::RegisterEditor(%this, %name, %editor)
{
%this.page[%name] = new GuiTabPageCtrl()
@@ -529,4 +514,81 @@
function EditorCore::deleteDialog(%this)
{
%this.dialog.delete();
-}
\ No newline at end of file
+}
+
+// Deferred delete for a specific dialog. Unlike deleteDialog's shared
+// %this.dialog slot, this takes the dialog as an argument, so stacked
+// dialogs can each schedule their own cleanup without racing. Never
+// schedule the native "delete" directly on an object - the scheduler
+// dispatches it inside the object's own script-callback guard, which
+// asserts; routing through a parent's script method like this one is safe.
+function EditorCore::deleteDialogObject(%this, %dialog)
+{
+ if(isObject(%dialog))
+ {
+ %dialog.delete();
+ }
+}
+
+// Titles the picker after what it is picking. The type names arrive from the
+// engine as one word ("ImageAsset"), which is how a class is spelled and not how
+// a title is, so the internal capital becomes a space and the article is chosen
+// to suit: an Image Asset, but a Font Asset.
+function EditorCore::assetTypeTitle(%this, %assetType)
+{
+ // strcmp, not $=. String-equal is case-insensitive in TorqueScript, so
+ // "m" $= "M" is true and a test for "is this letter a capital" written that
+ // way answers yes for every letter.
+ %spaced = "";
+ for(%i = 0; %i < strlen(%assetType); %i++)
+ {
+ %char = getSubStr(%assetType, %i, 1);
+ %isCapital = (strcmp(%char, strupr(%char)) == 0) && (strcmp(%char, strlwr(%char)) != 0);
+ if(%i > 0 && %isCapital)
+ {
+ %spaced = %spaced SPC %char;
+ }
+ else
+ {
+ %spaced = %spaced @ %char;
+ }
+ }
+
+ %first = strupr(getSubStr(%assetType, 0, 1));
+ %article = (strstr("AEIOU", %first) != -1) ? "an" : "a";
+
+ return "Choose" SPC %article SPC %spaced;
+}
+
+// The one way into the asset picker. Both callers come through here: the Gui
+// Profile Editor's Image Asset row, and the native inspector's browse button,
+// which the engine points at this method by name (see
+// GuiInspectorTypeAsset::constructEditControl). The picker hands the chosen
+// asset id to %callbackTarget.%callbackMethod, the same target-and-method pair
+// every other editor dialog returns through.
+function EditorCore::openAssetPicker(%this, %callbackTarget, %callbackMethod, %currentAsset, %assetType)
+{
+ if(%assetType $= "")
+ {
+ %this.alert("openAssetPicker needs an asset type to look for.");
+ return;
+ }
+
+ %width = 640;
+ %height = 500;
+ %dialog = new GuiControl()
+ {
+ class = "EditorAssetPickerDialog";
+ superclass = "EditorDialog";
+ dialogSize = (%width + 8) SPC (%height + 8);
+ dialogCanClose = true;
+ dialogText = %this.assetTypeTitle(%assetType);
+ assetType = %assetType;
+ currentAsset = %currentAsset;
+ callbackTarget = %callbackTarget;
+ callbackMethod = %callbackMethod;
+ };
+ %dialog.init(%width, %height);
+
+ Canvas.pushDialog(%dialog);
+}
diff --git a/editor/EditorCore/EditorDialog.cs b/editor/EditorCore/EditorDialog.cs
index 2308b13a6..4e1a4dd61 100644
--- a/editor/EditorCore/EditorDialog.cs
+++ b/editor/EditorCore/EditorDialog.cs
@@ -20,10 +20,32 @@
// IN THE SOFTWARE.
//-----------------------------------------------------------------------------
+// The room a dialog's content actually gets: the window's extent less its
+// border, and less the 30 pixel title bar above it.
+//
+// Worth having as a function because getting it wrong is invisible until the
+// content is tall enough to reach the bottom -- a button positioned from the
+// dialog's own height rather than from this lands below the fold, reachable only
+// by scrolling, which is how both the Frame Range and Duplicate dialogs shipped.
+function EditorDialog::contentWidth(%this)
+{
+ return getWord(%this.dialogSize, 0) - 8;
+}
+
+function EditorDialog::contentHeight(%this)
+{
+ return getWord(%this.dialogSize, 1) - 34;
+}
+
function EditorDialog::onAdd(%this)
{
ThemeManager.setProfile(%this, "overlayProfile");
+ // Resizable unless the dialog says otherwise. A form whose contents are laid
+ // out at fixed positions gains nothing from being dragged bigger and loses
+ // something from being dragged smaller, so those set dialogResizable = false.
+ %resizable = (%this.dialogResizable $= "") ? true : %this.dialogResizable;
+
%this.window = new GuiWindowCtrl()
{
class = "EditorDialogWindow";
@@ -36,6 +58,8 @@ class = "EditorDialogWindow";
canMove = true;
CanMinimize = false;
CanMaximize = false;
+ resizeWidth = %resizable;
+ resizeHeight = %resizable;
titleHeight = 30;
dialog = %this;
};
diff --git a/editor/EditorCore/EditorFieldRow.cs b/editor/EditorCore/EditorFieldRow.cs
new file mode 100644
index 000000000..dfdc515ca
--- /dev/null
+++ b/editor/EditorCore/EditorFieldRow.cs
@@ -0,0 +1,705 @@
+
+//-----------------------------------------------------------------------------
+// One field cell in an editor's properties pane: a caption above an editor
+// sized to the field's type, with a reset button beside it.
+//
+// Grown in the Gui Profile Editor and now shared -- the Gui Editor's inspector
+// pane, the Profile Editor's three forms and the Asset Manager's asset panes all
+// build their rows from this, which is why it lives in EditorCore rather than in
+// the module that first wanted it.
+//
+// Caption-above-editor rather than caption-beside-editor because these are grid
+// cells: the pane flows them left-to-right and wraps into as many columns as the
+// pane is wide, the way the native inspector does, so a cell has to stay narrow.
+// It also stops long captions ("Horizontal Align") from clipping.
+//
+// The grid resizes every cell it lays out, so the widgets carry sizing flags
+// rather than fixed geometry: the caption and editor follow the cell width and
+// the reset button stays pinned to its right edge.
+//
+// The row owns its widgets and nothing else. It never reads or writes the thing
+// being edited -- it hands values to its owner and takes them back, so the owner
+// stays the single place that knows about theme overrides, array-indexed fields,
+// and dirty marking. Commits arrive at owner.onFieldRowCommit(%row) and reset
+// clicks at owner.onFieldRowReset(%row).
+//
+// The creator sets these inline: fieldName, labelText, kind, owner, and for kind
+// "enum" the tab-separated enumItems. Optionally swatchWidth (a "color" row that
+// should not fill its cell) and editorHeight (how deep a "multiline" box is).
+// Call build() once after adding the row to its container -- the container
+// decides the cell width, so build() has to run after the add. It records the
+// laid-out height in .rowHeight. setTooltip() after that gives the row a standing
+// explanation of its field, which survives being greyed and re-enabled.
+//
+// Two things a row takes from its OWNER rather than from itself, because they
+// are decisions the whole pane makes once:
+//
+// swatchClass the class a color popup wears. Empty gives a plain one; the
+// Profile Editor points it at GuiProfileEditorColorPopup, which
+// fills the swatch row from the theme in its tree. That class
+// belongs to the Gui Editor module and cannot be named here.
+// findBase what a "file" row's Find button makes its path relative to.
+// The default is the game root, which is what a bitmap path
+// means; an asset's loose file is relative to the asset's own
+// folder instead.
+// fileFilters what that Find button's dialog offers, and
+// fileTitle what it calls itself. Both default to bitmaps, which is what a
+// "file" row was everywhere until fonts and sounds got panes.
+//
+// One thing a row takes from ITSELF, because a pane can hold rows that want
+// different answers -- the emitter pane has an image row and an animation row
+// side by side:
+//
+// assetType what an "asset" row's Find button offers to pick. Defaults to
+// ImageAsset, which is what every asset row was until then.
+//
+// Kinds: text, number, decimal, point, pointf, bool, color, enum, dropdown,
+// file, asset, multiline.
+//-----------------------------------------------------------------------------
+
+function EditorFieldRow::onAdd(%this)
+{
+ ThemeManager.setProfile(%this, "emptyProfile");
+}
+
+function EditorFieldRow::build(%this)
+{
+ // The grid sizes a cell the moment it is added, which is before build()
+ // runs, so lay out against the width we actually have rather than the
+ // nominal one -- otherwise every widget would be placed for a 220-wide cell
+ // inside a cell the grid had already widened. Sizing flags take it from here.
+ %w = getWord(%this.getExtent(), 0);
+ %pad = 4;
+ %resetW = 24;
+ %labelH = 16;
+
+ // No caption means no room kept for one. The text block asks for this: its
+ // text box is captioned by the row above, which is what leaves space beside
+ // the caption for the wrap and extend icons. The label control is still
+ // built, so setLabelText can give it one later.
+ %captioned = %this.labelText !$= "";
+ %editorY = %captioned ? (%labelH + 4) : 2;
+
+ // A multiline row is a wrapped text box several lines deep rather than one
+ // line of a plain one. Everything else about it is a text row. Three lines
+ // unless the creator asked for a particular depth -- a row that has a grid
+ // cell to itself can afford more, and an empty box the size of its neighbours
+ // says "this is where the prose goes" in a way a three-line one does not.
+ %editorH = 24;
+ if(%this.kind $= "multiline")
+ {
+ %editorH = (%this.editorHeight > 0) ? %this.editorHeight : 62;
+ }
+ %h = %editorY + %editorH + 4;
+
+ // The editor stops short of the reset button so the two never overlap once
+ // the reset appears.
+ %editorW = %w - (%pad * 2) - %resetW - 2;
+
+ %this.rowHeight = %h;
+ %this.setExtent(%w, %h);
+
+ %this.label = new GuiControl()
+ {
+ HorizSizing = "width";
+ Position = %pad SPC 2;
+ Extent = (%w - %pad * 2) SPC %labelH;
+ Text = %this.labelText;
+ align = "left";
+ vAlign = "middle";
+ Visible = %captioned;
+ };
+ ThemeManager.setProfile(%this.label, "labelProfile");
+ %this.add(%this.label);
+
+ %kind = %this.kind;
+ if(%kind $= "bool")
+ {
+ // The caption above already names the field, so the box carries no text
+ // of its own -- and it stays square rather than stretching, which a wide
+ // empty checkbox would do.
+ %this.editor = new GuiCheckBoxCtrl()
+ {
+ Position = %pad SPC %editorY;
+ Extent = "20 20";
+ Text = "";
+ boxOffset = "0 1";
+ boxExtent = "18 18";
+ textExtent = "0 18";
+ Command = %this.getID() @ ".commit();";
+ };
+ ThemeManager.setProfile(%this.editor, "checkboxProfile");
+ %this.add(%this.editor);
+ }
+ else if(%kind $= "color")
+ {
+ // swatchWidth opts a row out of filling its cell. A swatch that spans the
+ // whole width reads as a bar of flat color -- a progress bar, a divider --
+ // rather than as the button it is. Rows that show a state's colors keep
+ // the full width, because there four of them share it and the widths are
+ // how you tell which is which.
+ %this.editor = %this.makeSwatch(%pad, %editorY,
+ (%this.swatchWidth > 0) ? %this.swatchWidth : %editorW, 22);
+ }
+ else if(%kind $= "enum" || %kind $= "dropdown")
+ {
+ %this.editor = new GuiDropDownCtrl()
+ {
+ class = "EditorFieldRowDropDown";
+ HorizSizing = "width";
+ Position = %pad SPC %editorY;
+ Extent = %editorW SPC 22;
+ ConstantThumbHeight = false;
+ ScrollBarThickness = 12;
+ ShowArrowButtons = true;
+ owner = %this;
+ selectMethod = "commit";
+ };
+ ThemeManager.setProfile(%this.editor, "dropDownProfile");
+ ThemeManager.setProfile(%this.editor, "dropDownItemProfile", "listBoxProfile");
+ ThemeManager.setProfile(%this.editor, "emptyProfile", "backgroundProfile");
+ ThemeManager.setProfile(%this.editor, "scrollingPanelProfile", "ScrollProfile");
+ ThemeManager.setProfile(%this.editor, "scrollingPanelThumbProfile", "ThumbProfile");
+ ThemeManager.setProfile(%this.editor, "scrollingPanelTrackProfile", "TrackProfile");
+ ThemeManager.setProfile(%this.editor, "scrollingPanelArrowProfile", "ArrowProfile");
+ %this.add(%this.editor);
+
+ if(%kind $= "enum")
+ {
+ %this.fillItems(%this.enumItems);
+ }
+ }
+ else if(%kind $= "point" || %kind $= "pointf")
+ {
+ // A two-part field ("x y") gets one box per axis; either one commits both.
+ // Relative sizing splits the widened cell evenly between them.
+ %boxW = (%editorW - 6) / 2;
+ %this.editor = %this.makeInput(%pad, %editorY, %boxW, 22, true, "relative");
+ %this.editorY = %this.makeInput(%pad + %boxW + 6, %editorY, %boxW, 22, true, "relative");
+ }
+ else if(%kind $= "file")
+ {
+ // A path the user should not have to type: a text box plus a Find button
+ // that opens a file dialog and writes back what it picked.
+ %this.makeFindRow(%pad, %editorY, %editorW, ".onFindClicked();");
+ }
+ else if(%kind $= "asset")
+ {
+ // Same shape as a file, but Find opens the asset picker instead of the
+ // file dialog -- an asset id is no more typeable from memory than a path.
+ %this.makeFindRow(%pad, %editorY, %editorW, ".onFindAssetClicked();");
+ }
+ else if(%kind $= "multiline")
+ {
+ // The full cell width: this row's reset button is never shown, and the
+ // caption line above it is where its owner puts anything else.
+ //
+ // mTextWrap is what makes GuiTextEditCtrl multi-line -- it wraps, scrolls
+ // and hit-tests in line space (guiTextEditCtrl.cc). It still cannot take a
+ // typed newline: handleEnterKey has no insert path, so Enter goes on
+ // meaning commit and a long string simply wraps.
+ %this.editor = %this.makeInput(%pad, %editorY, %w - (%pad * 2), %editorH, false, "width");
+ %this.editor.textWrap = true;
+ %this.editor.vAlign = "top";
+ }
+ else
+ {
+ %this.editor = %this.makeInput(%pad, %editorY, %editorW, 22,
+ %kind $= "number" || %kind $= "decimal", "width");
+ }
+
+ // The per-field reset, shown only while the field is overridden. The icon is
+ // the circular revert arrow. "left" sizing keeps the button pinned to the
+ // cell's right edge as the grid widens.
+ %this.resetButton = new GuiButtonCtrl()
+ {
+ class = "EditorIconButton";
+ Frame = $EditorIcon::playback_reload;
+ HorizSizing = "left";
+ Position = (%w - %resetW - %pad) SPC (%editorY - 1);
+ Tooltip = "Reset this field to the theme's value";
+ Command = %this.getID() @ ".onResetClicked();";
+ Visible = false;
+ };
+ ThemeManager.setProfile(%this.resetButton, "iconButtonProfile");
+ %this.add(%this.resetButton);
+}
+
+// A text box with a Find button beside it, for the two fields whose value is
+// something to be chosen rather than typed. The caller supplies the method the
+// button calls; everything else about the two rows is identical, including the
+// button keeping its place at the cell's right edge as the grid widens.
+function EditorFieldRow::makeFindRow(%this, %pad, %editorY, %editorW, %command)
+{
+ %buttonW = 56;
+ %this.editor = %this.makeInput(%pad, %editorY, %editorW - %buttonW - 4, 22, false, "width");
+ %this.findButton = new GuiButtonCtrl()
+ {
+ HorizSizing = "left";
+ Position = (%pad + %editorW - %buttonW) SPC %editorY;
+ Extent = %buttonW SPC 22;
+ Text = "Find";
+ Command = %this.getID() @ %command;
+ };
+ ThemeManager.setProfile(%this.findButton, "buttonProfile");
+ %this.add(%this.findButton);
+}
+
+// True where the field holds a real number rather than a whole one. Kept as a
+// question about the kind rather than a flag on the row, because it is asked
+// from three places and has to answer the same way in all of them.
+function EditorFieldRow::isDecimalKind(%this)
+{
+ return %this.kind $= "decimal" || %this.kind $= "pointf";
+}
+
+// A text box that commits on blur (AltCommand) and on Enter, matching how the
+// native inspector and the border grid apply their edits.
+function EditorFieldRow::makeInput(%this, %x, %y, %w, %h, %numeric, %sizing)
+{
+ %decimal = %this.isDecimalKind();
+
+ %box = new GuiTextEditCtrl()
+ {
+ // The class only goes on a box that wants the arrow keys to step its
+ // value. GuiTextEditCtrl gives a script onUpArrow the key before its own
+ // caret movement (guiTextEditCtrl.cc handleKeyDownWithNoModifier), and
+ // isMethod answers for the CLASS, not the instance -- so wearing this on
+ // a text box meant nudge() swallowed both arrows and did nothing with
+ // them, which is what stopped the caret moving between lines in the
+ // multi-line box.
+ class = %numeric ? "EditorFieldRowInput" : "";
+ HorizSizing = %sizing;
+ Position = %x SPC %y;
+ Extent = %w SPC %h;
+ align = %numeric ? "center" : "left";
+ row = %this;
+ numeric = %numeric;
+
+ // What an arrow key is worth. A font size multiplier lives between 0.5
+ // and 3, so stepping it by one is the same as not offering the key.
+ step = %decimal ? 0.1 : 1;
+ };
+ if(%numeric)
+ {
+ // Number mode refuses the decimal point, which would make a float field
+ // impossible to type into.
+ %box.inputMode = %decimal ? "Decimal" : "Number";
+ }
+ ThemeManager.setProfile(%box, "textEditProfile");
+ %box.AltCommand = %this.getID() @ ".commit();";
+ %box.ReturnCommand = %this.getID() @ ".commit();";
+ %this.add(%box);
+ return %box;
+}
+
+// The swatch's class comes from the owner, not from here: the Profile Editor
+// wants one that fills its swatch row from the theme under the tree, and that
+// class lives in the Gui Editor module, which EditorCore knows nothing about.
+// Empty gives the plain popup, which is what a pane with no theme to offer wants.
+function EditorFieldRow::makeSwatch(%this, %x, %y, %w, %h)
+{
+ %swatch = new GuiColorPopupCtrl()
+ {
+ class = isObject(%this.owner) ? %this.owner.swatchClass : "";
+ // A fixed-width swatch stays put as the cell widens; a full-width one
+ // follows it.
+ HorizSizing = (%this.swatchWidth > 0) ? "anchorLeft" : "width";
+ Position = %x SPC %y;
+ Extent = %w SPC %h;
+ showColorValues = true;
+ };
+ ThemeManager.setProfile(%swatch, "colorPickerProfile");
+ ThemeManager.setProfile(%swatch, "emptyProfile", "backgroundProfile");
+ ThemeManager.setProfile(%swatch, "colorPopupProfile", "popupProfile");
+ ThemeManager.setProfile(%swatch, "emptyProfile", "pickerProfile");
+ ThemeManager.setProfile(%swatch, "colorPickerSelectorProfile", "selectorProfile");
+ ThemeManager.setProfile(%swatch, "textEditProfile", "valueProfile");
+ // The popup hands this on to its R/G/B/A boxes, which name their channel with
+ // a tooltip -- without it they would each fall back to a profile of their own.
+ ThemeManager.setProfile(%swatch, "tipProfile", "TooltipProfile");
+ %swatch.Command = %this.getID() @ ".commit();";
+ %this.add(%swatch);
+ return %swatch;
+}
+
+//-----------------------------------------------------------------------------
+// Value marshalling. The owner supplies and receives plain field strings; the
+// row knows how its widget spells them.
+//-----------------------------------------------------------------------------
+
+// Loading a value also records it, so a later commit can tell an actual edit
+// from a text box that merely lost focus. The recorded form is whatever the
+// widget reads back, not what was passed in, because the two differ: a ColorI
+// field holding "White" comes back out of the swatch as "255 255 255 255".
+function EditorFieldRow::setValue(%this, %value)
+{
+ %this.applyValue(%value);
+ %this.lastValue = %this.getValue();
+}
+
+// True when the widget now holds something other than what was loaded into it.
+function EditorFieldRow::hasChanged(%this)
+{
+ return %this.getValue() !$= %this.lastValue;
+}
+
+// Accept the widget's current contents as the new baseline, after a commit.
+function EditorFieldRow::markClean(%this)
+{
+ %this.lastValue = %this.getValue();
+}
+
+function EditorFieldRow::applyValue(%this, %value)
+{
+ %kind = %this.kind;
+ if(%kind $= "bool")
+ {
+ %this.editor.setStateOn(%value);
+ }
+ else if(%kind $= "color")
+ {
+ // setColorI wants four integers, but a ColorI field holding a stock color
+ // comes back as a single name token; baseColor parses those.
+ if(getWordCount(%value) >= 4)
+ {
+ %this.editor.setColorI(%value);
+ }
+ else
+ {
+ %this.editor.baseColor = %value;
+ }
+ }
+ else if(%kind $= "enum" || %kind $= "dropdown")
+ {
+ %this.selectItem(%value);
+ }
+ else if(%kind $= "point" || %kind $= "pointf")
+ {
+ %this.editor.setText(getWord(%value, 0));
+ %this.editorY.setText(getWord(%value, 1));
+ }
+ else
+ {
+ %this.editor.setText(%value);
+ }
+}
+
+function EditorFieldRow::getValue(%this)
+{
+ %kind = %this.kind;
+ if(%kind $= "bool")
+ {
+ return %this.editor.getStateOn();
+ }
+ if(%kind $= "color")
+ {
+ return %this.editor.getColorI();
+ }
+ if(%kind $= "enum" || %kind $= "dropdown")
+ {
+ return %this.editor.getText();
+ }
+ if(%kind $= "point" || %kind $= "pointf")
+ {
+ return %this.numberIn(%this.editor) SPC %this.numberIn(%this.editorY);
+ }
+ if(%kind $= "number" || %kind $= "decimal")
+ {
+ return %this.numberIn(%this.editor);
+ }
+ return %this.editor.getText();
+}
+
+// A whole-number field is floored, so a box left holding "12.0" does not write
+// "12.0" into a Point2I. A real one is not: flooring a font size multiplier
+// turns every 1.5 into a 1, which is how a control that would not resize looked
+// like a control whose font size did nothing.
+function EditorFieldRow::numberIn(%this, %box)
+{
+ return %this.isDecimalKind() ? %box.getText() : mFloor(%box.getText());
+}
+
+//-----------------------------------------------------------------------------
+// Drop-down contents.
+//-----------------------------------------------------------------------------
+
+// %items is tab-separated. The current selection survives a refill even when
+// the new list does not contain it (a font face outside the directory).
+function EditorFieldRow::fillItems(%this, %items)
+{
+ %selected = %this.currentItem;
+ %this.editor.clearItems();
+
+ %count = getFieldCount(%items);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %item = getField(%items, %i);
+ if(%item !$= "")
+ {
+ %this.editor.addItem(%item);
+ }
+ }
+
+ if(%selected !$= "")
+ {
+ %this.selectItem(%selected);
+ }
+}
+
+function EditorFieldRow::selectItem(%this, %value)
+{
+ %this.currentItem = %value;
+ if(%value $= "")
+ {
+ return;
+ }
+
+ %index = %this.editor.findItemText(%value, false);
+ if(%index >= 0)
+ {
+ %this.editor.setSelected(%index);
+ return;
+ }
+
+ // Not offered: keep it anyway rather than silently switching the field.
+ %this.editor.insertItem(0, %value);
+ %this.editor.setSelected(0);
+}
+
+//-----------------------------------------------------------------------------
+// Filtering, enabling and the override marker.
+//-----------------------------------------------------------------------------
+
+// What the row says about itself when it is working normally, as opposed to the
+// reason setEnabled gives for it not being.
+//
+// The two share one tooltip, so they have to be told about each other: a row that
+// is greyed and then re-enabled used to come back with no tooltip at all, because
+// setEnabled blanked it. The explanation is kept here so enabling can put it back.
+//
+// A field's own doc string would be the obvious source and is not one -- the
+// engine's is empty on every AudioAsset field and on most others, so this is set
+// by the pane that knows what the field means (AssetInspectorPane::tipFor).
+function EditorFieldRow::setTooltip(%this, %tip)
+{
+ %this.baseTip = %tip;
+ %this.applyTooltip(%tip);
+}
+
+// One tooltip on every widget the row owns. The editor alone is not enough on a
+// "file" or "asset" row, where the Find button is half the row's hit area, nor on
+// a point row, where the second box is half of it.
+function EditorFieldRow::applyTooltip(%this, %tip)
+{
+ %this.editor.Tooltip = %tip;
+ if(isObject(%this.editorY))
+ {
+ %this.editorY.Tooltip = %tip;
+ }
+ if(isObject(%this.findButton))
+ {
+ %this.findButton.Tooltip = %tip;
+ }
+}
+
+// A field the current control never reads stays visible but inert, so its value
+// is never lost -- the pane's Show All puts it back in reach.
+function EditorFieldRow::setEnabled(%this, %enabled, %reason)
+{
+ %this.editor.setActive(%enabled);
+ if(isObject(%this.editorY))
+ {
+ %this.editorY.setActive(%enabled);
+ }
+ if(isObject(%this.findButton))
+ {
+ %this.findButton.setActive(%enabled);
+ }
+
+ // Enabling restores what the row normally says rather than blanking it.
+ %this.applyTooltip(%enabled ? %this.baseTip : %reason);
+}
+
+// One field wears a different name depending on the category (cursorColor is a
+// text caret in one control and a focus rectangle in another), so the pane can
+// retitle a row after it is built.
+function EditorFieldRow::setLabelText(%this, %text)
+{
+ %this.labelText = %text;
+ %this.label.setText(%text);
+}
+
+function EditorFieldRow::setOverridden(%this, %overridden)
+{
+ ThemeManager.setProfile(%this.label, %overridden ? "overrideLabelProfile" : "labelProfile");
+ %this.resetButton.setVisible(%overridden);
+}
+
+//-----------------------------------------------------------------------------
+// Commit. Every path lands in commit(), which does nothing while the owner is
+// populating -- otherwise loading a profile would echo back as user edits and
+// mark spurious theme overrides.
+//-----------------------------------------------------------------------------
+
+function EditorFieldRow::commit(%this)
+{
+ if(!isObject(%this.owner) || %this.owner.populating)
+ {
+ return;
+ }
+ if(%this.kind $= "enum" || %this.kind $= "dropdown")
+ {
+ %this.currentItem = %this.editor.getText();
+ }
+ %this.owner.onFieldRowCommit(%this);
+}
+
+function EditorFieldRow::onResetClicked(%this)
+{
+ if(isObject(%this.owner))
+ {
+ %this.owner.onFieldRowReset(%this);
+ }
+}
+
+// What a chosen path is written back relative to. The game root is the right
+// answer for a bitmap named in a profile -- it is the only form that means the
+// same thing on somebody else's machine -- but an asset's loose file is stored
+// relative to the asset's own folder, so a pane that edits one sets findBase.
+function EditorFieldRow::pathBase(%this)
+{
+ if(isObject(%this.owner) && %this.owner.findBase !$= "")
+ {
+ return %this.owner.findBase;
+ }
+ return getMainDotCsDir();
+}
+
+// What the Find dialog offers, and what it calls itself.
+//
+// Bitmaps by default: a "file" row was a picture everywhere until the Asset
+// Manager grew panes for fonts and sounds, whose loose file is a .fnt or a .wav
+// and for which an image filter offers nothing that can be chosen. Taken from
+// the owner rather than the row for the same reason findBase is -- it is one
+// decision a pane makes about the one loose file it edits.
+function EditorFieldRow::fileFilters(%this)
+{
+ if(isObject(%this.owner) && %this.owner.fileFilters !$= "")
+ {
+ return %this.owner.fileFilters;
+ }
+ return "Image Files (*.png;*.jpg;*.jpeg;*.bmp)|*.png;*.jpg;*.jpeg;*.bmp|All Files (*.*)|*.*";
+}
+
+function EditorFieldRow::fileTitle(%this)
+{
+ if(isObject(%this.owner) && %this.owner.fileTitle !$= "")
+ {
+ return %this.owner.fileTitle;
+ }
+ return "Choose an Image";
+}
+
+// The Find button on a "file" row. Picks a file and writes its path back into
+// the box relative to whatever pathBase() says it should be measured from.
+function EditorFieldRow::onFindClicked(%this)
+{
+ // Where the path is measured from, and where the dialog opens. They are the
+ // same folder when a pane has named one; with no base the path is the game
+ // root's but the dialog still opens on the project, which is where the
+ // pictures are.
+ %base = %this.pathBase();
+ %start = (isObject(%this.owner) && %this.owner.findBase !$= "")
+ ? %base : pathConcat(%base, ProjectManager.getProjectFolder());
+
+ %dialog = new OpenFileDialog()
+ {
+ Filters = %this.fileFilters();
+ ChangePath = false;
+ MultipleFiles = false;
+ DefaultFile = "";
+ defaultPath = %start;
+ title = %this.fileTitle();
+ };
+ %result = %dialog.execute();
+ %fileName = %dialog.fileName;
+ %dialog.delete();
+
+ if(!%result || %fileName $= "")
+ {
+ return;
+ }
+
+ %this.editor.setText(makeRelativePath(%fileName, %base));
+ %this.commit();
+}
+
+// The Find button on an "asset" row. The picker lives in EditorCore because the
+// native inspector's browse button uses it too; it hands back the chosen id
+// through onAssetPicked. Whatever the box holds now is passed along so the
+// picker opens on the current choice.
+//
+// assetType is per ROW rather than per pane -- unlike findBase and fileFilters
+// above -- because the emitter pane carries an Image row and an Animation row in
+// the same block and they want different lists. It defaults to ImageAsset, which
+// was hardcoded here until the second kind of asset row existed.
+function EditorFieldRow::onFindAssetClicked(%this)
+{
+ %type = (%this.assetType $= "") ? "ImageAsset" : %this.assetType;
+
+ EditorCore.openAssetPicker(%this, "onAssetPicked", %this.editor.getText(), %type);
+}
+
+// An asset id is already portable -- it names a module and an asset, not a
+// place on this machine -- so unlike a bitmap path it goes in as it came out.
+function EditorFieldRow::onAssetPicked(%this, %assetId)
+{
+ %this.editor.setText(%assetId);
+ %this.commit();
+}
+
+//-----------------------------------------------------------------------------
+// Two widget helpers, kept here because each exists only to route one engine
+// callback back to whatever owns the widget -- the same arrangement the border
+// grid uses for GuiProfileEditorBorderInput. The drop-down is shared: the
+// profile pane's category picker uses it too, pointing selectMethod at its own
+// handler.
+//-----------------------------------------------------------------------------
+
+// No onTouchDown override here on purpose. Re-selecting the whole value on every
+// click made the caret impossible to place: the engine had already put it where
+// the click landed, and selecting all moved the selection anchor back to the
+// start, so the next drag swept from the beginning of the field. Tabbing in
+// still selects everything - GuiTextEditCtrl::setFirstResponder does that - and
+// a click now does what a click does.
+
+function EditorFieldRowInput::onUpArrow(%this)
+{
+ %this.nudge(1);
+}
+
+function EditorFieldRowInput::onDownArrow(%this)
+{
+ %this.nudge(-1);
+}
+
+function EditorFieldRowInput::nudge(%this, %delta)
+{
+ if(!%this.numeric)
+ {
+ return;
+ }
+
+ // Rounded, or repeated tenths accumulate into 1.2000000476837158.
+ %value = %this.getText() + (%delta * %this.step);
+ %this.setText(%this.step < 1 ? mFloatLength(%value, 2) : %value);
+
+ %this.selectAllText();
+ %this.row.commit();
+}
+
+function EditorFieldRowDropDown::onSelect(%this)
+{
+ %this.owner.call(%this.selectMethod);
+}
diff --git a/editor/EditorCore/EditorForm.cs b/editor/EditorCore/EditorForm.cs
index ae303577d..8db278222 100644
--- a/editor/EditorCore/EditorForm.cs
+++ b/editor/EditorCore/EditorForm.cs
@@ -42,6 +42,22 @@
return %label;
}
+// Explains a row on hover. The tip goes on the caption as well as on the input,
+// because the caption is the bigger target and it is what someone is looking at
+// when the question comes up -- and the caption is the input's parent, so
+// without both, half the row says nothing.
+function EditorForm::setItemTip(%this, %label, %control, %tip)
+{
+ %label.Tooltip = %tip;
+ ThemeManager.setProfile(%label, "tipProfile", "TooltipProfile");
+
+ if(isObject(%control))
+ {
+ %control.Tooltip = %tip;
+ ThemeManager.setProfile(%control, "tipProfile", "TooltipProfile");
+ }
+}
+
function EditorForm::createTextEditItem(%this, %label)
{
%textEdit = new GuiTextEditCtrl()
@@ -168,6 +184,70 @@ class = "EditorFormDropDown";
return %dropDown;
}
+// A color row matching the native inspector's ColorI editor (see
+// guiInspectorTypes.cc GuiInspectorTypeColor): a color-popup swatch plus four
+// numeric R/G/B/A edit boxes with labels, using the same color sub-profiles the
+// Profile Editor feeds its inspector. Returns the swatch; the four boxes are
+// hung on it as .redBox/.greenBox/.blueBox/.alphaBox. The caller wires the
+// swatch's Command and the boxes' AltCommand to its own apply logic and never
+// parses the color string itself (the swatch launders named<->numeric colors).
+//
+// %class is optional: pass a script class to give the popup extra behavior of
+// your own (the Profile Editor uses it to fill the popup's swatch row with the
+// selected theme's colors). Leave it out for a plain popup.
+function EditorForm::createColorItem(%this, %label, %class)
+{
+ // The row's name label (from addFormItem) sits along the top; the swatch and
+ // R/G/B/A boxes sit on the row below it.
+ %swatch = new GuiColorPopupCtrl()
+ {
+ class = %class;
+ Position = "10 24";
+ Extent = "30 30";
+ };
+ ThemeManager.setProfile(%swatch, "colorPickerProfile");
+ ThemeManager.setProfile(%swatch, "emptyProfile", "backgroundProfile");
+ ThemeManager.setProfile(%swatch, "colorPopupProfile", "popupProfile");
+ ThemeManager.setProfile(%swatch, "emptyProfile", "pickerProfile");
+ ThemeManager.setProfile(%swatch, "colorPickerSelectorProfile", "selectorProfile");
+ ThemeManager.setProfile(%swatch, "textEditProfile", "valueProfile");
+ ThemeManager.setProfile(%swatch, "tipProfile", "TooltipProfile");
+ %label.add(%swatch);
+
+ %swatch.redBox = %this.addColorChannel(%label, 48, 24, 48, "R");
+ %swatch.greenBox = %this.addColorChannel(%label, 100, 24, 48, "G");
+ %swatch.blueBox = %this.addColorChannel(%label, 152, 24, 48, "B");
+ %swatch.alphaBox = %this.addColorChannel(%label, 204, 24, 48, "A");
+
+ return %swatch;
+}
+
+// One R/G/B/A channel: a numeric edit box at (%x,%y) with a small label beneath.
+function EditorForm::addColorChannel(%this, %label, %x, %y, %width, %text)
+{
+ %box = new GuiTextEditCtrl()
+ {
+ Position = %x SPC %y;
+ Extent = %width SPC 28;
+ inputMode = "Number";
+ align = "center";
+ };
+ ThemeManager.setProfile(%box, "textEditProfile");
+ %label.add(%box);
+
+ %tag = new GuiControl()
+ {
+ Position = %x SPC (%y + 30);
+ Extent = %width SPC 16;
+ Text = %text;
+ align = "center";
+ };
+ ThemeManager.setProfile(%tag, "labelProfile");
+ %label.add(%tag);
+
+ return %box;
+}
+
function EditorForm::createCheckboxItem(%this, %label)
{
%box = new GuiCheckBoxCtrl()
@@ -192,3 +272,4 @@ class = "EditorFormDropDown";
{
%this.form.postEvent("DropDownSelect", %this);
}
+
diff --git a/editor/EditorCore/EditorIconButton.cs b/editor/EditorCore/EditorIconButton.cs
index f7c48db9c..96534fa34 100644
--- a/editor/EditorCore/EditorIconButton.cs
+++ b/editor/EditorCore/EditorIconButton.cs
@@ -1,19 +1,54 @@
+// The button, and the picture on it, are both sizeable now.
+//
+// They have to be said as fields rather than set afterwards, because this
+// forces its own extent here and the hover handlers below animate the icon to
+// numbers of their own -- so anything a caller set was undone by onAdd, and then
+// undone again by the first hover.
+//
+// iconSize is the size of the PICTURE, not of the sprite control holding it, and
+// keeping those two apart is the whole trick here.
+//
+// GuiSpriteCtrl::growTo animates mImageSize -- what is drawn -- and leaves the
+// control alone. The sprite then clamps its picture to its own content rect, so
+// the control has to be bigger than the biggest the picture will ever grow to or
+// the hover is clipped. Conflating the two gave a 36 pixel button a picture that
+// animated 32 down to 28: it looked like the icon exploded on hover and never
+// went back.
+//
+// Defaults reproduce the numbers this button has always used: a 24 button, a 20
+// sprite, a 16 picture that goes to 18 under the pointer.
+$EditorIconButton::defaultButtonSize = 24;
+$EditorIconButton::defaultIconSize = 16;
+
+// Room around the picture for it to grow into without being clamped.
+$EditorIconButton::iconSlack = 4;
+$EditorIconButton::hoverGrowth = 2;
+
function EditorIconButton::onAdd(%this)
{
%this.text = "";
- %this.extent = "24 24";
+
+ %buttonSize = (%this.buttonSize $= "") ? $EditorIconButton::defaultButtonSize : %this.buttonSize;
+ %this.iconSize = (%this.iconSize $= "") ? $EditorIconButton::defaultIconSize : %this.iconSize;
+
+ %this.extent = %buttonSize SPC %buttonSize;
+
+ // The container, sized to hold the picture at its hovered size with room to
+ // spare. Never equal to the picture: the sprite clamps to its content rect.
+ %holder = %this.iconSize + $EditorIconButton::iconSlack;
+
%this.icon = new GuiSpriteCtrl()
{
HorizSizing="center";
VertSizing="center";
- Extent = "20 20";
- minExtent = "20 20";
+ Extent = %holder SPC %holder;
+ minExtent = %holder SPC %holder;
Position = "0 0";
constrainProportions = "1";
fullSize = "0";
Image = "EditorCore:EditorIcons16";
- ImageSize = "16 16";
+ ImageSize = %this.iconSize SPC %this.iconSize;
ImageColor = ThemeManager.activeTheme.iconButtonProfile.FontColor;
Frame = %this.frame;
Tooltip = %this.Tooltip;
@@ -29,25 +64,49 @@
}
}
+// Every hover and press handler below is guarded on isActive(), because the
+// engine still delivers touch events to a disabled control: GuiControl::
+// findHitControl tests mVisible and mUseInput and never mActive. Without the
+// guard, moving the pointer over a disabled button repainted its icon in the
+// enabled hover color and the disabled look was gone until something else
+// forced it back -- the button read as clickable when it was not.
function EditorIconButton::onTouchEnter(%this)
{
+ if(!%this.isActive())
+ {
+ return;
+ }
%this.icon.fadeTo(ThemeManager.activeTheme.iconButtonProfile.fontColorHL, 200, "EaseInOut");
- %this.icon.growTo("18 18", 200, "EaseInOut");
+
+ %hover = %this.iconSize + $EditorIconButton::hoverGrowth;
+ %this.icon.growTo(%hover SPC %hover, 200, "EaseInOut");
}
function EditorIconButton::onTouchLeave(%this)
{
+ if(!%this.isActive())
+ {
+ return;
+ }
%this.icon.fadeTo(ThemeManager.activeTheme.iconButtonProfile.fontColor, 200, "EaseInOut");
- %this.icon.growTo("16 16", 200, "EaseInOut");
+ %this.icon.growTo(%this.iconSize SPC %this.iconSize, 200, "EaseInOut");
}
function EditorIconButton::onTouchDown(%this)
{
+ if(!%this.isActive())
+ {
+ return;
+ }
%this.icon.fadeTo(ThemeManager.activeTheme.iconButtonProfile.fontColorSL, 200, "EaseInOut");
}
function EditorIconButton::onTouchUp(%this)
{
+ if(!%this.isActive())
+ {
+ return;
+ }
%this.icon.fadeTo(ThemeManager.activeTheme.iconButtonProfile.fontColorHL, 200, "EaseInOut");
}
diff --git a/editor/EditorCore/EditorIcons.cs b/editor/EditorCore/EditorIcons.cs
new file mode 100644
index 000000000..34fbaaa56
--- /dev/null
+++ b/editor/EditorCore/EditorIcons.cs
@@ -0,0 +1,324 @@
+//-----------------------------------------------------------------------------
+// Frame indices into the editor icon sheets. GENERATED - do not hand-edit.
+//
+// EditorCore:editorIcons16/24/32/48 are the same 32x10 grid of the same 304
+// icons at four sizes, so one index names the same icon in every sheet and a
+// control can change size without changing its Frame.
+//
+// Names come from the source art. Four icons ship in both an outline and a
+// solid form under names that collapse together; the outline one carries an
+// _alt suffix. Three began with a digit, which is not a legal identifier, and
+// were turned around: grid_2x2, grid_3x3, grid_3x3_2.
+//
+// Indices are alphabetical, so inserting an icon reflows them. Reference an
+// icon through its constant and a regenerated sheet costs nothing; a raw
+// number will silently become the wrong picture.
+//-----------------------------------------------------------------------------
+
+$EditorIcon::air_signal = 0;
+$EditorIcon::align_bottom = 1;
+$EditorIcon::align_center = 2;
+$EditorIcon::align_just = 3;
+$EditorIcon::align_left = 4;
+$EditorIcon::align_middle = 5;
+$EditorIcon::align_right = 6;
+$EditorIcon::align_top = 7;
+$EditorIcon::app_window = 8;
+$EditorIcon::app_window_alt = 9;
+$EditorIcon::app_window_black = 10;
+$EditorIcon::app_window_black_alt = 11;
+$EditorIcon::app_window_cross = 12;
+$EditorIcon::app_window_cross_alt = 13;
+$EditorIcon::app_window_shell = 14;
+$EditorIcon::app_window_shell_alt = 15;
+$EditorIcon::arrow_bottom = 16;
+$EditorIcon::arrow_bottom_left = 17;
+$EditorIcon::arrow_bottom_rigth = 18;
+$EditorIcon::arrow_l = 19;
+$EditorIcon::arrow_left = 20;
+$EditorIcon::arrow_r = 21;
+$EditorIcon::arrow_right = 22;
+$EditorIcon::arrow_top = 23;
+$EditorIcon::arrow_top_left = 24;
+$EditorIcon::arrow_top_right = 25;
+$EditorIcon::arrow_two_head = 26;
+$EditorIcon::arrow_two_head_2 = 27;
+$EditorIcon::attention = 28;
+$EditorIcon::balance = 29;
+$EditorIcon::battery = 30;
+$EditorIcon::bell = 31;
+$EditorIcon::book = 32;
+$EditorIcon::book_side = 33;
+$EditorIcon::bookmark_1 = 34;
+$EditorIcon::bookmark_2 = 35;
+$EditorIcon::box = 36;
+$EditorIcon::br_down = 37;
+$EditorIcon::br_next = 38;
+$EditorIcon::br_prev = 39;
+$EditorIcon::br_up = 40;
+$EditorIcon::brackets = 41;
+$EditorIcon::browser = 42;
+$EditorIcon::brush = 43;
+$EditorIcon::bug = 44;
+$EditorIcon::burst = 45;
+$EditorIcon::calc = 46;
+$EditorIcon::calendar_1 = 47;
+$EditorIcon::calendar_2 = 48;
+$EditorIcon::cancel = 49;
+$EditorIcon::case = 50;
+$EditorIcon::cassette = 51;
+$EditorIcon::cc = 52;
+$EditorIcon::cert = 53;
+$EditorIcon::chart_bar = 54;
+$EditorIcon::chart_line = 55;
+$EditorIcon::chart_line_2 = 56;
+$EditorIcon::chart_pie = 57;
+$EditorIcon::chat_bubble_message_square = 58;
+$EditorIcon::checkbox_checked = 59;
+$EditorIcon::checkbox_unchecked = 60;
+$EditorIcon::checkmark = 61;
+$EditorIcon::clip = 62;
+$EditorIcon::clipboard_copy = 63;
+$EditorIcon::clipboard_cut = 64;
+$EditorIcon::clipboard_past = 65;
+$EditorIcon::clock = 66;
+$EditorIcon::cloud = 67;
+$EditorIcon::cloud_rain = 68;
+$EditorIcon::coffe_cup = 69;
+$EditorIcon::cog = 70;
+$EditorIcon::cogs = 71;
+$EditorIcon::comp = 72;
+$EditorIcon::compass = 73;
+$EditorIcon::connect = 74;
+$EditorIcon::contact = 75;
+$EditorIcon::contact_card = 76;
+$EditorIcon::cube = 77;
+$EditorIcon::cur_bp = 78;
+$EditorIcon::cur_dollar = 79;
+$EditorIcon::cur_euro = 80;
+$EditorIcon::cur_yen = 81;
+$EditorIcon::cursor_H_split = 82;
+$EditorIcon::cursor_V_split = 83;
+$EditorIcon::cursor_arrow = 84;
+$EditorIcon::cursor_drag_arrow = 85;
+$EditorIcon::cursor_drag_arrow_2 = 86;
+$EditorIcon::cursor_drag_hand = 87;
+$EditorIcon::cursor_hand = 88;
+$EditorIcon::dashboard = 89;
+$EditorIcon::db = 90;
+$EditorIcon::delete = 91;
+$EditorIcon::delete_server = 92;
+$EditorIcon::disconnected = 93;
+$EditorIcon::doc_delete = 94;
+$EditorIcon::doc_edit = 95;
+$EditorIcon::doc_empty = 96;
+$EditorIcon::doc_export = 97;
+$EditorIcon::doc_import = 98;
+$EditorIcon::doc_lines = 99;
+$EditorIcon::doc_lines_stright = 100;
+$EditorIcon::doc_minus = 101;
+$EditorIcon::doc_new = 102;
+$EditorIcon::doc_plus = 103;
+$EditorIcon::document = 104;
+$EditorIcon::download = 105;
+$EditorIcon::eject = 106;
+$EditorIcon::emotion_sad = 107;
+$EditorIcon::emotion_smile = 108;
+$EditorIcon::expand = 109;
+$EditorIcon::export = 110;
+$EditorIcon::eye = 111;
+$EditorIcon::eye_inv = 112;
+$EditorIcon::facebook = 113;
+$EditorIcon::fastforward_next = 114;
+$EditorIcon::fill = 115;
+$EditorIcon::filter = 116;
+$EditorIcon::fire = 117;
+$EditorIcon::flag = 118;
+$EditorIcon::flag_2 = 119;
+$EditorIcon::folder = 120;
+$EditorIcon::folder_arrow = 121;
+$EditorIcon::folder_delete = 122;
+$EditorIcon::folder_minus = 123;
+$EditorIcon::folder_open = 124;
+$EditorIcon::folder_plus = 125;
+$EditorIcon::font_bold = 126;
+$EditorIcon::font_italic = 127;
+$EditorIcon::font_size = 128;
+$EditorIcon::font_strokethrough = 129;
+$EditorIcon::font_underline = 130;
+$EditorIcon::game_pad = 131;
+$EditorIcon::glasses = 132;
+$EditorIcon::globe_1 = 133;
+$EditorIcon::globe_2 = 134;
+$EditorIcon::globe_3 = 135;
+$EditorIcon::google = 136;
+$EditorIcon::grid_2x2 = 137;
+$EditorIcon::grid_3x3 = 138;
+$EditorIcon::grid_3x3_2 = 139;
+$EditorIcon::hand_1 = 140;
+$EditorIcon::hand_2 = 141;
+$EditorIcon::hand_contra = 142;
+$EditorIcon::hand_pro = 143;
+$EditorIcon::hanger = 144;
+$EditorIcon::headphones = 145;
+$EditorIcon::heart = 146;
+$EditorIcon::heart_empty = 147;
+$EditorIcon::home = 148;
+$EditorIcon::image_text = 149;
+$EditorIcon::import = 150;
+$EditorIcon::inbox = 151;
+$EditorIcon::indent_decrease = 152;
+$EditorIcon::indent_increase = 153;
+$EditorIcon::info = 154;
+$EditorIcon::inject = 155;
+$EditorIcon::invisible_light = 156;
+$EditorIcon::invisible_revert = 157;
+$EditorIcon::iphone = 158;
+$EditorIcon::key = 159;
+$EditorIcon::layers_1 = 160;
+$EditorIcon::layers_2 = 161;
+$EditorIcon::lightbulb = 162;
+$EditorIcon::lighting = 163;
+$EditorIcon::link = 164;
+$EditorIcon::list_bullets = 165;
+$EditorIcon::list_num = 166;
+$EditorIcon::loading_throbber = 167;
+$EditorIcon::lock_open = 168;
+$EditorIcon::magic_wand = 169;
+$EditorIcon::magic_wand_2 = 170;
+$EditorIcon::mail = 171;
+$EditorIcon::mail_2 = 172;
+$EditorIcon::message_attention = 173;
+$EditorIcon::mic = 174;
+$EditorIcon::microphone = 175;
+$EditorIcon::money = 176;
+$EditorIcon::monitor = 177;
+$EditorIcon::movie = 178;
+$EditorIcon::music = 179;
+$EditorIcon::music_square = 180;
+$EditorIcon::net_comp = 181;
+$EditorIcon::network = 182;
+$EditorIcon::not_connected = 183;
+$EditorIcon::notepad = 184;
+$EditorIcon::notepad_2 = 185;
+$EditorIcon::off = 186;
+$EditorIcon::on = 187;
+$EditorIcon::on_off = 188;
+$EditorIcon::openid = 189;
+$EditorIcon::padlock_closed = 190;
+$EditorIcon::padlock_open = 191;
+$EditorIcon::page_layout = 192;
+$EditorIcon::paper_airplane = 193;
+$EditorIcon::paragraph = 194;
+$EditorIcon::pencil = 195;
+$EditorIcon::phone = 196;
+$EditorIcon::phone_1 = 197;
+$EditorIcon::phone_2 = 198;
+$EditorIcon::phone_touch = 199;
+$EditorIcon::photo = 200;
+$EditorIcon::picture = 201;
+$EditorIcon::pin = 202;
+$EditorIcon::pin_2 = 203;
+$EditorIcon::pin_map = 204;
+$EditorIcon::pin_map_down = 205;
+$EditorIcon::pin_map_left = 206;
+$EditorIcon::pin_map_right = 207;
+$EditorIcon::pin_map_top = 208;
+$EditorIcon::pin_sq_down = 209;
+$EditorIcon::pin_sq_left = 210;
+$EditorIcon::pin_sq_right = 211;
+$EditorIcon::pin_sq_top = 212;
+$EditorIcon::playback_ff = 213;
+$EditorIcon::playback_next = 214;
+$EditorIcon::playback_pause = 215;
+$EditorIcon::playback_play = 216;
+$EditorIcon::playback_prev = 217;
+$EditorIcon::playback_rec = 218;
+$EditorIcon::playback_reload = 219;
+$EditorIcon::playback_rew = 220;
+$EditorIcon::playback_stop = 221;
+$EditorIcon::podcast = 222;
+$EditorIcon::preso = 223;
+$EditorIcon::print = 224;
+$EditorIcon::push_pin = 225;
+$EditorIcon::redo = 226;
+$EditorIcon::refresh = 227;
+$EditorIcon::reload = 228;
+$EditorIcon::rewind_previous = 229;
+$EditorIcon::rnd_br_down = 230;
+$EditorIcon::rnd_br_first = 231;
+$EditorIcon::rnd_br_last = 232;
+$EditorIcon::rnd_br_next = 233;
+$EditorIcon::rnd_br_prev = 234;
+$EditorIcon::rnd_br_up = 235;
+$EditorIcon::round = 236;
+$EditorIcon::round_and_up = 237;
+$EditorIcon::round_arrow_left = 238;
+$EditorIcon::round_arrow_right = 239;
+$EditorIcon::round_checkmark = 240;
+$EditorIcon::round_delete = 241;
+$EditorIcon::round_minus = 242;
+$EditorIcon::round_plus = 243;
+$EditorIcon::rss = 244;
+$EditorIcon::rss_sq = 245;
+$EditorIcon::sand = 246;
+$EditorIcon::sat_dish = 247;
+$EditorIcon::save = 248;
+$EditorIcon::server = 249;
+$EditorIcon::shapes = 250;
+$EditorIcon::share = 251;
+$EditorIcon::share_2 = 252;
+$EditorIcon::shield = 253;
+$EditorIcon::shield_2 = 254;
+$EditorIcon::shop_cart = 255;
+$EditorIcon::shopping_bag = 256;
+$EditorIcon::shopping_bag_dollar = 257;
+$EditorIcon::sound_high = 258;
+$EditorIcon::sound_low = 259;
+$EditorIcon::sound_mute = 260;
+$EditorIcon::spechbubble = 261;
+$EditorIcon::spechbubble_2 = 262;
+$EditorIcon::spechbubble_sq = 263;
+$EditorIcon::spechbubble_sq_line = 264;
+$EditorIcon::sq_br_down = 265;
+$EditorIcon::sq_br_first = 266;
+$EditorIcon::sq_br_last = 267;
+$EditorIcon::sq_br_next = 268;
+$EditorIcon::sq_br_prev = 269;
+$EditorIcon::sq_br_up = 270;
+$EditorIcon::sq_down = 271;
+$EditorIcon::sq_minus = 272;
+$EditorIcon::sq_next = 273;
+$EditorIcon::sq_plus = 274;
+$EditorIcon::sq_prev = 275;
+$EditorIcon::sq_up = 276;
+$EditorIcon::square_shape = 277;
+$EditorIcon::stairs_down = 278;
+$EditorIcon::stairs_up = 279;
+$EditorIcon::star = 280;
+$EditorIcon::star_fav = 281;
+$EditorIcon::star_fav_empty = 282;
+$EditorIcon::stop_watch = 283;
+$EditorIcon::sun = 284;
+$EditorIcon::tag = 285;
+$EditorIcon::tape = 286;
+$EditorIcon::target = 287;
+$EditorIcon::text_curstor = 288;
+$EditorIcon::text_letter_t = 289;
+$EditorIcon::top_right_expand = 290;
+$EditorIcon::track = 291;
+$EditorIcon::trash = 292;
+$EditorIcon::twitter = 293;
+$EditorIcon::twitter_2 = 294;
+$EditorIcon::undo = 295;
+$EditorIcon::user = 296;
+$EditorIcon::users = 297;
+$EditorIcon::vault = 298;
+$EditorIcon::wallet = 299;
+$EditorIcon::wifi_router = 300;
+$EditorIcon::wireless_signal = 301;
+$EditorIcon::wrench = 302;
+$EditorIcon::wrench_plus = 303;
+$EditorIcon::wrench_plus_2 = 304;
+$EditorIcon::youtube = 305;
+$EditorIcon::zoom = 306;
diff --git a/editor/EditorCore/EditorMenu.cs b/editor/EditorCore/EditorMenu.cs
new file mode 100644
index 000000000..88328754e
--- /dev/null
+++ b/editor/EditorCore/EditorMenu.cs
@@ -0,0 +1,89 @@
+//-----------------------------------------------------------------------------
+// One menu on the shared bar, or one submenu inside another - they are the same
+// control and the same class, which is why nesting needs nothing extra.
+//
+// Never construct one of these directly. EditorMenuSet::addMenu makes the
+// top-level ones and addSubMenu makes the rest, and both exist to enforce the
+// one rule this control has:
+//
+// A MENU MUST BE CREATED EMPTY, PUT IN ITS PARENT, AND ONLY THEN FILLED.
+//
+// GuiMenuItemCtrl learns which bar it belongs to when it is added to one, and a
+// submenu learns it from its parent at the moment IT is added. Nothing ever
+// fills that in afterwards, so a tree built standalone and handed to the bar
+// whole leaves every descendant with no bar at all - and the first time such a
+// menu is opened, the engine dereferences it. The methods below add first and
+// return the thing to be filled, so the rule is the shape of the code rather
+// than something to remember.
+//-----------------------------------------------------------------------------
+
+// A command. %accelerator and %group are both optional.
+//
+// %group names a set of items that grey out together, and is the answer to menus
+// like the Gui Editor's Layout, where thirteen items are not thirteen questions
+// but one - is anything selected. The set flips a whole group in one call. Items
+// whose state is their own are greyed through the handle this returns instead.
+function EditorMenu::addItem(%this, %text, %command, %accelerator, %group)
+{
+ %item = new GuiMenuItemCtrl()
+ {
+ Text = %text;
+ Command = %command;
+ Accelerator = %accelerator;
+ };
+ %this.add(%item);
+
+ if(%group !$= "")
+ {
+ %this.set.addToGroup(%group, %item);
+ }
+
+ return %item;
+}
+
+// A menu inside this one. Returned empty, to be filled the same way this was.
+function EditorMenu::addSubMenu(%this, %text)
+{
+ %menu = new GuiMenuItemCtrl()
+ {
+ Class = "EditorMenu";
+ Text = %text;
+ set = %this.set;
+ };
+ %this.add(%menu);
+
+ return %menu;
+}
+
+// A checkable item. Command runs when it is switched on and %altCommand when it
+// is switched off, which is the engine's own split and not a convention we could
+// change here.
+function EditorMenu::addToggle(%this, %text, %command, %altCommand, %accelerator, %isOn)
+{
+ %item = new GuiMenuItemCtrl()
+ {
+ Text = %text;
+ Toggle = "1";
+ IsOn = %isOn;
+ Command = %command;
+ AltCommand = %altCommand;
+ Accelerator = %accelerator;
+ };
+ %this.add(%item);
+
+ return %item;
+}
+
+// The horizontal rule between groups of commands. The text IS the separator -
+// the engine decides an item is one by finding "-" there when it is added, which
+// is also why this has to go through the same add path as everything else.
+function EditorMenu::addSeparator(%this)
+{
+ %item = new GuiMenuItemCtrl()
+ {
+ Text = "-";
+ };
+ %this.add(%item);
+
+ return %item;
+}
diff --git a/editor/EditorCore/EditorMenuSet.cs b/editor/EditorCore/EditorMenuSet.cs
new file mode 100644
index 000000000..8a76c9be8
--- /dev/null
+++ b/editor/EditorCore/EditorMenuSet.cs
@@ -0,0 +1,138 @@
+//-----------------------------------------------------------------------------
+// The menus one editor puts on the shared bar.
+//
+// The bar is shared but the menus are not: File means "new Gui, open Gui, save
+// Gui" in the Gui Editor and "new asset, save asset, revert asset" in the Asset
+// Manager, and neither is a version of the other. So each editor owns a set,
+// builds it once, and hands it to EditorCore.setEditorMenus when it opens. Only
+// one set is ever on the bar; the rest are parked in a group of their own.
+//
+// Parking rather than greying is what makes the shortcuts right. The canvas
+// keeps one flat list of accelerators built by walking whatever is on show, and
+// it does not check whether an item is active before firing it - only whether
+// the item ITSELF is, never its menu. Greying File therefore left Ctrl+N still
+// running the Gui Editor's New Gui from inside the Asset Manager. An item that
+// is not in the tree is not in that list at all.
+//
+// Subclass this: set class to your own name and superclass to EditorMenuSet,
+// then define build(), which is called once with the bar ready to be added to,
+// and refresh(), which is called every time the set goes back on the bar and
+// whenever the editor's state changes underneath it.
+//-----------------------------------------------------------------------------
+
+function EditorMenuSet::init(%this)
+{
+ // Somewhere for the menus to live while another editor has the bar. A group
+ // rather than nothing at all: a control taken out of its parent with no new
+ // home is registered and unreachable, which is a leak wearing a disguise.
+ %this.parked = new SimGroup();
+ %this.menuCount = 0;
+
+ %this.build();
+
+ // Built into the bar, because that is the only place a menu can be built.
+ // Nobody has opened this editor yet, so take them straight back off.
+ %this.detach();
+}
+
+// Overridden by every subclass. Here to say so out loud when one forgets.
+function EditorMenuSet::build(%this)
+{
+ warn("EditorMenuSet::build - " @ %this.class @ " has no build method, so its menu set is empty.");
+}
+
+// Overridden by any subclass with something to grey out. Called on attach, so
+// the menus look new every time the editor is opened rather than carrying the
+// state they had when it was last closed.
+function EditorMenuSet::refresh(%this)
+{
+}
+
+// A top-level menu, empty, already on the bar and ready to be filled.
+function EditorMenuSet::addMenu(%this, %text)
+{
+ %menu = new GuiMenuItemCtrl()
+ {
+ Class = "EditorMenu";
+ Text = %text;
+ set = %this;
+ };
+ EditorCore.menuBar.add(%menu);
+
+ %this.menu[%this.menuCount] = %menu;
+ %this.menuCount++;
+
+ return %menu;
+}
+
+function EditorMenuSet::attach(%this)
+{
+ for(%i = 0; %i < %this.menuCount; %i++)
+ {
+ EditorCore.menuBar.add(%this.menu[%i]);
+ }
+
+ %this.refresh();
+}
+
+function EditorMenuSet::detach(%this)
+{
+ for(%i = 0; %i < %this.menuCount; %i++)
+ {
+ %this.parked.add(%this.menu[%i]);
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Groups: items that grey out together.
+//-----------------------------------------------------------------------------
+
+function EditorMenuSet::addToGroup(%this, %group, %item)
+{
+ // A group is named on the spot by whoever adds to it, so the first add finds
+ // no count at all. Left as the empty string it reads as zero in the addition
+ // below but writes the item to the index "" rather than 0, and the read back
+ // in setGroupActive finds nothing there - the first item of every group would
+ // silently never grey.
+ %count = %this.groupCount[%group];
+ if(%count $= "")
+ {
+ %count = 0;
+ }
+
+ %this.groupItem[%group, %count] = %item;
+ %this.groupCount[%group] = %count + 1;
+}
+
+function EditorMenuSet::setGroupActive(%this, %group, %active)
+{
+ %count = %this.groupCount[%group];
+ for(%i = 0; %i < %count; %i++)
+ {
+ %this.groupItem[%group, %i].setActive(%active);
+ }
+}
+
+function EditorMenuSet::onRemove(%this)
+{
+ // If we are the set on show, come off it properly first: the bar's own
+ // bookkeeping is repaired by the remove, and EditorCore stops pointing at
+ // something about to stop existing.
+ if(isObject(EditorCore) && EditorCore.activeMenus == %this)
+ {
+ EditorCore.setEditorMenus("");
+ }
+
+ for(%i = 0; %i < %this.menuCount; %i++)
+ {
+ if(isObject(%this.menu[%i]))
+ {
+ %this.menu[%i].delete();
+ }
+ }
+
+ if(isObject(%this.parked))
+ {
+ %this.parked.delete();
+ }
+}
diff --git a/editor/EditorCore/EditorPreferences.cs b/editor/EditorCore/EditorPreferences.cs
new file mode 100644
index 000000000..754b641ab
--- /dev/null
+++ b/editor/EditorCore/EditorPreferences.cs
@@ -0,0 +1,120 @@
+//-----------------------------------------------------------------------------
+// The editor's memory between runs.
+//
+// Until now the editor had none: every choice a person made -- which theme, how
+// a list was arranged -- lasted exactly as long as the process. The things worth
+// remembering are the ones a person sets once and expects to stay set, and the
+// Asset Library's view mode and sort order are the first two.
+//
+// Deliberately NOT $pref:: globals. Those are the engine's own settings, they
+// are re-declared on every boot by defaultPreferences.cs, and script has no
+// setVariable() to write one by name -- only eval(). Dynamic fields on a
+// SimObject give the same key/value store with getFieldValue/setFieldValue and
+// no string-built code.
+//
+// The file is written to the platform's per-user application data folder
+// (getPrefsPath), never into the repository or a project, because it describes
+// the person rather than the work.
+//-----------------------------------------------------------------------------
+
+function EditorPreferences::onAdd(%this)
+{
+ %this.path = getPrefsPath("editorPreferences.taml");
+ %this.load();
+}
+
+// A value the editor has never been told is not an error -- it is the first run.
+function EditorPreferences::get(%this, %key, %fallback)
+{
+ %value = %this.getFieldValue(%key);
+
+ return (%value $= "") ? %fallback : %value;
+}
+
+// Written through immediately. There is no "apply" step anywhere in the editor,
+// and a preferences file that only survives a clean exit is one that never
+// survives the interesting exits.
+function EditorPreferences::set(%this, %key, %value)
+{
+ if(%this.getFieldValue(%key) $= %value)
+ {
+ return;
+ }
+
+ %this.setFieldValue(%key, %value);
+ %this.save();
+}
+
+//-----------------------------------------------------------------------------
+// The file.
+//-----------------------------------------------------------------------------
+
+function EditorPreferences::load(%this)
+{
+ if(!%this.fileExists(%this.path))
+ {
+ return;
+ }
+
+ %stored = TamlRead(%this.path);
+ if(!isObject(%stored))
+ {
+ warn("EditorPreferences: could not read " @ %this.path);
+ return;
+ }
+
+ // getDynamicField answers with the field's NAME and nothing else, despite the
+ // "myField myValue" its own doc comment implies -- see simObject_ScriptBinding.h,
+ // which sprintfs entry->slotName alone. The value has to be asked for separately.
+ %count = %stored.getDynamicFieldCount();
+ for(%i = 0; %i < %count; %i++)
+ {
+ %name = %stored.getDynamicField(%i);
+ %this.setFieldValue(%name, %stored.getFieldValue(%name));
+ }
+
+ %stored.delete();
+}
+
+// Saved as a plain ScriptObject rather than as this object, because "class" is a
+// persistent field: writing this one out would record class="EditorPreferences",
+// and reading it back would construct a second one, whose onAdd would load the
+// file again.
+function EditorPreferences::save(%this)
+{
+ %store = new ScriptObject();
+
+ %count = %this.getDynamicFieldCount();
+ for(%i = 0; %i < %count; %i++)
+ {
+ %name = %this.getDynamicField(%i);
+
+ // path is this object's own bookkeeping, not something to remember.
+ if(%name $= "path")
+ {
+ continue;
+ }
+
+ %store.setFieldValue(%name, %this.getFieldValue(%name));
+ }
+
+ // The per-user folder does not exist until something makes it, and TamlWrite
+ // fails by logging rather than by telling the caller.
+ createPath(%this.path);
+
+ TamlWrite(%store, %this.path);
+ %store.delete();
+}
+
+// isFile() answers from the ResourceManager's dictionary rather than from the
+// disk, so it can say yes to a path nothing ever wrote and no to one written
+// this session. Opening the file is the only answer that is about the file.
+function EditorPreferences::fileExists(%this, %path)
+{
+ %file = new FileObject();
+ %found = %file.openForRead(%path);
+ %file.close();
+ %file.delete();
+
+ return %found;
+}
diff --git a/editor/EditorCore/EditorToggleIcon.cs b/editor/EditorCore/EditorToggleIcon.cs
new file mode 100644
index 000000000..d623ca8d2
--- /dev/null
+++ b/editor/EditorCore/EditorToggleIcon.cs
@@ -0,0 +1,190 @@
+
+//-----------------------------------------------------------------------------
+// An icon that stays pressed: a toggle button, built from a GuiCheckBoxCtrl.
+//
+// A checkbox already IS a toggle. GuiCheckBoxCtrl::onAction flips mStateOn and
+// then runs the Command, and it refuses to do either while the control is
+// inactive -- which is the whole contract, and more than GuiButtonCtrl offers.
+// What makes it look like a checkbox rather than a button is only its layout:
+// a small box beside a caption. Take the caption away and let the box have the
+// whole control and the same class renders as a button that holds its state:
+//
+// boxOffset "0 0" the box starts at the content rect's corner
+// boxExtent the extent GuiCheckBoxCtrl clamps it to the content rect
+// text "" and textExtent "0 0", so no caption is drawn
+//
+// GuiCheckBoxCtrl::renderInnerControl then draws a universal rect for the whole
+// control in the current state, and an icon sitting on top (UseInput off, so it
+// never eats the click) says what the button is for.
+//
+// The icon carries the on/off reading rather than the background, because a
+// profile cannot express it without art: GuiControlProfile::getFillColor maps
+// NormalStateOn to mFillColor, the same color as NormalState, so the four "On"
+// states differ only when the profile renders from an image or bitmap. Bright
+// icon means on, dim means off, and a second frame can say it again where there
+// is art for one (a closed and an open padlock).
+//
+// There is deliberately no hover animation. EditorIconButton has one and it is
+// where its disabled state went: the engine delivers touch events to inactive
+// controls (findHitControl checks mVisible and mUseInput, never mActive), so
+// the hover handler repainted over the disabled color. Here the state is
+// computed in one place, refresh(), and nothing else writes the icon.
+//
+// The creator sets frameOn, frameOff, tipOn, tipOff, owner and toggleName
+// inline. Clicks arrive at owner.onToggleIconChanged(%this).
+//-----------------------------------------------------------------------------
+
+function EditorToggleIcon::onAdd(%this)
+{
+ // Field assignment, not setBoxOffset/setBoxExtent: those bindings document
+ // one argument and read two (argv[2] and argv[3]), so a single "0 0" string
+ // leaves whatever happened to be in the second slot. The fields themselves
+ // are TypePoint2I and parse the pair correctly.
+ %extent = %this.getExtent();
+
+ %this.setText("");
+ %this.boxOffset = "0 0";
+ %this.boxExtent = %extent;
+ %this.textOffset = "0 0";
+ %this.textExtent = "0 0";
+
+ // iconSize is the PICTURE, and the sprite holding it is deliberately bigger --
+ // the same arrangement, and the same reason, as EditorIconButton: a sprite
+ // clamps its picture to its own content rect, so a control the same size as
+ // the artwork loses a pixel or two of it to the profile's insets. The two
+ // widgets are frequently sat next to each other and have to agree.
+ %iconSize = (%this.iconSize $= "") ? 16 : %this.iconSize;
+ %holder = %iconSize + 4;
+
+ %this.icon = new GuiSpriteCtrl()
+ {
+ HorizSizing = "center";
+ VertSizing = "center";
+ Extent = %holder SPC %holder;
+ MinExtent = %holder SPC %holder;
+ Position = "0 0";
+ Image = "EditorCore:EditorIcons16";
+ ImageSize = %iconSize SPC %iconSize;
+ constrainProportions = "1";
+ fullSize = "0";
+ Frame = %this.frameOff;
+ UseInput = false;
+ };
+ ThemeManager.setProfile(%this.icon, "spriteProfile");
+ %this.add(%this.icon);
+
+ // ThemeManager repaints what it was given a profile for, and the icon's tint
+ // is not that: refresh() COPIES a color off the profile onto the sprite, so
+ // swapping the profile underneath leaves the copy behind. Without this the
+ // button's background changed theme and the picture on it did not, until
+ // something happened to call refresh() again -- which for a segmented row
+ // meant the icons corrected themselves the moment you clicked one.
+ %this.startListening(ThemeManager);
+
+ %this.Command = %this.getID() @ ".onToggled();";
+ %this.refresh();
+}
+
+function EditorToggleIcon::onThemeChange(%this, %theme)
+{
+ %this.refresh();
+}
+
+// GuiCheckBoxCtrl has already flipped mStateOn by the time the Command runs, so
+// the owner is told what the value became rather than being asked to work it
+// out.
+function EditorToggleIcon::onToggled(%this)
+{
+ %this.refresh();
+
+ if(isObject(%this.owner))
+ {
+ %this.owner.onToggleIconChanged(%this);
+ }
+}
+
+// Set the state without telling the owner, for loading a value in.
+function EditorToggleIcon::setValue(%this, %on)
+{
+ %this.setStateOn(%on);
+ %this.refresh();
+}
+
+function EditorToggleIcon::getValue(%this)
+{
+ return %this.getStateOn();
+}
+
+// The single place the icon's look is decided: which frame, which tooltip, and
+// what tints it.
+function EditorToggleIcon::refresh(%this)
+{
+ %on = %this.getStateOn();
+
+ // frameOn is optional. Where there is only one icon for the idea, the tint
+ // alone carries the state.
+ %frame = (%on && %this.frameOn !$= "") ? %this.frameOn : %this.frameOff;
+
+ // So is frameOff. A button with no icon at all is a deliberate shape: it is
+ // how EditorChoiceRow spells the "unset" end of a segmented control,
+ // where the absence of a picture is the meaning.
+ %this.icon.setVisible(%frame !$= "");
+ if(%frame !$= "")
+ {
+ %this.icon.setImageFrame(%frame);
+ }
+
+ %this.icon.setImageColor(%this.getIconTint(%on));
+
+ %this.Tooltip = %this.buildTip(%on);
+}
+
+// What the icon is tinted with, split out so a toggle whose color is part of its
+// meaning can answer differently. The two profile inks below say "on" and "off"
+// in the editor's own palette, which is right for a switch but not for a button
+// that stands for a color -- see AssetParticleChannelToggle.
+function EditorToggleIcon::getIconTint(%this, %on)
+{
+ %profile = ThemeManager.activeTheme.iconButtonProfile;
+
+ if(!%this.isActive())
+ {
+ return %profile.fontColorNA;
+ }
+
+ return %on ? %profile.fontColorHL : %profile.fontColor;
+}
+
+// Two lines, now that a control's text can hold a line break: what this is and
+// which way it is set, then what that means.
+//
+// Visible - On
+// Draws when the game runs...
+//
+// The first line only appears for a toggle that names itself. A segmented row's
+// buttons are choices rather than switches -- "Centre text - On" would be a
+// worse caption than "Centre text" -- so those pass no label and keep the one
+// line they had.
+function EditorToggleIcon::buildTip(%this, %on)
+{
+ %tip = %on ? %this.tipOn : %this.tipOff;
+
+ if(%this.toggleLabel $= "")
+ {
+ return %tip;
+ }
+
+ %heading = %this.toggleLabel @ " - " @ (%on ? "On" : "Off");
+ return (%tip $= "") ? %heading : (%heading NL %tip);
+}
+
+// setActive does not repaint on its own, and the disabled tint is ours to draw.
+function EditorToggleIcon::onActive(%this)
+{
+ %this.refresh();
+}
+
+function EditorToggleIcon::onInactive(%this)
+{
+ %this.refresh();
+}
diff --git a/editor/EditorCore/EditorTransportBar.cs b/editor/EditorCore/EditorTransportBar.cs
new file mode 100644
index 000000000..9b97c7df2
--- /dev/null
+++ b/editor/EditorCore/EditorTransportBar.cs
@@ -0,0 +1,154 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2013 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+
+//-----------------------------------------------------------------------------
+// The chrome a transport bar is made of: sized icon buttons, stateful toggles,
+// and the gaps between them. What each button DOES belongs to the subclass; this
+// knows only how one should look and how to put it on the chain.
+//
+// A bar of these sits as an overlay on a preview rather than in a strip of its
+// own -- which is where the audio play button already sat, and proof that an
+// overlay there receives clicks over a SceneWindow. It costs no layout and takes
+// no room from the art.
+//
+// Not built from EditorButtonBar: that makes EditorIconButtons, which are
+// momentary, and a transport usually has at least one button that has to SHOW a
+// state.
+//
+// A subclass is a GuiChainCtrl with class = its own name and superclass =
+// "EditorTransportBar". onAdd does not chain in TorqueScript, so the subclass's
+// onAdd calls %this.init() first and then adds its buttons.
+//
+// Used by AssetAnimationTransportBar and AssetParticleTransportBar.
+//-----------------------------------------------------------------------------
+
+$EditorTransportBar::buttonSize = 24;
+$EditorTransportBar::playSize = 36;
+$EditorTransportBar::spacing = 4;
+$EditorTransportBar::gap = 16;
+
+function EditorTransportBar::init(%this)
+{
+ ThemeManager.setProfile(%this, "emptyProfile");
+}
+
+// How much bigger a toggle has to be than a push button to LOOK the same size.
+//
+// They draw differently. A GuiButtonCtrl paints its chrome across the whole
+// control less its margins; a GuiCheckBoxCtrl paints a box that
+// GuiCheckBoxCtrl::onRender clamps into the CONTENT rect -- inside the borders
+// and padding as well. iconButtonProfile has a 2 pixel border on all four sides,
+// so a 24 pixel toggle drew a 20 pixel box beside a 24 pixel button, and no
+// amount of boxExtent fixed it: the clamp will not let the box out.
+//
+// So the toggle is built that much larger and its box comes out the right size.
+// Read from the profile rather than written as 4, because a theme is free to
+// give the button a different border.
+function EditorTransportBar::chromeInset(%this)
+{
+ %profile = ThemeManager.activeTheme.iconButtonProfile;
+
+ return (%profile.borderLeft.border + %profile.borderRight.border) SPC
+ (%profile.borderTop.border + %profile.borderBottom.border);
+}
+
+// A button that shows which of two states it is in, and reports the change to
+// the bar as onToggleIconChanged.
+function EditorTransportBar::addToggle(%this, %name, %frameOn, %frameOff, %tipOn, %tipOff)
+{
+ %size = $EditorTransportBar::buttonSize;
+ %inset = %this.chromeInset();
+
+ %button = new GuiCheckBoxCtrl()
+ {
+ class = "EditorToggleIcon";
+ Position = "0 0";
+ VertSizing = "center";
+ Extent = (%size + getWord(%inset, 0)) SPC (%size + getWord(%inset, 1));
+ frameOn = %frameOn;
+ frameOff = %frameOff;
+ tipOn = %tipOn;
+ tipOff = %tipOff;
+ toggleName = %name;
+ owner = %this;
+ };
+ ThemeManager.setProfile(%button, "iconButtonProfile");
+ ThemeManager.setProfile(%button, "tipProfile", "TooltipProfile");
+ %this.add(%button);
+
+ return %button;
+}
+
+function EditorTransportBar::addButton(%this, %method, %frame, %tooltip, %size)
+{
+ %size = (%size $= "") ? $EditorTransportBar::buttonSize : %size;
+
+ // Said in the block, not set afterwards. EditorIconButton forces its own
+ // extent in onAdd and its hover handlers animate the icon to sizes of their
+ // own, so a resize applied after the add survived exactly until the pointer
+ // first crossed it -- and the chain had already sized itself around the
+ // smaller button by then, which is what clipped the big one.
+ %button = new GuiButtonCtrl()
+ {
+ class = "EditorIconButton";
+ Position = "0 0";
+ VertSizing = "center";
+ // buttonSize only. The icon is deliberately left at its default, so the
+ // big play button is a bigger BUTTON with the same picture on it as the
+ // rest -- which is what makes it easy to find without making it look like
+ // a different kind of control.
+ buttonSize = %size;
+ Frame = %frame;
+ Command = %this.getId() @ "." @ %method @ "();";
+ Tooltip = %tooltip;
+ };
+ ThemeManager.setProfile(%button, "iconButtonProfile");
+ ThemeManager.setProfile(%button, "tipProfile", "TooltipProfile");
+ %this.add(%button);
+
+ return %button;
+}
+
+// A chain lays out what it can see, so an empty control is how a gap is spelled
+// -- there is no spacing-before on a child.
+function EditorTransportBar::addSpacer(%this, %width)
+{
+ %spacer = new GuiControl()
+ {
+ Position = "0 0";
+ Extent = %width SPC $EditorTransportBar::buttonSize;
+ UseInput = false;
+ };
+ ThemeManager.setProfile(%spacer, "emptyProfile");
+ %this.add(%spacer);
+
+ return %spacer;
+}
+
+// A chain lays out only the children it can see, and nothing re-lays it out when
+// one is hidden -- so swapping two buttons over is a resize away from leaving a
+// hole where the other one was. Every subclass that hides a button needs this.
+function EditorTransportBar::relayout(%this)
+{
+ %this.resize(getWord(%this.getPosition(), 0), getWord(%this.getPosition(), 1),
+ getWord(%this.getExtent(), 0), getWord(%this.getExtent(), 1));
+}
diff --git a/editor/EditorCore/ModuleStamper.cs b/editor/EditorCore/ModuleStamper.cs
new file mode 100644
index 000000000..c0ea9e4d2
--- /dev/null
+++ b/editor/EditorCore/ModuleStamper.cs
@@ -0,0 +1,226 @@
+//-----------------------------------------------------------------------------
+// Renaming a module is not renaming its ModuleId.
+//
+// The engine calls ::, so the id in module.taml and
+// the namespace in the module's script are the same name written twice. Asset
+// ids are that name a third time: ":", in script and in
+// every taml file that references an asset. Change only the one in module.taml
+// -- which is what all three of the editor's rename paths used to do -- and the
+// module still loads, still reports the new name, and silently does nothing:
+// create never fires, and its assets resolve to a module that no longer exists.
+//
+// So a rename is a pass over the module's own source. Only .cs and .taml files
+// are read; art and audio are left alone.
+//
+// The engine has half of this already: ModuleManager::copyModule runs a
+// TamlModuleIdUpdateVisitor when the source and target ids differ. It is not
+// enough on its own -- the visitor is root-only, so an asset id on a nested
+// element is missed, it cannot touch .cs at all, and it renames module.taml to
+// .module.taml, a name every editor script that opens a module
+// definition does not expect. The copy is therefore made under the template's
+// own id and the rename happens here, on the copy.
+//-----------------------------------------------------------------------------
+
+// The extensions worth reading. Everything else in a module is content.
+function ModuleStamper::onAdd(%this)
+{
+ %this.textExtensions = ".cs" TAB ".taml";
+}
+
+// Template modules carry two dynamic fields the engine knows nothing about:
+// Template marks a module as something to stamp out rather than install, and
+// DisplayName is what to call it in a picker. Neither is a ModuleDefinition
+// field, so both ride along as taml attributes the way AppCore's Project and
+// ProjectDescription do. The names live here so the dialogs that read them and
+// the code that strips them off a copy agree on the spelling.
+function ModuleStamper::displayName(%this, %module)
+{
+ if(%module.DisplayName !$= "")
+ {
+ return %module.DisplayName;
+ }
+
+ return %module.ModuleID;
+}
+
+// A stamped copy is a module in its own right, not a template, so the markers
+// that made it stampable do not belong on it.
+function ModuleStamper::clearTemplateMarkers(%this, %definition)
+{
+ %definition.Template = "";
+ %definition.DisplayName = "";
+}
+
+// %modulePath is the module's folder; %oldId and %newId are module ids. Returns
+// true if the walk completed, whether or not any file needed changing.
+function ModuleStamper::renameInPlace(%this, %modulePath, %oldId, %newId)
+{
+ if(%oldId $= "" || %newId $= "" || %oldId $= %newId)
+ {
+ return true;
+ }
+
+ if(!isDirectory(%modulePath))
+ {
+ error("ModuleStamper: no module at " @ %modulePath);
+ return false;
+ }
+
+ return %this.rewriteTree(%modulePath, %oldId, %newId);
+}
+
+// One level at a time rather than getDirectoryList's depth argument: that
+// binding passes noBasePath, so the base folder is never in the list and the
+// returned names are relative to it. Recursing by hand keeps the full path in
+// hand at every level.
+function ModuleStamper::rewriteTree(%this, %dir, %old, %new)
+{
+ %files = getFileList(%dir);
+ for(%i = 0; %i < getFieldCount(%files); %i++)
+ {
+ %file = getField(%files, %i);
+ if(%this.isTextFile(%file))
+ {
+ %this.rewriteFile(pathConcat(%dir, %file), %old, %new);
+ }
+ }
+
+ %dirs = getDirectoryList(%dir);
+ for(%i = 0; %i < getFieldCount(%dirs); %i++)
+ {
+ %sub = getField(%dirs, %i);
+ if(%sub $= "" || %sub $= "." || %sub $= "..")
+ {
+ continue;
+ }
+
+ %this.rewriteTree(pathConcat(%dir, %sub), %old, %new);
+ }
+
+ return true;
+}
+
+function ModuleStamper::isTextFile(%this, %file)
+{
+ %ext = fileExt(%file);
+ for(%i = 0; %i < getFieldCount(%this.textExtensions); %i++)
+ {
+ if(%ext $= getField(%this.textExtensions, %i))
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+// Read the whole file before writing any of it. FileObject reads through the
+// ResourceManager, which caches a file's size the first time it is asked for
+// one, so a file read back after being written in the same session can be read
+// at its old length.
+function ModuleStamper::rewriteFile(%this, %path, %old, %new)
+{
+ %file = new FileObject();
+ if(!%file.openForRead(%path))
+ {
+ %file.delete();
+ error("ModuleStamper: could not read " @ %path);
+ return false;
+ }
+
+ %count = 0;
+ %changed = false;
+ while(!%file.isEOF())
+ {
+ %line = %file.readLine();
+ %rewritten = %this.replaceToken(%line, %old, %new);
+ if(%rewritten !$= %line)
+ {
+ %changed = true;
+ }
+
+ %out[%count] = %rewritten;
+ %count++;
+ }
+ %file.close();
+
+ if(!%changed)
+ {
+ %file.delete();
+ return true;
+ }
+
+ if(!%file.openForWrite(%path))
+ {
+ %file.delete();
+ error("ModuleStamper: could not write " @ %path);
+ return false;
+ }
+
+ for(%i = 0; %i < %count; %i++)
+ {
+ %file.writeLine(%out[%i]);
+ }
+ %file.close();
+ %file.delete();
+
+ return true;
+}
+
+// A whole-word replace. strreplace would do for "BlankGame", but this also runs
+// over modules a person named themselves, where the old id can be a substring
+// of an ordinary word in a comment or a string. A match counts only where the
+// characters on either side cannot be part of an identifier -- which the three
+// forms that matter all satisfy: ModuleId="Name", Name::create, "Name:asset".
+function ModuleStamper::replaceToken(%this, %line, %old, %new)
+{
+ %length = strlen(%old);
+ if(%length == 0)
+ {
+ return %line;
+ }
+
+ %result = "";
+ %from = 0;
+
+ while(true)
+ {
+ %at = strpos(%line, %old, %from);
+ if(%at == -1)
+ {
+ return %result @ getSubStr(%line, %from, strlen(%line) - %from);
+ }
+
+ %before = (%at == 0) ? "" : getSubStr(%line, %at - 1, 1);
+ %after = getSubStr(%line, %at + %length, 1);
+
+ %result = %result @ getSubStr(%line, %from, %at - %from);
+ if(%this.isIdentifierChar(%before) || %this.isIdentifierChar(%after))
+ {
+ %result = %result @ %old;
+ }
+ else
+ {
+ %result = %result @ %new;
+ }
+
+ %from = %at + %length;
+ }
+}
+
+function ModuleStamper::isIdentifierChar(%this, %char)
+{
+ if(%char $= "")
+ {
+ return false;
+ }
+
+ if(%char $= "_")
+ {
+ return true;
+ }
+
+ // $= is case insensitive, so the lower case half of the alphabet answers for
+ // both.
+ return strpos("abcdefghijklmnopqrstuvwxyz0123456789", strlwr(%char)) != -1;
+}
diff --git a/editor/EditorCore/Themes/BaseTheme/BaseTheme.cs b/editor/EditorCore/Themes/BaseTheme/BaseTheme.cs
index 93ed1162d..fdccc061f 100644
--- a/editor/EditorCore/Themes/BaseTheme/BaseTheme.cs
+++ b/editor/EditorCore/Themes/BaseTheme/BaseTheme.cs
@@ -63,6 +63,7 @@
%this.makeDropDownProfile();
%this.makeWindowProfile();
%this.makeListBoxProfile();
+ %this.makeFrameGridProfile();
%this.makeTreeViewProfile();
%this.makeGraphProfile();
%this.makeTextDisplayProfile();
@@ -80,7 +81,7 @@
%this.font[1] = "raleway";//Most common font
%this.font[2] = "black ops one";//Title fontType
%this.font[3] = "fira code semibold";//Code and console font
- %this.fontDirectory = expandPath("^EditorCore/Themes/BaseTheme/Fonts");
+ %this.fontDirectory = expandPath("^EditorCore/Themes/BaseTheme/fonts");
%this.fontSize = 20;
%this.color1 = "10 10 10 255";//Most commonly used for backgrounds
@@ -1156,6 +1157,20 @@
borderDefault = %labelBorder;
};
+ %this.overrideLabelProfile = new GuiControlProfile()
+ {
+ fillColor = "0 0 0 0";
+
+ fontType = %this.font[2];
+ fontDirectory = %this.fontDirectory;
+ fontSize = %this.fontSize - 2;
+ fontColor = %this.color5;
+ align = "left";
+ vAlign = "top";
+
+ borderDefault = %labelBorder;
+ };
+
%textBorderV = new GuiBorderProfile()
{
padding = 2;
@@ -1872,6 +1887,66 @@
};
}
+//-----------------------------------------------------------------------------
+// The animation editor's frame grids -- the palette of an image's frames and the
+// timeline of the frames an animation plays.
+//
+// They had been borrowing listBoxProfile, because it is the one with canKeyFocus,
+// and three of its fields mean something different there to what they have to
+// mean here:
+//
+// fillColorHL a list row's hover is deliberately a whisper -- color1 nudged
+// by 4 -- which over a picture is no change at all
+// fontColorSL is the ink drawn ON a selected row, so it is dark against the
+// accent. Used as a playhead bar it vanished completely
+// fillColorSL the accent, which reads well as a selection but leaves nothing
+// distinct for the playhead sitting on top of it
+//
+// So the grids get their own, and what each field is for is written down here
+// because there is no other way to know from the far end:
+//
+// fillColor the strip behind the cells
+// fillColorHL the cell under the pointer -- a real, visible change
+// fillColorSL the picked cell: a raised surface, NOT the accent, so that
+// the playhead stays legible on top of it
+// fillColorNA the cell a drag would discard if released now
+// fontColor the frame numbers, quiet enough to read art through
+// fontColorHL the insertion caret, which is transient and wants to be seen
+// fontColorSL the playhead. The accent, and the only thing here that has to
+// be findable at a glance while the animation runs
+//-----------------------------------------------------------------------------
+function BaseTheme::makeFrameGridProfile(%this)
+{
+ %this.frameGridProfile = new GuiControlProfile()
+ {
+ fillColor = %this.adjustValue(%this.color1, 2);
+ fillColorHL = %this.color2;
+ fillColorSL = %this.color3;
+ fillColorNA = %this.setAlpha(%this.color3, 110);
+
+ fontType = %this.font[3];
+ fontDirectory = %this.fontDirectory;
+ fontSize = %this.fontSize;
+ fontColor = %this.setAlpha(%this.color4, 200);
+ fontColorHL = %this.color4;
+ fontColorSL = %this.color5;
+
+ // Errors, in the sense the console profile uses this slot for. The
+ // timeline outlines a frame naming a cell the image no longer has, and this
+ // is the color of that outline and of its label. The four FILL colors are
+ // all spoken for -- background, hover, selected, and about-to-be-discarded
+ // during a drag -- which is why a missing frame is a border rather than a
+ // wash.
+ fontColorNA = "255 0 0 255";
+
+ // The Delete key only reaches a control that can hold focus, and the
+ // timeline's whole keyboard depends on it.
+ canKeyFocus = true;
+
+ borderDefault = %this.emptyBorder;
+ };
+}
+
function BaseTheme::makeTreeViewProfile(%this)
{
%this.treeViewProfile = new GuiControlProfile ()
@@ -2010,6 +2085,19 @@
borderRight = %spacerBorder;
borderBottom = %spacerBorder;
};
+
+ %this.impactProfile = new GuiControlProfile()
+ {
+ fillColor = %this.color5;
+ fontType = %this.font[3];
+ fontDirectory = %this.fontDirectory;
+ fontSize = 16;
+ fontColor = %this.color1;
+ align = "center";
+ vAlign = "middle";
+
+ borderDefault = %this.emptyBorder;
+ };
}
function BaseTheme::makeGuiEditorProfile(%this)
diff --git a/editor/EditorCore/Themes/ForestRobe/ForestRobeTheme.cs b/editor/EditorCore/Themes/ForestRobe/ForestRobeTheme.cs
index 8a0cd9b1b..71a5d43e2 100644
--- a/editor/EditorCore/Themes/ForestRobe/ForestRobeTheme.cs
+++ b/editor/EditorCore/Themes/ForestRobe/ForestRobeTheme.cs
@@ -6,7 +6,7 @@
%this.font[1] = "raleway";//Most common font
%this.font[2] = "cinzel decorative bold";//Title fontType
%this.font[3] = "fira code semibold";//Code and console font
- %this.fontDirectory = expandPath("^EditorCore/Themes/ForestRobe/Fonts");
+ %this.fontDirectory = expandPath("^EditorCore/Themes/ForestRobe/fonts");
%this.fontSize = 20;
%this.color1 = "43 53 66 255";
diff --git a/editor/EditorCore/Themes/LabCoat/LabCoatTheme.cs b/editor/EditorCore/Themes/LabCoat/LabCoatTheme.cs
index 38675b7ce..3657f43de 100644
--- a/editor/EditorCore/Themes/LabCoat/LabCoatTheme.cs
+++ b/editor/EditorCore/Themes/LabCoat/LabCoatTheme.cs
@@ -6,7 +6,7 @@
%this.font[1] = "roboto";//Most common font
%this.font[2] = "zen dots";//Title fontType
%this.font[3] = "share tech mono";//Code and console font
- %this.fontDirectory = expandPath("^EditorCore/Themes/LabCoat/Fonts");
+ %this.fontDirectory = expandPath("^EditorCore/Themes/LabCoat/fonts");
%this.fontSize = 20;
%this.color1 = "255 255 255 255";
diff --git a/editor/EditorCore/Themes/TorqueSuit/TorqueSuitTheme.cs b/editor/EditorCore/Themes/TorqueSuit/TorqueSuitTheme.cs
index dc0c2b634..730a4435e 100644
--- a/editor/EditorCore/Themes/TorqueSuit/TorqueSuitTheme.cs
+++ b/editor/EditorCore/Themes/TorqueSuit/TorqueSuitTheme.cs
@@ -6,7 +6,7 @@
%this.font[1] = "raleway";//Most common font
%this.font[2] = "Audiowide";//Title fontType
%this.font[3] = "vt323";//Code and console font
- %this.fontDirectory = expandPath("^EditorCore/Themes/TorqueSuit/Fonts");
+ %this.fontDirectory = expandPath("^EditorCore/Themes/TorqueSuit/fonts");
%this.fontSize = 22;
%this.color1 = "34 19 30 255";
@@ -61,6 +61,9 @@
paddingHL = 2;
paddingSL = 2;
paddingNA = 2;
+
+ borderColorSL = %this.color5;
+ borderSL = 2;
};
%this.iconButtonProfile = new GuiControlProfile()
diff --git a/editor/EditorCore/gui/fonts/Roboto-OFL.txt b/editor/EditorCore/gui/fonts/Roboto-OFL.txt
new file mode 100644
index 000000000..9c48e05a2
--- /dev/null
+++ b/editor/EditorCore/gui/fonts/Roboto-OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2011 The Roboto Project Authors (https://github.com/googlefonts/roboto-classic)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+https://openfontlicense.org
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/editor/EditorCore/gui/fonts/Roboto-Regular.ttf b/editor/EditorCore/gui/fonts/Roboto-Regular.ttf
new file mode 100644
index 000000000..3db0d1fb0
Binary files /dev/null and b/editor/EditorCore/gui/fonts/Roboto-Regular.ttf differ
diff --git a/editor/EditorCore/gui/guiProfiles.cs b/editor/EditorCore/gui/guiProfiles.cs
index 370300c4e..48a737a01 100644
--- a/editor/EditorCore/gui/guiProfiles.cs
+++ b/editor/EditorCore/gui/guiProfiles.cs
@@ -35,7 +35,13 @@
if ($platform $= "windows")
%this.platformFontType = "share tech mono";
else if ($platform $= "Android")
- %this.platformFontType = "Droid";
+ // "Droid" is gone from modern Android (Roboto since ~2014); request "Roboto",
+ // which is the system face AND the bundled assets/fonts/Roboto-Regular.ttf.
+ %this.platformFontType = "Roboto";
+ else if ($platformUnixType $= "emscripten")
+ // Web build: no system fonts; use a face that ships a pre-baked .uft cache.
+ // ($platform is "x86UNIX" on web, same as Linux, so key off $platformUnixType.)
+ %this.platformFontType = "share tech mono";
else
%this.platformFontType = "monaco";
if ($platform $= "ios")
@@ -44,6 +50,19 @@
%this.platformFontSize = 14;
else
%this.platformFontSize = 12;
+
+ // Where GuiDefaultProfile looks for its pre-baked .uft glyph cache. The legacy
+ // default "^EditorCore/gui/fonts" does NOT exist on disk — desktop survives only
+ // because createPlatformFont() synthesizes the font from a system face. The web
+ // build has no font backend, so the cache lookup must point at a real, resolvable
+ // dir that actually ships the requested .uft. Use an EXPANDED path (the resource
+ // manager does not resolve the ^Module expando for cache lookups), under the
+ // ^EditorCore module (the only expando registered at editor boot — ^AppCore is
+ // not loaded), to the LabCoat theme's fonts, which bundle "share tech mono".
+ if ($platformUnixType $= "emscripten")
+ %this.platformFontDirectory = expandPath("^EditorCore/Themes/LabCoat/fonts");
+ else
+ %this.platformFontDirectory = "^EditorCore/gui/fonts";
}
function EditorCore::AdjustColorValue(%this, %color, %percent)
@@ -102,52 +121,20 @@
%this.SetProfileColors();
%this.SetProfileFont();
- //Changing the default gui profile and border profile might cause engine instability! Consider making a new child profile instead.
- %this.SafeCreateNamedObject("GuiDefaultBorderProfile", new GuiBorderProfile()
- {
- // Default margin
- margin = 0;
- marginHL = 0;
- marginSL = 0;
- marginNA = 0;
- //Default Border
- border = 0;
- borderHL = 0;
- borderSL = 0;
- borderNA = 0;
- //Default border color
- borderColor = %this.color1;
- borderColorHL = %this.AdjustColorValue(%this.color1, 10);
- borderColorSL = %this.AdjustColorValue(%this.color1, 10);
- borderColorNA = %this.SetColorAlpha(%this.color1, 100);
- //Default Padding
- padding = 0;
- paddingHL = 0;
- paddingSL = 0;
- paddingNA = 0;
- //Default underfill
- underfill = true;
- });
-
- //See the warning above! You should avoid changing this.
- %this.SafeCreateNamedObject("GuiDefaultProfile", new GuiControlProfile()
- {
- // fill color
- fillColor = "0 0 0 0";
-
- // font
- fontType = %this.platformFontType;
- fontDirectory = "^EditorCore/gui/fonts";
- fontSize = %this.platformFontSize;
- fontColor = "255 255 255 255";
- align = center;
- vAlign = middle;
-
- cursorColor = "0 0 0 255";
-
- borderDefault = GuiDefaultBorderProfile;
- category = "default";
- });
+ // GuiDefaultProfile and GuiDefaultBorderProfile are not created here - the
+ // engine makes them at start-up (GuiControlProfile::createDefaultProfile), so
+ // the name every control falls back to can never be missing. What is left is
+ // tuning them for the editor, and that still matters: a new profile copies its
+ // unset fields from these two, and 28 of BaseTheme's profiles name no font of
+ // their own, so this is where the editor's face and border colors come from.
+ GuiDefaultBorderProfile.borderColor = %this.color1;
+ GuiDefaultBorderProfile.borderColorHL = %this.AdjustColorValue(%this.color1, 10);
+ GuiDefaultBorderProfile.borderColorSL = %this.AdjustColorValue(%this.color1, 10);
+ GuiDefaultBorderProfile.borderColorNA = %this.SetColorAlpha(%this.color1, 100);
+
+ GuiDefaultProfile.fontType = %this.platformFontType;
+ GuiDefaultProfile.fontDirectory = %this.platformFontDirectory;
+ GuiDefaultProfile.fontSize = %this.platformFontSize;
%this.SafeCreateNamedObject("GuiBrightBorderProfile", new GuiBorderProfile()
{
diff --git a/editor/EditorCore/images/editorIcons16.asset.taml b/editor/EditorCore/images/editorIcons16.asset.taml
index 090cb0c2b..e72bbaeaa 100644
--- a/editor/EditorCore/images/editorIcons16.asset.taml
+++ b/editor/EditorCore/images/editorIcons16.asset.taml
@@ -2,7 +2,7 @@
AssetName="editorIcons16"
ImageFile="editorIcons16.png"
CellCountX="32"
- CellCountY="2"
+ CellCountY="10"
CellWidth="16"
CellHeight="16"
AssetInternal="1" />
diff --git a/editor/EditorCore/images/editorIcons16.png b/editor/EditorCore/images/editorIcons16.png
index 1cdac4710..0d09ee41d 100644
Binary files a/editor/EditorCore/images/editorIcons16.png and b/editor/EditorCore/images/editorIcons16.png differ
diff --git a/editor/EditorCore/images/editorIcons24.asset.taml b/editor/EditorCore/images/editorIcons24.asset.taml
new file mode 100644
index 000000000..155781c73
--- /dev/null
+++ b/editor/EditorCore/images/editorIcons24.asset.taml
@@ -0,0 +1,8 @@
+
diff --git a/editor/EditorCore/images/editorIcons24.png b/editor/EditorCore/images/editorIcons24.png
new file mode 100644
index 000000000..b019fe64b
Binary files /dev/null and b/editor/EditorCore/images/editorIcons24.png differ
diff --git a/editor/EditorCore/images/editorIcons32.asset.taml b/editor/EditorCore/images/editorIcons32.asset.taml
new file mode 100644
index 000000000..3b4a3eda6
--- /dev/null
+++ b/editor/EditorCore/images/editorIcons32.asset.taml
@@ -0,0 +1,8 @@
+
diff --git a/editor/EditorCore/images/editorIcons32.png b/editor/EditorCore/images/editorIcons32.png
new file mode 100644
index 000000000..377e0eab7
Binary files /dev/null and b/editor/EditorCore/images/editorIcons32.png differ
diff --git a/editor/EditorCore/images/editorIcons48.asset.taml b/editor/EditorCore/images/editorIcons48.asset.taml
new file mode 100644
index 000000000..bda716ea7
--- /dev/null
+++ b/editor/EditorCore/images/editorIcons48.asset.taml
@@ -0,0 +1,8 @@
+
diff --git a/editor/EditorCore/images/editorIcons48.png b/editor/EditorCore/images/editorIcons48.png
new file mode 100644
index 000000000..0799816ae
Binary files /dev/null and b/editor/EditorCore/images/editorIcons48.png differ
diff --git a/editor/EditorCore/scripts/EditorProjectSelector.cs b/editor/EditorCore/scripts/EditorProjectSelector.cs
index 53476a492..b66ec64a6 100644
--- a/editor/EditorCore/scripts/EditorProjectSelector.cs
+++ b/editor/EditorCore/scripts/EditorProjectSelector.cs
@@ -188,14 +188,19 @@
function EditorProjectSelector::onNewProject(%this)
{
+ // Six 50 pixel form rows, the feedback line, and the button row, plus the 34
+ // pixels the window keeps for its title bar and border. A fixed form gains
+ // nothing from being dragged bigger and loses the bottom of itself behind a
+ // scroll bar when dragged smaller, so it does not resize.
%width = 700;
- %height = 340;
+ %height = 470;
%dialog = new GuiControl()
{
class = "NewProjectDialog";
superclass = "EditorDialog";
dialogSize = (%width + 8) SPC (%height + 8);
dialogCanClose = true;
+ dialogResizable = false;
dialogText = "New Project";
};
%dialog.init(%width, %height);
diff --git a/editor/EditorCore/scripts/NewProjectDialog.cs b/editor/EditorCore/scripts/NewProjectDialog.cs
index de52054ad..0e4b3e137 100644
--- a/editor/EditorCore/scripts/NewProjectDialog.cs
+++ b/editor/EditorCore/scripts/NewProjectDialog.cs
@@ -17,24 +17,45 @@ class = "EditorForm";
%item = %form.addFormItem("Title", %width SPC 30);
%this.titleBox = %form.createTextEditItem(%item);
- %this.titleBox.Command = %this.getId() @ ".Validate();";
+ %form.setItemTip(%item, %this.titleBox, "What your project is called. This is the name on its card in the project picker, so write it for people to read - spaces and punctuation are fine.");
%item = %form.addFormItem("Directory", %width SPC 30);
%this.dirBox = %form.createTextEditItem(%item);
- %this.dirBox.Command = %this.getId() @ ".Validate();";
+ %form.setItemTip(%item, %this.dirBox, "The folder your project lives in, made inside the Torque2D folder. It has to be empty or not exist yet, so nothing you already have is written over.");
- %item = %form.addFormItem("Description", %width SPC 130);
+ %item = %form.addFormItem("Game Core", %width SPC 30);
+ %this.coreDropDown = %form.createDropDownItem(%item);
+ %this.populateGameCores();
+ %form.setItemTip(%item, %this.coreDropDown, "Which template your game is copied from. Blank Game gives you a window, a background and some music, ready to be replaced with your own.");
+
+ %item = %form.addFormItem("Module Name", %width SPC 30);
+ %this.moduleNameBox = %form.createTextEditItem(%item);
+ %form.setItemTip(%item, %this.moduleNameBox, "What to call your game module. It becomes the module's folder, its ModuleId, and the namespace your game's create and destroy functions hang off - so letters, numbers and underscores only. It follows the title until you edit it.");
+
+ %item = %form.addFormItem("Author", %width SPC 30);
+ %this.authorBox = %form.createTextEditItem(%item);
+ %form.setItemTip(%item, %this.authorBox, "Who to credit for the game module. Optional: left empty, the Project Manager credits Torque2D. You can change it later under Edit Module.");
+
+ %item = %form.addFormItem("Description", %width SPC 30);
%this.descBox = %form.createTextEditItem(%item);
- %this.descBox.Command = %this.getId() @ ".Validate();";
+ %form.setItemTip(%item, %this.descBox, "A sentence saying what the project is. It shows under the title on the project card and on the game module in the Project Manager.");
+ %this.form = %form;
%content.add(%form);
+ // Positioned from the room the content actually gets rather than from the
+ // dialog's own height: the window spends 34 pixels of it on the title bar and
+ // its border (EditorDialog::contentHeight), and a button placed past that puts
+ // the whole form behind a scroll bar.
+ %formHeight = 6 * 50;
+ %buttonTop = %this.contentHeight() - 46;
+
%this.feedback = new GuiControl()
{
HorizSizing = "right";
VertSizing = "bottom";
- Position = "12 170";
- Extent = (%width - 24) SPC 80;
+ Position = "12" SPC (%formHeight + 10);
+ Extent = (%width - 24) SPC (%buttonTop - %formHeight - 28);
text = "";
textWrap = true;
textExtend = true;
@@ -45,7 +66,7 @@ class = "EditorForm";
{
HorizSizing = "right";
VertSizing = "bottom";
- Position = "478 270";
+ Position = "478" SPC (%buttonTop + 2);
Extent = "100 30";
Text = "Cancel";
Command = %this.getID() @ ".onClose();";
@@ -56,7 +77,7 @@ class = "EditorForm";
{
HorizSizing = "right";
VertSizing = "bottom";
- Position = "588 268";
+ Position = "588" SPC %buttonTop;
Extent = "100 34";
Text = "Create";
Command = %this.getID() @ ".onCreate();";
@@ -70,12 +91,111 @@ class = "EditorForm";
%this.validate();
}
+// The game cores are the library templates a project can be built out of. A
+// private ModuleManager rather than ModuleDatabase: the project selector has
+// nothing scanned at this point and this must not leave anything behind that
+// would show up as a module of the project about to be created.
+function NewProjectDialog::populateGameCores(%this)
+{
+ %manager = new ModuleManager();
+ %manager.EchoInfo = false;
+ %manager.ScanModules(pathConcat(getMainDotCsDir(), "library"));
+
+ %cores = %manager.findModuleTypes("Game Core", false);
+ for(%i = 0; %i < getWordCount(%cores); %i++)
+ {
+ %core = getWord(%cores, %i);
+ if(!%core.Template)
+ {
+ continue;
+ }
+
+ // The dropdown shows a display name and sortByText reorders it, so the id
+ // is remembered against the name. The module definitions do not outlive
+ // the manager.
+ %name = ModuleStamper.displayName(%core);
+ %this.coreDropDown.addItem(%name);
+ %this.coreID[%name] = %core.ModuleID;
+ }
+
+ %manager.delete();
+
+ %this.coreDropDown.sortByText();
+ if(%this.coreDropDown.getItemCount() > 0)
+ {
+ %this.coreDropDown.setSelected(0);
+ }
+}
+
+function NewProjectDialog::selectedGameCore(%this)
+{
+ return %this.coreID[%this.coreDropDown.getText()];
+}
+
+// Titles are written for people and module ids are written for the script
+// compiler, so the suggestion is the title with everything a namespace cannot
+// carry taken out of it.
+function NewProjectDialog::suggestModuleName(%this)
+{
+ %title = %this.titleBox.getText();
+ %name = "";
+
+ for(%i = 0; %i < strlen(%title); %i++)
+ {
+ %char = getSubStr(%title, %i, 1);
+ if(ModuleStamper.isIdentifierChar(%char))
+ {
+ %name = %name @ %char;
+ }
+ }
+
+ if(%name $= "")
+ {
+ return "";
+ }
+
+ return %name @ "Game";
+}
+
+// The module name follows the title until the moment someone types their own,
+// and from then on it is theirs. setText does not run the box's command, so the
+// guard is belt and braces against that changing.
+function NewProjectDialog::onKeyPressed(%this, %textBox)
+{
+ if(%textBox == %this.moduleNameBox)
+ {
+ if(!%this.settingModuleName)
+ {
+ %this.moduleNameEdited = true;
+ }
+ }
+ else if(%textBox == %this.titleBox && !%this.moduleNameEdited)
+ {
+ %this.settingModuleName = true;
+ %this.moduleNameBox.setText(%this.suggestModuleName());
+ %this.settingModuleName = false;
+ }
+
+ %this.validate();
+}
+
+function NewProjectDialog::onReturnPressed(%this, %textBox)
+{
+ %this.onCreate();
+}
+
+function NewProjectDialog::onDropDownClosed(%this, %dropDown)
+{
+ %this.validate();
+}
+
function NewProjectDialog::Validate(%this)
{
%this.createButton.active = false;
%title = %this.titleBox.getText();
%directory = %this.dirBox.getText();
+ %moduleName = %this.moduleNameBox.getText();
%description = %this.descBox.getText();
if(%title $= "")
@@ -104,6 +224,17 @@ class = "EditorForm";
}
}
+ if(%this.selectedGameCore() $= "")
+ {
+ %this.feedback.setText("Please choose the game core to build your project from. If this list is empty, the library folder has no game core template in it.");
+ return false;
+ }
+
+ if(!%this.validateModuleName(%moduleName))
+ {
+ return false;
+ }
+
if(%description $= "")
{
%this.feedback.setText("Please add a short, meaningful description for your project.");
@@ -115,46 +246,104 @@ class = "EditorForm";
return true;
}
+// The module name becomes a folder, a ModuleId, and the namespace the engine
+// calls create and destroy on, so it has to be a legal script identifier and it
+// cannot collide with the modules copied in beside it.
+function NewProjectDialog::validateModuleName(%this, %moduleName)
+{
+ if(%moduleName $= "")
+ {
+ %this.feedback.setText("Please enter a name for your game module.");
+ return false;
+ }
+
+ for(%i = 0; %i < strlen(%moduleName); %i++)
+ {
+ if(!ModuleStamper.isIdentifierChar(getSubStr(%moduleName, %i, 1)))
+ {
+ %this.feedback.setText("The module name becomes a script namespace, so it can only contain letters, numbers and underscores.");
+ return false;
+ }
+ }
+
+ if(strpos("0123456789", getSubStr(%moduleName, 0, 1)) != -1)
+ {
+ %this.feedback.setText("The module name cannot start with a number.");
+ return false;
+ }
+
+ if(%moduleName $= "AppCore" || %moduleName $= "Audio" || %moduleName $= "themes")
+ {
+ %this.feedback.setText("AppCore, Audio and themes are already used by every project. Please pick another module name.");
+ return false;
+ }
+
+ return true;
+}
+
function NewProjectDialog::onCreate(%this)
{
if(%this.validate())
{
%title = %this.titleBox.getText();
%directory = %this.dirBox.getText();
+ %moduleName = %this.moduleNameBox.getText();
+ %author = %this.authorBox.getText();
%description = %this.descBox.getText();
+ %core = %this.selectedGameCore();
+ // createPath wants a separator on the end -- without one the last folder is
+ // read as a filename and never made -- and everything else wants none. A
+ // trailing separator left on %path ends up in the middle of every path
+ // built from it, and while the engine's own file calls expand that away,
+ // script side isDirectory does not: it stats the string it is given.
%path = makeFullPath(%directory, getMainDotCsDir());
%lastChar = getSubStr(%path, strlen(%path) - 1, 1);
- if(%lastChar !$= "\\" && %lastChar !$= "\/")
+ if(%lastChar $= "\\" || %lastChar $= "/")
{
- %path = %path @ "\\";
+ %path = getSubStr(%path, 0, strlen(%path) - 1);
}
- createPath(%path);
+ createPath(%path @ "/");
+
+ %modulePath = pathConcat(%path, %moduleName);
ModuleDatabase.scanModules(pathConcat(getMainDotCsDir(), "library"));
ModuleDatabase.CopyModule("AppCore", 1, "AppCore", %path, true);
ModuleDatabase.CopyModule("Audio", 1, "Audio", %path, true);
- ModuleDatabase.CopyModule("BlankGame", 1, "BlankGame", pathConcat(%path, "BlankGame"), false);
+
+ // Copied under the core's own id, then renamed on the copy. Handing
+ // CopyModule a different target id would make it rename module.taml to
+ // .module.taml, which is not the name the rest of the editor
+ // opens a module definition by, and there is no fileRename in script to
+ // put it back. Its taml rewriting is also root-only and cannot reach a
+ // script file at all, which is where the namespace lives.
+ ModuleDatabase.CopyModule(%core, 1, %core, %modulePath, false);
ModuleDatabase.clearDatabase();
+ // The stock theme and its baked font caches. Not a module: a theme belongs
+ // to the project and has to survive AppCore being updated, which replaces
+ // that module's whole directory. pathCopy recurses, so this brings the
+ // fonts folder with it. (AppCore generates a theme if it ever finds none,
+ // so a project without this still works - it just starts from a theme with
+ // no baked fonts.)
+ pathCopy(pathConcat(getMainDotCsDir(), "library", "themes"), pathConcat(%path, "themes"), false);
+
+ ModuleStamper.renameInPlace(%modulePath, %core, %moduleName);
+
%file = TamlRead(pathConcat(%path, "AppCore", "1", "module.taml"));
%file.Project = %title;
%file.ProjectDescription = %description;
TamlWrite(%file, pathConcat(%path, "AppCore", "1", "module.taml"));
+ %file.delete();
- %file = TamlRead(pathConcat(%path, "BlankGame", "module.taml"));
+ %file = TamlRead(pathConcat(%modulePath, "module.taml"));
%file.Group = "launch";
%file.Type = "Game Module";
- %file.Author = "";
- TamlWrite(%file, pathConcat(%path, "BlankGame", "module.taml"));
-
- %data = new ScriptObject()
- {
- title = %title;
- directory = %directory;
- description = %description;
- icon = pathConcat(%path, "AppCore", %file.Icon);
- };
+ %file.Author = %author;
+ %file.Description = %description;
+ ModuleStamper.clearTemplateMarkers(%file);
+ TamlWrite(%file, pathConcat(%modulePath, "module.taml"));
+ %file.delete();
%this.postEvent("ProjectCreated", %directory);
%this.onClose();
diff --git a/editor/EditorCore/scripts/defaultPreferences.cs b/editor/EditorCore/scripts/defaultPreferences.cs
index 6a2e38301..b8dbc10a5 100644
--- a/editor/EditorCore/scripts/defaultPreferences.cs
+++ b/editor/EditorCore/scripts/defaultPreferences.cs
@@ -57,3 +57,10 @@
/// Fonts.
$Gui::fontCacheDirectory = expandPath( "^EditorCore/gui/fonts" );
+
+/// Generic fallback font (a .ttf rasterized by FreeType) for platforms with no
+/// system fonts -- currently the web (Emscripten) build. The editor keeps its OWN
+/// copy (separate from the app's in AppCore), so editor fonts stay independent and
+/// the editor never reaches into AppCore. This overrides AppCore's value while the
+/// editor is loaded; a shipped game (editor/ removed) falls back to AppCore's.
+$pref::Web::fallbackFont = expandPath( "^EditorCore/gui/fonts/Roboto-Regular.ttf" );
diff --git a/editor/GuiEditor/GuiEditor.cs b/editor/GuiEditor/GuiEditor.cs
index 756ea34b5..48780f914 100644
--- a/editor/GuiEditor/GuiEditor.cs
+++ b/editor/GuiEditor/GuiEditor.cs
@@ -23,18 +23,117 @@
function GuiEditor::create( %this )
{
exec("./scripts/GuiEditorBrain.cs");
+ exec("./scripts/GuiEditorControlIcons.cs");
exec("./scripts/GuiEditorControlListWindow.cs");
- exec("./scripts/GuiEditorControlListBox.cs");
+ exec("./scripts/GuiEditorControlGroup.cs");
+ exec("./scripts/GuiEditorControlTile.cs");
exec("./scripts/GuiEditorInspectorWindow.cs");
- exec("./scripts/GuiEditorInspector.cs");
exec("./scripts/GuiEditorExplorerWindow.cs");
exec("./scripts/GuiEditorExplorerTree.cs");
exec("./scripts/GuiEditorSaveGuiDialog.cs");
+ exec("./scripts/GuiEditorConfirmSaveDialog.cs");
exec("./scripts/GuiEditorGridSizeDialog.cs");
- exec("./scripts/GuiEditorColorWindow.cs");
+ exec("./scripts/GuiEditorToolsWindow.cs");
+ exec("./scripts/GuiProfileEditorDialog.cs");
+ exec("./scripts/GuiProfileEditorColorPopup.cs");
+ exec("./scripts/GuiProfileEditorBorderGrid.cs");
+ exec("./scripts/GuiProfileEditorBorderSetter.cs");
+ exec("./scripts/GuiProfileEditorBorderForm.cs");
+ exec("./scripts/GuiProfileEditorFieldSpec.cs");
+ exec("./scripts/GuiProfileEditorStateColorRow.cs");
+ exec("./scripts/GuiProfileEditorProfileForm.cs");
+ exec("./scripts/GuiProfileEditorCursorForm.cs");
+ exec("./scripts/ProfileThemeEditForm.cs");
+ exec("./scripts/GuiProfileEditorLibrary.cs");
+ exec("./scripts/GuiProfileEditorTree.cs");
+ exec("./scripts/GuiProfileEditorPreview.cs");
+ exec("./scripts/GuiProfileEditorNameDialog.cs");
+ exec("./scripts/GuiProfileEditorConfirmDialog.cs");
+ exec("./scripts/GuiEditorThemeApplier.cs");
+ exec("./scripts/GuiEditorThemeDialog.cs");
+
+ // The properties pane that replaced the native GuiInspector.
+ exec("./scripts/GuiEditorControlSpec.cs");
+ exec("./scripts/GuiEditorAnchorPicker.cs");
+ exec("./scripts/GuiEditorTextBlock.cs");
+ exec("./scripts/GuiEditorMenuItemBlock.cs");
+ exec("./scripts/GuiEditorHeaderBlock.cs");
+ exec("./scripts/GuiEditorDynamicFields.cs");
+ exec("./scripts/GuiEditorItemRow.cs");
+ exec("./scripts/GuiEditorItemsBlock.cs");
+ exec("./scripts/GuiEditorInspectorPane.cs");
+
+ // Undo. The engine has owned the machinery all along - GuiEditCtrl holds an
+ // UndoManager and a trash group it never empties - and nothing had ever
+ // built it an action.
+ exec("./scripts/GuiEditorUndoAction.cs");
+ exec("./scripts/GuiEditorUndoRecorder.cs");
+
+ // Copy, cut and paste, which is undo's machinery plus a deep clone.
+ exec("./scripts/GuiEditorClipboard.cs");
+
+ // File, Edit, Layout and Select, which this editor owns and lends to the
+ // shared bar for as long as it is the one open.
+ exec("./scripts/GuiEditorMenus.cs");
%this.guiPage = EditorCore.RegisterEditor("Gui Editor", %this);
+ // Built here, because a menu can only be built into the bar and EditorCore
+ // has made it by now - every editor module depends on EditorCore. The set
+ // takes itself back off again immediately; open() puts it on.
+ %this.menus = new ScriptObject()
+ {
+ class = "GuiEditorMenus";
+ superclass = "EditorMenuSet";
+ tool = %this;
+ };
+
+ // What the control palette can offer and what each entry looks like. Built
+ // before the palette window, which reads it as it populates. Generated from
+ // the icon sheets, so the table and the art cannot disagree.
+ %this.controlIcons = new ScriptObject()
+ {
+ class = "GuiEditorControlIcons";
+ };
+
+ // The theme library and the applier are both wanted before the Profile
+ // Editor is ever opened - the Set Theme button and every newly dropped
+ // control go through them - so they are built with the editor rather than on
+ // demand. The library also outlives each Profile Editor session so theme
+ // member profiles stay alive for the Guis wearing them.
+ %this.themeLibrary = new ScriptObject()
+ {
+ class = "GuiProfileEditorLibrary";
+ owner = %this;
+ };
+
+ // rootContainer is filled in below, once the simulated canvas exists: the
+ // applier compares against it to tell a Gui's root controls (which take the
+ // Panel profile) from everything nested inside them.
+ %this.themeApplier = new ScriptObject()
+ {
+ class = "GuiEditorThemeApplier";
+ library = %this.themeLibrary;
+ };
+
+ // Every change to the Gui being authored goes through the recorder. It asks
+ // the brain for the UndoManager when it needs one, so it can be built before
+ // the brain is.
+ %this.undoRecorder = new ScriptObject()
+ {
+ class = "GuiEditorUndoRecorder";
+ owner = %this;
+ };
+
+ // Holds copied controls for as long as the editor is open, which is longer
+ // than any one document: a copy taken from one Gui can be pasted into the
+ // next one opened.
+ %this.clipboard = new ScriptObject()
+ {
+ class = "GuiEditorClipboard";
+ owner = %this;
+ };
+
%this.content = %this.createFrameSet();
%this.brain = new GuiEditCtrl()
@@ -47,6 +146,32 @@
};
ThemeManager.setProfile(%this.brain, "guiEditorProfile");
+ // The frameset docks children into empty frames in add-order (depth-first
+ // through the splits), so the Gui Tools window must be added before the
+ // inspector window to land in the frame above it.
+ %this.guiToolsWindow = new GuiWindowCtrl()
+ {
+ Class = "GuiEditorToolsWindow";
+ HorizSizing = "right";
+ VertSizing = "bottom";
+ Position = "0 0";
+ Extent = "360 92";
+ MinExtent = "100 64";
+ text = "Gui Tools";
+ canMove = true;
+ canClose = false;
+ canMinimize = true;
+ canMaximize = false;
+ resizeWidth = true;
+ resizeHeight = true;
+ };
+ ThemeManager.setProfile(%this.guiToolsWindow, "windowProfile");
+ ThemeManager.setProfile(%this.guiToolsWindow, "windowContentProfile", "ContentProfile");
+ ThemeManager.setProfile(%this.guiToolsWindow, "windowButtonProfile", "CloseButtonProfile");
+ ThemeManager.setProfile(%this.guiToolsWindow, "windowButtonProfile", "MinButtonProfile");
+ ThemeManager.setProfile(%this.guiToolsWindow, "windowButtonProfile", "MaxButtonProfile");
+ %this.content.add(%this.guiToolsWindow);
+
%this.inspectorWindow = new GuiWindowCtrl()
{
Class = "GuiEditorInspectorWindow";
@@ -145,6 +270,7 @@
class = "SimulatedCanvas";
};
%this.background.add(%this.rootGui);
+ %this.themeApplier.rootContainer = %this.rootGui;
%this.brain.extent = %this.background.getExtent();
%this.background.add(%this.brain);
%this.fileName = "";
@@ -156,29 +282,6 @@ class = "SimulatedCanvas";
%this.brain.root = %this.rootGui;
%this.explorerWindow.inspect(%this.rootGui);
- /* %this.colorWindow = new GuiWindowCtrl()
- {
- Class = "GuiEditorColorWindow";
- HorizSizing = "right";
- VertSizing = "bottom";
- Position = "610 0";
- Extent = "400 380";
- MinExtent = "100 100";
- text = "Color Test";
- canMove = true;
- canClose = false;
- canMinimize = true;
- canMaximize = false;
- resizeWidth = true;
- resizeHeight = true;
- };
- ThemeManager.setProfile(%this.colorWindow, "windowProfile");
- ThemeManager.setProfile(%this.colorWindow, "windowContentProfile", "ContentProfile");
- ThemeManager.setProfile(%this.colorWindow, "windowButtonProfile", "CloseButtonProfile");
- ThemeManager.setProfile(%this.colorWindow, "windowButtonProfile", "MinButtonProfile");
- ThemeManager.setProfile(%this.colorWindow, "windowButtonProfile", "MaxButtonProfile");
- %this.guiPage.add(%this.colorWindow); */
-
EditorCore.FinishRegistration(%this.guiPage);
}
@@ -204,13 +307,24 @@ class = "SimulatedCanvas";
%leftID = getWord(%idList, 0);
%rightID = getWord(%idList, 1);
%content.anchorFrame(%rightID);
- %content.setFrameSize(%rightID, 300);
+
+ // 340, not 300: this column holds the control palette, whose grid view fits
+ // as many 100-pixel tiles per row as the width allows. At 300, once the
+ // scroll bar is taken out, that is two -- and the leftover is shared between
+ // them, so the tiles sit in gappy columns. 340 makes it three.
+ %content.setFrameSize(%rightID, 340);
%ids = %content.createHorizontalSplit(%leftID);
%inspectorFrameID = getWord(%ids, 0);
%centerFrameID = getWord(%ids, 1);
%content.setFrameSize(%inspectorFrameID, 360);
-
+
+ // Split the inspector column so the Gui Tools window docks above the
+ // Gui Inspector. The top child of a vertical split is the anchored frame.
+ %ids = %content.createVerticalSplit(%inspectorFrameID);
+ %guiToolsFrameID = getWord(%ids, 0);
+ %content.setFrameSize(%guiToolsFrameID, 92);
+
%ids = %content.createVerticalSplit(%rightID);
%toolFrameID = getWord(%ids, 0);
%explorerFrameID = getWord(%ids, 1);
@@ -223,29 +337,95 @@ class = "SimulatedCanvas";
function GuiEditor::destroy( %this )
{
+ // Order matters. The Profile Editor's live preview wears theme member
+ // profiles owned by the theme library, and the library deliberately outlives
+ // the dialog (see openProfileEditor). Freeing it while the dialog is still up
+ // leaves those preview controls holding freed profiles, and the dangling
+ // mProfile is not touched until the canvas itself is torn down - inside
+ // Sim::shutdown, long after this runs - so it surfaces as an access violation
+ // at exit with no obvious cause. Close the dialog first.
+ %this.closeProfileEditor();
+
+ if(isObject(%this.themeApplier))
+ {
+ %this.themeApplier.delete();
+ }
+
+ if(isObject(%this.themeLibrary))
+ {
+ %this.themeLibrary.delete();
+ }
+ if(isObject(%this.controlIcons))
+ {
+ %this.controlIcons.delete();
+ }
+
+ // Empty the stacks while the brain (and so the UndoManager it owns) is still
+ // here, rather than leaving the actions to the manager's destructor during
+ // canvas teardown.
+ if(isObject(%this.undoRecorder))
+ {
+ %this.undoRecorder.clear();
+ %this.undoRecorder.delete();
+ }
+
+ // The copies it holds are real controls wearing real profiles, so they go the
+ // same way and for the same reason: before the profiles do.
+ if(isObject(%this.clipboard))
+ {
+ %this.clipboard.delete();
+ }
+
+ // Takes itself off the bar first if it is still on it.
+ if(isObject(%this.menus))
+ {
+ %this.menus.delete();
+ }
}
function GuiEditor::open(%this, %content)
{
- EditorCore.menuBar.setMenuActive("File", true);
- //EditorCore.menuBar.setMenuActive("Edit", true); //These features still need development
- EditorCore.menuBar.setMenuActive("Layout", true);
- EditorCore.menuBar.setMenuActive("Select", true);
+ // First time in: pick up the project's theme. Not done at create time -
+ // the editor registers before a project's AppCore has loaded its themes.
+ if(%this.themeName $= "")
+ {
+ %this.adoptTheme("");
+ }
+
+ // Puts the four menus on the shared bar, and refreshes them on the way: Undo
+ // and Redo grey from the stacks, Cut and Copy from the selection, Paste from
+ // whether anything has been copied, and Revert from whether the document has
+ // a file. The menus look new every time the editor is opened.
+ EditorCore.setEditorMenus(%this.menus);
+
+ // The window title is the other thing that looks new every time: the tools
+ // window was built with a placeholder and has not been told about the
+ // document since.
+ %this.refreshDocumentTitle();
+
editorMode(true);
}
function GuiEditor::close(%this)
{
editorMode(false);
- EditorCore.menuBar.setMenuActive("File", false);
- EditorCore.menuBar.setMenuActive("Edit", false);
- EditorCore.menuBar.setMenuActive("Layout", false);
- EditorCore.menuBar.setMenuActive("Select", false);
+ EditorCore.setEditorMenus("");
}
//MENU FUNCTIONS---------------------------------------------------------------
+//
+// The two that replace the document ask first and then do it. The asking half is
+// what the menu calls; the doing half is what the guard runs once there is
+// nothing left to lose.
+//-----------------------------------------------------------------------------
+
function GuiEditor::NewGui(%this)
+{
+ %this.guardDocument("GuiEditor.newGuiNow();");
+}
+
+function GuiEditor::newGuiNow(%this)
{
%this.rootGui.clear();
%this.fileName = "";
@@ -255,9 +435,28 @@ class = "SimulatedCanvas";
%this.module = "";
%this.brain.clearSelection();
%this.explorerWindow.tree.refresh();
+
+ // Every record on the stack names controls that have just been freed.
+ %this.undoRecorder.clear();
+
+ // A new Gui joins the theme this session is working in, so the first control
+ // dropped into it is already themed.
+ %theme = %this.defaultTheme();
+ %this.themeName = isObject(%theme) ? %theme.getName() : "";
+
+ // Last, and after the clear above: emptying the stack leaves the recorder
+ // somewhere no replay can reach, which is right for a document that lost its
+ // records and wrong for this one, which has nothing in it to save.
+ %this.undoRecorder.markClean();
+ %this.refreshFileMenu();
}
function GuiEditor::OpenGui(%this)
+{
+ %this.guardDocument("GuiEditor.openGuiNow();");
+}
+
+function GuiEditor::openGuiNow(%this)
{
%path = pathConcat(getMainDotCsDir(), ProjectManager.getProjectFolder());
%dialog = new OpenFileDialog()
@@ -273,46 +472,80 @@ class = "SimulatedCanvas";
if ( %result )
{
- if(fileExt(%dialog.fileName) $= ".taml")
- {
- %guiContent = TAMLRead(%dialog.fileName);
- %includesSimulatedCanvas = (%guiContent.class $= "SimulatedCanvas");
- }
- else
- {
- exec(%dialog.fileName);
- }
- if(%includesSimulatedCanvas $= "")
- {
- %includesSimulatedCanvas = true;
- }
- if(isObject(%guiContent))
- {
- %this.fileName = fileName(%dialog.fileName);
- %this.filePath = %dialog.fileName;
- %this.formatIndex = 0;
- if(getSubStr(%dialog.fileName, strlen(%dialog.fileName) - 5, 5) $= ".taml")
- {
- %this.formatIndex = 1;
- }
- %this.folder = makeRelativePath(filePath(%dialog.fileName), getMainDotCsDir());
- %this.module = EditorCore.findModuleOfPath(%dialog.fileName);
- %this.DisplayGuiContent(%guiContent, %includesSimulatedCanvas);
- }
- else
- {
- EditorCore.alert("Something went wrong while opening the Gui File. Gui Files should be structures with the root object assigned to %guiContent. If this file was made outside of the editor, you can change it manually and then open it in the Gui Editor.");
- }
+ %this.loadGuiFile(%dialog.fileName);
}
// Cleanup
%dialog.delete();
}
+// Read a Gui file and make it the document. Everything about opening except
+// choosing the file, so that Revert - which has already chosen - reads exactly
+// what Open reads and the two cannot drift apart.
+function GuiEditor::loadGuiFile(%this, %path)
+{
+ if(fileExt(%path) $= ".taml")
+ {
+ %guiContent = TAMLRead(%path);
+ %includesSimulatedCanvas = (%guiContent.class $= "SimulatedCanvas");
+ }
+ else
+ {
+ exec(%path);
+ }
+ if(%includesSimulatedCanvas $= "")
+ {
+ %includesSimulatedCanvas = true;
+ }
+ if(isObject(%guiContent))
+ {
+ %this.fileName = fileName(%path);
+ %this.filePath = %path;
+ %this.formatIndex = 0;
+ if(getSubStr(%path, strlen(%path) - 5, 5) $= ".taml")
+ {
+ %this.formatIndex = 1;
+ }
+ %this.folder = makeRelativePath(filePath(%path), getMainDotCsDir());
+ %this.module = EditorCore.findModuleOfPath(%path);
+ %this.DisplayGuiContent(%guiContent, %includesSimulatedCanvas);
+ %this.refreshFileMenu();
+ }
+ else
+ {
+ EditorCore.alert("Something went wrong while opening the Gui File. Gui Files should be structures with the root object assigned to %guiContent. If this file was made outside of the editor, you can change it manually and then open it in the Gui Editor.");
+ }
+}
+
+// Throw away everything done since the last save by reading the file again.
+// Guarded like the rest: it is the most deliberate discard there is, and it
+// should still say what it is about to lose.
+function GuiEditor::Revert(%this)
+{
+ if(%this.filePath $= "")
+ {
+ return;
+ }
+
+ %this.guardDocument("GuiEditor.revertNow();");
+}
+
+function GuiEditor::revertNow(%this)
+{
+ %this.loadGuiFile(%this.filePath);
+}
+
function GuiEditor::DisplayGuiContent(%this, %content, %includesSimulatedCanvas)
{
%this.rootGui.deleteObjects();
%this.brain.clearSelection();
+ // The document the stack was recorded against has just been deleted.
+ %this.undoRecorder.clear();
+
+ // Read off the root before it is unpacked - in the simulated-canvas case the
+ // object carrying the field is deleted a few lines down.
+ %recordedTheme = %content.guiTheme;
+
if(%includesSimulatedCanvas)
{
%count = %content.getCount();
@@ -328,12 +561,20 @@ class = "SimulatedCanvas";
%this.explorerWindow.tree.refresh();
%this.brain.onSelect(%this.rootGui.getObject(0));
}
- else
+ else
{
%this.rootGui.add(%content);
%this.explorerWindow.tree.refresh();
%this.brain.onSelect(%content);
}
+
+ %this.adoptTheme(%recordedTheme);
+
+ // What was just put on the canvas is what is on disk. The clear above left
+ // the recorder unreachable by any replay, which is the right answer for a
+ // document whose records were thrown away and the wrong one for a document
+ // that has only this moment been read in.
+ %this.undoRecorder.markClean();
}
function GuiEditor::SaveGui(%this)
@@ -369,10 +610,421 @@ class = "GuiEditorSaveGuiDialog";
Canvas.pushDialog(%dialog);
}
+function GuiEditor::getThemeLibrary(%this)
+{
+ %this.themeLibrary.scanThemes();
+ return %this.themeLibrary;
+}
+
+function GuiEditor::openProfileEditor(%this)
+{
+ %canvasSize = Canvas.getExtent();
+ %width = getWord(%canvasSize, 0) - 80;
+ %height = getWord(%canvasSize, 1) - 80;
+
+ %dialog = new GuiControl()
+ {
+ class = "GuiProfileEditorDialog";
+ superclass = "EditorDialog";
+ dialogSize = (%width + 8) SPC (%height + 8);
+ dialogCanClose = true;
+ dialogText = "Gui Profile Editor";
+ library = %this.themeLibrary;
+ };
+ %dialog.init(%width, %height);
+ %this.profileEditorDialog = %dialog;
+
+ Canvas.pushDialog(%dialog);
+}
+
+//THE DOCUMENT-----------------------------------------------------------------
+//
+// What is being edited and whether it has changes that are not on disk. The
+// answer to the second lives on the undo recorder, which is already the one
+// funnel every change goes through, so there is no second flag here to fall out
+// of step with it.
+//-----------------------------------------------------------------------------
+
+// A Gui that has never been saved has no name to show, so it is given one. It is
+// what the file will be called if the user accepts the Save dialog's default,
+// which is where the same string comes from.
+function GuiEditor::documentName(%this)
+{
+ return (%this.fileName $= "") ? "untitled.gui" : %this.fileName;
+}
+
+// Called by the recorder every time the document moves, and by the three places
+// that change its file name. Guarded because the recorder is built before the
+// window is (see create), so a record made in between would arrive early.
+function GuiEditor::refreshDocumentTitle(%this)
+{
+ if(isObject(%this.guiToolsWindow))
+ {
+ %this.guiToolsWindow.showDocument(%this.documentName(),
+ %this.undoRecorder.isModified());
+ }
+}
+
+// Revert is the only File item whose offer changes, and what it turns on is
+// whether the document has a file to go back to. That changes far more rarely
+// than the document does - on a save, a new one and an open - so it is called
+// from those three rather than from refreshDocumentTitle, which runs on every
+// edit. The menu set refreshes it for itself when it goes back on the bar.
+function GuiEditor::refreshFileMenu(%this)
+{
+ %this.menus.refreshFile();
+}
+
+//-----------------------------------------------------------------------------
+// The guard.
+//
+// Four commands throw the document away: New, Open, Revert, and - from the
+// Torque2D menu - Close Project and Exit. Each of them hands what it was about
+// to do to guardDocument instead of doing it, and gets it back either at once or
+// once the user has answered for it.
+//
+// A command string rather than a method name, because a menu item in this
+// codebase IS a command string: the caller passes exactly what it would have
+// run, and quit() and restartInstance() - which belong to nobody - go through
+// unchanged.
+//
+// Not guarded, and it cannot be: the window's own close button. quit() posts the
+// quit message the moment it is called, and the X posts it straight from the
+// window procedure with no script in between, so there is no moment at which to
+// ask. onPreExit runs inside shutdown, long past it.
+//-----------------------------------------------------------------------------
+
+function GuiEditor::guardDocument(%this, %command)
+{
+ if(!%this.undoRecorder.isModified())
+ {
+ eval(%command);
+ return;
+ }
+
+ %this.pendingCommand = %command;
+
+ %width = 460;
+ %height = 150;
+ %dialog = new GuiControl()
+ {
+ class = "GuiEditorConfirmSaveDialog";
+ superclass = "EditorDialog";
+ dialogSize = (%width + 8) SPC (%height + 8);
+ dialogCanClose = true;
+ dialogText = "Unsaved Changes";
+ message = "\"" @ %this.documentName() @
+ "\" has changes that have not been saved.";
+ };
+ %dialog.init(%width, %height);
+
+ Canvas.pushDialog(%dialog);
+}
+
+// Go through with whatever was interrupted. Called from exactly two places: the
+// Discard button, and the end of a save that really did write a file.
+function GuiEditor::runPendingCommand(%this)
+{
+ %command = %this.pendingCommand;
+ %this.pendingCommand = "";
+
+ if(%command !$= "")
+ {
+ eval(%command);
+ }
+}
+
+// And the other end: nothing was saved and nothing else is going to happen.
+// Cancel on the prompt, and Cancel on the Save As dialog it can lead to - which
+// is the one that matters, because a save that was called off has to call off
+// what it was saving for.
+function GuiEditor::dropPendingCommand(%this)
+{
+ %this.pendingCommand = "";
+}
+
+//THEMES-----------------------------------------------------------------------
+//
+// A Gui belongs to a theme. Setting one re-profiles every control in the
+// document by category, a newly dropped control joins it on arrival, and the
+// theme's name is saved with the Gui so reopening it lands back where it was.
+// The intent is that profiles are something a developer chooses once, in the
+// Profile Editor, and rarely thinks about again.
+//-----------------------------------------------------------------------------
+
+function GuiEditor::openThemeDialog(%this)
+{
+ %width = 420;
+ %height = 200;
+ %dialog = new GuiControl()
+ {
+ class = "GuiEditorThemeDialog";
+ superclass = "EditorDialog";
+ dialogSize = (%width + 8) SPC (%height + 8);
+ dialogCanClose = true;
+ dialogText = "Set Gui Theme";
+ };
+ %dialog.init(%width, %height);
+
+ Canvas.pushDialog(%dialog);
+}
+
+// Put %theme on the whole document. The simulated canvas is skipped: it is the
+// editor's stage, not part of the Gui being authored.
+function GuiEditor::setTheme(%this, %theme, %overrideStandalone)
+{
+ if(!isObject(%theme))
+ {
+ return;
+ }
+
+ %this.themeName = %theme.getName();
+ %this.lastThemeName = %this.themeName;
+
+ // One undo step for the whole sweep, however many profile slots it fills.
+ %this.undoRecorder.begin("Set Theme", "");
+ %changed = %this.themeApplier.applyToChildren(%this.rootGui, %theme, %overrideStandalone);
+ %this.undoRecorder.end();
+
+ %this.explorerWindow.tree.refresh();
+
+ // The properties pane caches which profiles it offers, so it has to be told
+ // as well -- otherwise it goes on showing the profile the selected control
+ // wore before the sweep, from a theme that is no longer the Gui's.
+ %this.inspectorWindow.onRethemed(%this.inspectorWindow.pane.target);
+
+ echo("Gui Editor: " @ %this.themeName @ " applied to " @ %changed @ " profile slot(s).");
+}
+
+// The theme a new Gui starts on: the last one used this session, falling back to
+// whatever the project has. There is no preferences file to remember it across
+// runs, and none is needed - an existing Gui carries its own theme.
+function GuiEditor::defaultTheme(%this)
+{
+ %themes = %this.getThemeLibrary().getThemes();
+
+ if(%this.lastThemeName !$= "")
+ {
+ for(%i = 0; %i < getWordCount(%themes); %i++)
+ {
+ %theme = getWord(%themes, %i);
+ if(%theme.getName() $= %this.lastThemeName)
+ {
+ return %theme;
+ }
+ }
+ }
+
+ return (getWordCount(%themes) > 0) ? getWord(%themes, 0) : 0;
+}
+
+function GuiEditor::themeByName(%this, %name)
+{
+ if(%name $= "")
+ {
+ return 0;
+ }
+
+ %themes = %this.getThemeLibrary().getThemes();
+ for(%i = 0; %i < getWordCount(%themes); %i++)
+ {
+ %theme = getWord(%themes, %i);
+ if(%theme.getName() $= %name)
+ {
+ return %theme;
+ }
+ }
+
+ return 0;
+}
+
+// Work out which theme a freshly opened Gui is on. The name recorded when it was
+// saved wins; a Gui written before that field existed, or authored by hand, is
+// judged by the profiles its controls wear; failing both, it joins the theme the
+// session is already working in.
+function GuiEditor::adoptTheme(%this, %recordedName)
+{
+ %theme = %this.themeByName(%recordedName);
+
+ if(!isObject(%theme))
+ {
+ %theme = %this.themeApplier.inferTheme(%this.rootGui);
+ }
+
+ if(!isObject(%theme))
+ {
+ %theme = %this.defaultTheme();
+ }
+
+ %this.themeName = isObject(%theme) ? %theme.getName() : "";
+ if(%this.themeName !$= "")
+ {
+ %this.lastThemeName = %this.themeName;
+ }
+}
+
+// Called by the theme library before it frees a theme or profile the document
+// might be wearing. A control's profile field is a raw pointer, so it has to be
+// moved off the doomed profile before the delete rather than after.
+function GuiEditor::detachTheme(%this, %theme, %profile)
+{
+ // The stack is full of profile ids that are about to stop resolving, and a
+ // detach is not itself something to undo - the profile it moved off will not
+ // exist to move back to.
+ %this.undoRecorder.clear();
+
+ // And the clipboard holds controls whose profile fields are raw pointers to
+ // the same doomed profiles. Nothing reads them while the copy sits in the
+ // stash, but a paste would - and by then the profile is gone.
+ %this.clipboard.clear();
+
+ %this.themeApplier.detach(%this.rootGui, %theme, %profile);
+}
+
+// The other half: after a revert has re-read the theme files, the document's
+// theme is a new object with the same name, so put it back on.
+function GuiEditor::reattachTheme(%this)
+{
+ %theme = %this.themeByName(%this.themeName);
+ if(isObject(%theme))
+ {
+ // Repairing the document after a revert is not an edit the user made, so
+ // it is not one they can take back. Suspended rather than cleared: the
+ // detach that preceded this already emptied the stack.
+ %this.undoRecorder.suspend();
+ %this.themeApplier.applyToChildren(%this.rootGui, %theme, false);
+ %this.undoRecorder.resume();
+
+ %this.explorerWindow.tree.refresh();
+ }
+}
+
+// Tear the Profile Editor dialog down synchronously (not via the usual deferred
+// close). Called at shutdown before the editor and AppCore modules unload, so
+// the dialog's live preview and controls stop referencing theme and editor
+// profiles before those profiles are freed - otherwise the controls' onSleep
+// runs decRefCount on freed profiles during final teardown and crashes.
+function GuiEditor::closeProfileEditor(%this)
+{
+ if(isObject(%this.profileEditorDialog))
+ {
+ if(isObject(Canvas))
+ {
+ Canvas.popDialog(%this.profileEditorDialog);
+ }
+ %this.profileEditorDialog.delete();
+ %this.profileEditorDialog = "";
+ }
+}
+
+//-----------------------------------------------------------------------------
+// What the legacy .gui script format cannot carry.
+//
+// FileObject::writeObject walks a control's fields and its child objects, and
+// that is the whole of it. Two things in the engine are neither: a list box or
+// drop down's static rows, and a frame set's layout. Both are written as TAML
+// custom nodes, so saving as .gui drops them silently - which the frame set has
+// done since it was written, and which is worth saying out loud now that
+// something people use every day is in the same boat.
+//
+// Returns a sentence naming what would go, or "" when there is nothing to say.
+//-----------------------------------------------------------------------------
+
+function GuiEditor::tamlOnlyStateSummary(%this)
+{
+ %this.tamlOnlyRows = 0;
+ %this.tamlOnlyLists = 0;
+ %this.tamlOnlyFrameSets = 0;
+ %this.countTamlOnlyState(%this.rootGui);
+
+ if(%this.tamlOnlyRows == 0 && %this.tamlOnlyFrameSets == 0)
+ {
+ return "";
+ }
+
+ // Only what the document actually holds gets named, in the heading and in
+ // the tally both: telling someone their frame layouts are at risk when there
+ // is not a frame set in the Gui is how a warning gets ignored.
+ %kinds = "";
+ %parts = "";
+
+ if(%this.tamlOnlyRows > 0)
+ {
+ %kinds = "list rows";
+ %parts = %this.tamlOnlyRows SPC
+ ((%this.tamlOnlyRows == 1) ? "row" : "rows") SPC "on" SPC
+ %this.tamlOnlyLists SPC ((%this.tamlOnlyLists == 1) ? "list" : "lists");
+ }
+
+ if(%this.tamlOnlyFrameSets > 0)
+ {
+ %kinds = (%kinds $= "") ? "frame layouts" : (%kinds @ " or frame layouts");
+
+ %frames = %this.tamlOnlyFrameSets SPC
+ ((%this.tamlOnlyFrameSets == 1) ? "frame layout" : "frame layouts");
+ %parts = (%parts $= "") ? %frames : (%parts @ " and " @ %frames);
+ }
+
+ return "This format cannot save" SPC %kinds @ ":" SPC %parts SPC
+ "would be lost. Save as TAML to keep them.";
+}
+
+function GuiEditor::countTamlOnlyState(%this, %ctrl)
+{
+ if(!isObject(%ctrl))
+ {
+ return;
+ }
+
+ // A tree's rows are generated from a root object and are never written, so
+ // it is not a list for this purpose however much it derives from one.
+ if((%ctrl.isMemberOfClass("GuiListBoxCtrl") || %ctrl.isMemberOfClass("GuiDropDownCtrl")) &&
+ !%ctrl.isMemberOfClass("GuiTreeViewCtrl"))
+ {
+ %rows = %ctrl.getItemCount();
+ if(%rows > 0)
+ {
+ %this.tamlOnlyRows += %rows;
+ %this.tamlOnlyLists++;
+ }
+ }
+
+ // An unsplit frame set has a layout of one frame holding one control, which
+ // is what it would be rebuilt as anyway. Eight numbers is one frame.
+ if(%ctrl.isMemberOfClass("GuiFrameSetCtrl") && getWordCount(%ctrl.getFrameLayout()) > 8)
+ {
+ %this.tamlOnlyFrameSets++;
+ }
+
+ for(%i = 0; %i < %ctrl.getCount(); %i++)
+ {
+ %this.countTamlOnlyState(%ctrl.getObject(%i));
+ }
+}
+
function GuiEditor::SaveCore(%this, %filePath, %formatIndex, %folder, %module)
{
+ // Record the theme on whichever object is about to be written, so reopening
+ // the Gui does not have to guess. It means nothing to the game.
+ //
+ // canSaveDynamicFields has to be turned on for it to survive the trip: every
+ // GuiControl clears that flag in its constructor (guiControl.cc), so dynamic
+ // fields on controls are dropped by both writers by default.
+ %root = (%this.rootGui.getCount() == 1) ? %this.rootGui.getObject(0) : %this.rootGui;
+ %root.canSaveDynamicFields = true;
+ %root.guiTheme = %this.themeName;
+
if(%formatIndex == 0)
{
+ // The save dialog says this in its feedback line, but a re-save never
+ // opens one: Ctrl+S goes straight here with the format the Gui was
+ // last written in.
+ %warning = %this.tamlOnlyStateSummary();
+ if(%warning !$= "")
+ {
+ warn("Gui Editor: " @ %warning);
+ }
+
%fo = new FileObject();
%fo.openForWrite(%filePath);
%fo.writeLine("//--- Created with the GuiEditor ---//");
@@ -409,47 +1061,219 @@ class = "GuiEditorSaveGuiDialog";
%this.formatIndex = %formatIndex;
%this.folder = %folder;
%this.module = %module;
+
+ // After the name is set, not before: marking clean refreshes the title, and
+ // the title is the name that was just written.
+ %this.undoRecorder.markClean();
+
+ // The file is on disk, so whatever was waiting on it can go ahead. This is
+ // one of only two places that releases it, and the only one reached by a
+ // save - which is what makes a cancelled Save As call the whole thing off
+ // rather than quietly continuing without a file.
+ %this.refreshFileMenu();
+ %this.runPendingCommand();
}
+//UNDO-------------------------------------------------------------------------
+//
+// The stack lives on the UndoManager the brain (a C++ GuiEditCtrl) has always
+// owned; GuiEditorUndoRecorder is what fills it. Undoing writes to the same
+// controls the editor writes to, so the recorder is suspended for the duration
+// or the replay would record itself.
+//-----------------------------------------------------------------------------
+
function GuiEditor::Undo(%this)
{
%undoManager = %this.brain.getUndoManager();
+ if(%undoManager.getUndoCount() == 0)
+ {
+ return;
+ }
+
+ %this.undoRecorder.suspend();
%undoManager.undo();
+ %this.undoRecorder.resume();
+
+ %this.afterReplay();
}
function GuiEditor::Redo(%this)
{
%undoManager = %this.brain.getUndoManager();
+ if(%undoManager.getRedoCount() == 0)
+ {
+ return;
+ }
+
+ %this.undoRecorder.suspend();
%undoManager.redo();
+ %this.undoRecorder.resume();
- %count = %undoManager.getRedoCount();
+ %this.afterReplay();
+}
+// What the rest of the editor has to be told after a replay. The action reports
+// which controls it touched on its way through, so the selection can land on
+// what just changed - a Ctrl+Z that moves a control scrolled off the top of the
+// canvas would otherwise look like nothing happened.
+function GuiEditor::afterReplay(%this)
+{
+ %this.explorerWindow.tree.refresh();
+ %this.selectAfterReplay(%this.undoRecorder.replayTouched);
+ %this.undoRecorder.refreshMenu();
}
-function GuiEditor::Cut(%this)
+function GuiEditor::selectAfterReplay(%this, %list)
{
-
+ %wanted = "";
+
+ for(%i = 0; %i < getWordCount(%list); %i++)
+ {
+ %ctrl = getWord(%list, %i);
+
+ // Undoing an add puts the control in the trash, and redoing a delete
+ // puts it back there. Either way it is no longer part of the Gui, so
+ // there is nothing to select.
+ if(isObject(%ctrl) && %this.inDocument(%ctrl))
+ {
+ %wanted = (%wanted $= "") ? %ctrl : (%wanted SPC %ctrl);
+ }
+ }
+
+ %this.brain.restoreSelection(%wanted);
+}
+
+function GuiEditor::inDocument(%this, %ctrl)
+{
+ %parent = %ctrl.getParent();
+ while(isObject(%parent))
+ {
+ if(%parent == %this.rootGui)
+ {
+ return true;
+ }
+ %parent = %parent.getParent();
+ }
+
+ return false;
}
+//CLIPBOARD--------------------------------------------------------------------
+//
+// The copies live on GuiEditorClipboard; these three are the Edit menu's way in.
+// Ctrl+X/C/V reach them as menu accelerators, which the canvas only consults
+// once the first responder has passed on the key (guiCanvas.cc) - so a text box
+// in the properties pane keeps Ctrl+C for its own text, and the canvas gets it
+// only when nothing else wanted it.
+//-----------------------------------------------------------------------------
+
function GuiEditor::Copy(%this)
{
-
+ %this.clipboard.copy(%this.brain.getSelected());
+}
+
+// Copy, then delete. Cut is those two things and has no third thing of its own,
+// so it says so rather than keeping a second copy of what deleting means.
+function GuiEditor::Cut(%this)
+{
+ if(!%this.clipboard.copy(%this.brain.getSelected()))
+ {
+ return;
+ }
+
+ %this.DeleteSelection();
+}
+
+// What the Delete key already does, reachable from the Edit menu - which is the
+// only place that says the command exists at all. The C++ moves the selection
+// into the trash and announces it, and the recorder turns that into one undo
+// step (GuiEditorBrain::onTrashSelection). Nothing is deleted for real, so this
+// is undoable and the controls are still alive in the trash - which is also why
+// a cut and paste keeps the names it had: a trashed control is not in the
+// document, so nothing there holds its name.
+//
+// DeleteSelection rather than Delete, and it has to be: delete is a console
+// method on every SimObject, so GuiEditor.Delete() would destroy the editor.
+function GuiEditor::DeleteSelection(%this)
+{
+ %this.brain.deleteSelection();
+ %this.brain.onDelete();
}
function GuiEditor::Paste(%this)
{
-
+ %this.clipboard.paste();
}
+// A copy that goes straight back into the parent it came from, leaving whatever
+// is on the clipboard where it is.
+function GuiEditor::Duplicate(%this)
+{
+ %this.clipboard.duplicate(%this.brain.getSelected());
+}
+
+//LAYOUT-----------------------------------------------------------------------
+//
+// The Layout menu's commands go through here rather than straight to the brain,
+// because the brain's C++ says nothing when it aligns or restacks a selection -
+// unlike a drag or a nudge, which it brackets with callbacks. Recording either
+// side of the call is cheaper than teaching the engine to announce them.
+//-----------------------------------------------------------------------------
+
function GuiEditor::changeExtent(%this, %x, %y)
{
%set = %this.brain.getSelected();
if(%set.getCount() >= 1)
{
+ %this.undoRecorder.snapshot(%set);
+
%obj = %set.getObject(0);
%ext = %obj.getExtent();
%obj.setExtent(getWord(%ext, 0) + %x, getWord(%ext, 1) + %y);
+
+ // Same kind as a nudge, and for the same reason: holding the key down is
+ // one resize, not one per repeat.
+ %this.undoRecorder.commitGeometry("Resize Control", "resize");
+ }
+}
+
+function GuiEditor::Justify(%this, %mode)
+{
+ %this.undoRecorder.snapshot(%this.brain.getSelected());
+ %this.brain.Justify(%mode);
+ %this.undoRecorder.commitGeometry("Align Controls", "");
+}
+
+function GuiEditor::BringToFront(%this)
+{
+ %this.restack("BringToFront", "Bring to Front");
+}
+
+function GuiEditor::PushToBack(%this)
+{
+ %this.restack("PushToBack", "Push to Back");
+}
+
+// Both do the same thing to the same one control - the C++ ignores anything but
+// a single selection - and both change only its index among its siblings.
+function GuiEditor::restack(%this, %method, %name)
+{
+ %set = %this.brain.getSelected();
+ if(%set.getCount() != 1)
+ {
+ return;
}
+
+ %ctrl = %set.getObject(0);
+ %parent = %ctrl.getParent();
+ if(!isObject(%parent))
+ {
+ return;
+ }
+
+ %oldIndex = %this.undoRecorder.indexOf(%parent, %ctrl);
+ %this.brain.call(%method);
+ %this.undoRecorder.recordMove(%ctrl, %parent, %oldIndex, %name);
}
function GuiEditor::SetGridSize(%this)
@@ -469,13 +1293,20 @@ class = "GuiEditorGridSizeDialog";
Canvas.pushDialog(%dialog);
}
+// The Layout menu's Snap to Grid toggle, which says whether to use the grid and
+// nothing about how big it is. Turning it back on asks the brain what the grid
+// was rather than naming a number: setSnapToGrid(0) clears the flag and leaves
+// the spacing alone precisely so that it can be picked back up here, and a size
+// the user chose in Set Grid Size should not be thrown away by a switch that was
+// never about the size. There is always an answer to pick up - the brain sets a
+// grid of 10 in onAdd, and 0 only ever means "off".
function GuiEditor::SnapToGrid(%this, %gridOn)
{
if(%gridOn)
{
- %this.brain.setSnapToGrid(10);
+ %this.brain.setSnapToGrid(%this.brain.getGridSize());
}
- else
+ else
{
%this.brain.setSnapToGrid(0);
}
diff --git a/editor/GuiEditor/gui/theme_sample.gui.taml b/editor/GuiEditor/gui/theme_sample.gui.taml
new file mode 100644
index 000000000..b16af73ec
--- /dev/null
+++ b/editor/GuiEditor/gui/theme_sample.gui.taml
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/editor/GuiEditor/images/controlIcons128.asset.taml b/editor/GuiEditor/images/controlIcons128.asset.taml
new file mode 100644
index 000000000..36989487f
--- /dev/null
+++ b/editor/GuiEditor/images/controlIcons128.asset.taml
@@ -0,0 +1,8 @@
+
diff --git a/editor/GuiEditor/images/controlIcons128.png b/editor/GuiEditor/images/controlIcons128.png
new file mode 100644
index 000000000..6db50d89e
Binary files /dev/null and b/editor/GuiEditor/images/controlIcons128.png differ
diff --git a/editor/GuiEditor/images/controlIcons16.asset.taml b/editor/GuiEditor/images/controlIcons16.asset.taml
new file mode 100644
index 000000000..4342901ce
--- /dev/null
+++ b/editor/GuiEditor/images/controlIcons16.asset.taml
@@ -0,0 +1,8 @@
+
diff --git a/editor/GuiEditor/images/controlIcons16.png b/editor/GuiEditor/images/controlIcons16.png
new file mode 100644
index 000000000..a02e7eba8
Binary files /dev/null and b/editor/GuiEditor/images/controlIcons16.png differ
diff --git a/editor/GuiEditor/images/controlIcons64.asset.taml b/editor/GuiEditor/images/controlIcons64.asset.taml
new file mode 100644
index 000000000..e55469475
--- /dev/null
+++ b/editor/GuiEditor/images/controlIcons64.asset.taml
@@ -0,0 +1,8 @@
+
diff --git a/editor/GuiEditor/images/controlIcons64.png b/editor/GuiEditor/images/controlIcons64.png
new file mode 100644
index 000000000..af7305c12
Binary files /dev/null and b/editor/GuiEditor/images/controlIcons64.png differ
diff --git a/editor/GuiEditor/module.taml b/editor/GuiEditor/module.taml
index 209f270d9..37067c7c8 100644
--- a/editor/GuiEditor/module.taml
+++ b/editor/GuiEditor/module.taml
@@ -5,4 +5,9 @@
Description="Allows for the creation and editing of GUI files."
ScriptFile="GuiEditor.cs"
CreateFunction="create"
- DestroyFunction="destroy" />
+ DestroyFunction="destroy">
+
+
diff --git a/editor/GuiEditor/scripts/GuiEditorAnchorPicker.cs b/editor/GuiEditor/scripts/GuiEditorAnchorPicker.cs
new file mode 100644
index 000000000..d5f4dba30
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiEditorAnchorPicker.cs
@@ -0,0 +1,314 @@
+
+//-----------------------------------------------------------------------------
+// The sizing widget in the Gui Editor's properties pane header: which edges of
+// a control stay put when its parent resizes.
+//
+// Four edge pins say directly what the field means -- which edges stay put --
+// and the readout along the bottom names the pair the Gui file will actually
+// carry, so nothing is hidden.
+//
+// The names below are the anchor set, which now comes first in the engine's
+// tables (guiControl.cc):
+//
+// anchorLeft the left edge stays put
+// anchorRight the right edge stays put
+// width both edges stay; the width follows the parent
+// center neither edge; the control stays centred
+// scale both edges scale with the parent
+// fill position 0, extent = the parent's inner extent
+//
+// The original names said the opposite of what they did -- "right" pinned the
+// LEFT edge, because parentResized has no branch for it and so nothing moves.
+// They still load; they are simply never written any more. This widget only
+// ever speaks the new set.
+//
+// Fill and Scale are not pin states -- fill also zeroes the position and
+// measures against the parent's INNER rect, which no combination of pins can
+// express -- so they are per-axis toggles that supersede the pins. Their rows
+// are labelled H: and V: because two unlabelled rows of identical checkboxes
+// gave no clue which axis was which. The chips are named for the values they
+// set, so "Scale" is now the field's own word rather than this editor's.
+//
+// Nothing here has a fixed width. The pane lays its cells out in a GuiGridCtrl,
+// which resizes each child to the column it computed, so a hard-coded width is
+// overwritten anyway -- and a widget about sizing flags ought to use them. The
+// pin cluster stays pinned left, the labels and the readout follow the width.
+//
+// The creator sets owner inline, then calls build(). Changes are reported to
+// owner.onAnchorChanged(); the widget never writes to a control.
+//-----------------------------------------------------------------------------
+
+function GuiEditorAnchorPicker::onAdd(%this)
+{
+ ThemeManager.setProfile(%this, "emptyProfile");
+}
+
+function GuiEditorAnchorPicker::build(%this)
+{
+ // Tall enough for the pin cluster and the readout beneath it. Height is the
+ // one dimension worth stating: the grid runs in variable-row mode, where a
+ // cell takes the taller of its child and the nominal cell size, so this is
+ // what reserves the room. Width is the grid's to decide.
+ %w = getWord(%this.getExtent(), 0);
+ %this.setExtent(%w, 124);
+
+ %this.title = %this.makeLabel(4, 0, %w - 8, "Anchor", "width");
+
+ // The pin cluster. Each wears the arrow that points at the edge it holds.
+ %this.topPin = %this.makePin(34, 18, "top", $EditorIcon::arrow_top, "Pin the top edge");
+ %this.leftPin = %this.makePin(8, 44, "left", $EditorIcon::arrow_left, "Pin the left edge");
+ %this.rightPin = %this.makePin(60, 44, "right", $EditorIcon::arrow_right, "Pin the right edge");
+ %this.bottomPin = %this.makePin(34, 70, "bottom", $EditorIcon::arrow_bottom, "Pin the bottom edge");
+
+ // The two special modes, one row per axis and each row named, so it is
+ // never a guess which axis a checkbox belongs to.
+ %this.hLabel = %this.makeLabel(96, 22, 20, "H:", "right");
+ %this.hFill = %this.makeChip(118, 20, 56, "h", "fill", "Fill",
+ "Fill the parent horizontally");
+ %this.hRel = %this.makeChip(178, 20, 80, "h", "scale", "Scale",
+ "Scale both edges with the parent's width");
+
+ %this.vLabel = %this.makeLabel(96, 62, 20, "V:", "right");
+ %this.vFill = %this.makeChip(118, 60, 56, "v", "fill", "Fill",
+ "Fill the parent vertically");
+ %this.vRel = %this.makeChip(178, 60, 80, "v", "scale", "Scale",
+ "Scale both edges with the parent's height");
+
+ // The resolved pair, along the bottom where it reads as the answer rather
+ // than as another control.
+ %this.readout = %this.makeLabel(8, 98, %w - 16, "", "width");
+}
+
+// %sizing is the raw enum, so "right" means pinned to the left and "width"
+// means both edges -- the very naming this widget exists to hide, spelled out
+// here because these are the only four places it is written by hand.
+function GuiEditorAnchorPicker::makeLabel(%this, %x, %y, %w, %text, %sizing)
+{
+ %label = new GuiControl()
+ {
+ HorizSizing = %sizing;
+ Position = %x SPC %y;
+ Extent = %w SPC 20;
+ Text = %text;
+ align = "left";
+ vAlign = "middle";
+ };
+ ThemeManager.setProfile(%label, "labelProfile");
+ %this.add(%label);
+ return %label;
+}
+
+// A pin is a toggle, so it is a checkbox rather than a button: the state has to
+// stay put, and only a checkbox refuses to act while it is disabled.
+function GuiEditorAnchorPicker::makePin(%this, %x, %y, %edge, %frame, %tip)
+{
+ %pin = new GuiCheckBoxCtrl()
+ {
+ class = "EditorToggleIcon";
+ Position = %x SPC %y;
+ Extent = "24 24";
+ frameOff = %frame;
+ frameOn = %frame;
+ tipOn = %tip;
+ tipOff = %tip;
+ toggleName = %edge;
+ owner = %this;
+ };
+ ThemeManager.setProfile(%pin, "iconButtonProfile");
+ ThemeManager.setProfile(%pin, "tipProfile", "TooltipProfile");
+ %this.add(%pin);
+ return %pin;
+}
+
+function GuiEditorAnchorPicker::makeChip(%this, %x, %y, %w, %axis, %mode, %text, %tip)
+{
+ %chip = new GuiCheckBoxCtrl()
+ {
+ Position = %x SPC %y;
+ Extent = %w SPC 22;
+ Text = %text;
+ boxOffset = "0 1";
+ boxExtent = "16 16";
+ textOffset = "19 1";
+ textExtent = (%w - 19) SPC 21;
+ Tooltip = %tip;
+ Command = %this.getID() @ ".onChipClicked(\"" @ %axis @ "\",\"" @ %mode @ "\");";
+ };
+ ThemeManager.setProfile(%chip, "checkboxProfile");
+ ThemeManager.setProfile(%chip, "tipProfile", "TooltipProfile");
+ %this.add(%chip);
+ return %chip;
+}
+
+//-----------------------------------------------------------------------------
+// The enum on both sides of the widget. These two functions are the whole of
+// the naming problem, kept together so the inversion is visible in one place.
+//-----------------------------------------------------------------------------
+
+function GuiEditorAnchorPicker::readEnums(%this, %horiz, %vert)
+{
+ %this.hSpecial = "";
+ %this.vSpecial = "";
+ %this.pinLeft = false;
+ %this.pinRight = false;
+ %this.pinTop = false;
+ %this.pinBottom = false;
+
+ // Both name sets are read, because a Gui written before the rename still
+ // carries the old ones and the engine will happily hand them back if the
+ // field was set from such a file in the same session.
+ switch$(%horiz)
+ {
+ case "fill": %this.hSpecial = "fill";
+ case "scale" or "relative": %this.hSpecial = "scale";
+ case "width": %this.pinLeft = true; %this.pinRight = true;
+ case "anchorLeft" or "right": %this.pinLeft = true;
+ case "anchorRight" or "left": %this.pinRight = true;
+ // "center" pins neither, which is the remaining state.
+ }
+
+ switch$(%vert)
+ {
+ case "fill": %this.vSpecial = "fill";
+ case "scale" or "relative": %this.vSpecial = "scale";
+ case "height": %this.pinTop = true; %this.pinBottom = true;
+ case "anchorTop" or "bottom": %this.pinTop = true;
+ case "anchorBottom" or "top": %this.pinBottom = true;
+ }
+
+ %this.updateWidgets();
+}
+
+function GuiEditorAnchorPicker::horizEnum(%this)
+{
+ if(%this.hSpecial !$= "")
+ {
+ return %this.hSpecial;
+ }
+ if(%this.pinLeft && %this.pinRight)
+ {
+ return "width";
+ }
+ if(%this.pinLeft)
+ {
+ return "anchorLeft";
+ }
+ if(%this.pinRight)
+ {
+ return "anchorRight";
+ }
+ return "center";
+}
+
+function GuiEditorAnchorPicker::vertEnum(%this)
+{
+ if(%this.vSpecial !$= "")
+ {
+ return %this.vSpecial;
+ }
+ if(%this.pinTop && %this.pinBottom)
+ {
+ return "height";
+ }
+ if(%this.pinTop)
+ {
+ return "anchorTop";
+ }
+ if(%this.pinBottom)
+ {
+ return "anchorBottom";
+ }
+ return "center";
+}
+
+//-----------------------------------------------------------------------------
+// Interaction.
+//-----------------------------------------------------------------------------
+
+// A pin toggled itself; read its state back rather than flipping our own copy,
+// so the widget and the checkbox can never disagree.
+function GuiEditorAnchorPicker::onToggleIconChanged(%this, %pin)
+{
+ if(%this.populating)
+ {
+ return;
+ }
+
+ // A pin and a special cannot both be in effect, so touching a pin takes
+ // that axis back to the pins.
+ switch$(%pin.toggleName)
+ {
+ case "left": %this.pinLeft = %pin.getStateOn(); %this.hSpecial = "";
+ case "right": %this.pinRight = %pin.getStateOn(); %this.hSpecial = "";
+ case "top": %this.pinTop = %pin.getStateOn(); %this.vSpecial = "";
+ case "bottom": %this.pinBottom = %pin.getStateOn(); %this.vSpecial = "";
+ }
+
+ %this.updateWidgets();
+ %this.owner.onAnchorChanged(%this);
+}
+
+function GuiEditorAnchorPicker::onChipClicked(%this, %axis, %mode)
+{
+ if(%this.populating)
+ {
+ return;
+ }
+
+ // Clicking the mode that is already on turns it off, which drops the axis
+ // back to whatever its pins say.
+ if(%axis $= "h")
+ {
+ %this.hSpecial = (%this.hSpecial $= %mode) ? "" : %mode;
+ }
+ else
+ {
+ %this.vSpecial = (%this.vSpecial $= %mode) ? "" : %mode;
+ }
+
+ %this.updateWidgets();
+ %this.owner.onAnchorChanged(%this);
+}
+
+// Show the current state. A pin is on only when its axis is actually being
+// driven by pins -- with Fill in effect the pins are not what is happening, so
+// they read as off.
+function GuiEditorAnchorPicker::updateWidgets(%this)
+{
+ %this.populating = true;
+
+ %hPinned = %this.hSpecial $= "";
+ %vPinned = %this.vSpecial $= "";
+
+ %this.leftPin.setValue(%this.pinLeft && %hPinned);
+ %this.rightPin.setValue(%this.pinRight && %hPinned);
+ %this.topPin.setValue(%this.pinTop && %vPinned);
+ %this.bottomPin.setValue(%this.pinBottom && %vPinned);
+
+ %this.hFill.setStateOn(%this.hSpecial $= "fill");
+ %this.hRel.setStateOn(%this.hSpecial $= "scale");
+ %this.vFill.setStateOn(%this.vSpecial $= "fill");
+ %this.vRel.setStateOn(%this.vSpecial $= "scale");
+
+ %this.readout.setText(%this.horizEnum() @ " / " @ %this.vertEnum());
+
+ %this.populating = false;
+}
+
+// Grey the axis where the parent owns the sizing anyway. Everything here is a
+// checkbox, which will not act while inactive, so this disables the behaviour
+// as well as the look.
+function GuiEditorAnchorPicker::setAxisEnabled(%this, %horiz, %vert)
+{
+ %this.leftPin.setActive(%horiz);
+ %this.rightPin.setActive(%horiz);
+ %this.hFill.setActive(%horiz);
+ %this.hRel.setActive(%horiz);
+ %this.hLabel.setActive(%horiz);
+
+ %this.topPin.setActive(%vert);
+ %this.bottomPin.setActive(%vert);
+ %this.vFill.setActive(%vert);
+ %this.vRel.setActive(%vert);
+ %this.vLabel.setActive(%vert);
+}
diff --git a/editor/GuiEditor/scripts/GuiEditorBrain.cs b/editor/GuiEditor/scripts/GuiEditorBrain.cs
index b77f14935..5c38cc2e2 100644
--- a/editor/GuiEditor/scripts/GuiEditorBrain.cs
+++ b/editor/GuiEditor/scripts/GuiEditorBrain.cs
@@ -5,8 +5,34 @@
%this.setSnapToGrid("10");
}
+//-----------------------------------------------------------------------------
+// A control arriving by drag.
+//
+// Both of these are only ever called about a point that is over the canvas -
+// except that they are not. GuiDragAndDropCtrl hit-tests from the drag control's
+// PARENT, which is this control, and GuiControl::findHitControl answers "me"
+// when none of its children was hit, whatever the point it was asked about. So
+// this hears about a drop anywhere on the screen: over the palette, over the
+// explorer, over the menu bar. Left to itself it then adds the control at the
+// cursor, which is how dragging one back onto the palette to change your mind
+// put a control behind the palette.
+//
+// So each of them asks first. A drop that is not over the canvas is not a drop:
+// nothing is added, and the payload dies with the drag control that carried it
+// (deleteOnMouseUp, and a group deletes what it holds).
+//-----------------------------------------------------------------------------
+
function GuiEditorBrain::onControlDragged(%this, %payload, %position)
{
+ // Off the canvas: keep the container that is being worked in. Hit-testing a
+ // point outside the Gui answers the Gui itself, so without this a drag that
+ // strayed over a tool window quietly reset the add set to the root - and the
+ // click gesture places into the add set, so the next click moved with it.
+ if(!%this.isOverCanvas(%this.cursorFrom(%payload)))
+ {
+ return;
+ }
+
%x = getWord(%position, 0);
%y = getWord(%position, 1);
%target = %this.root.findHitControl(%x, %y);
@@ -23,19 +49,388 @@
}
function GuiEditorBrain::onControlDropped(%this, %payload, %position)
+{
+ if(!%this.isOverCanvas(%this.cursorFrom(%payload)))
+ {
+ return;
+ }
+
+ %this.placeControl(%payload);
+}
+
+// A control arriving from a gesture that has no cursor to be off the canvas -
+// which is what clicking a palette tile is. The position was worked out from the
+// container being worked in and is inside it by construction, so there is
+// nothing left to police.
+//
+// It has to be a door of its own rather than the drag's, because the drag's
+// question cannot be asked of every control. A GuiMenuBarCtrl pins itself to its
+// parent's origin - resize throws away the position it is handed - so its
+// payload sits at 0,0 however it is placed, the cursor test measures a point up
+// in the editor's own chrome, and clicking Menu Bar in the palette did nothing
+// at all.
+function GuiEditorBrain::placeControl(%this, %payload)
{
%pos = %payload.getGlobalPosition();
%x = getWord(%pos, 0);
%y = getWord(%pos, 1);
- %this.addNewCtrl(%payload);
+ %this.acceptControl(%payload);
+
%payload.setPositionGlobal(%x, %y);
- %this.setFirstResponder();
- %this.postEvent("AddControl", %payload);
- %this.postEvent("Inspect", %payload);
%this.schedule(40, "finishControlDropped", %payload, %x, %y);
}
+//-----------------------------------------------------------------------------
+// Where the canvas is, and where the pointer is over it.
+//
+// Everything here is in global coordinates. The position the two callbacks above
+// are handed is not: GuiDragAndDropCtrl::sendDragEvent builds it from the drag
+// control's own bounds, which are local to this control - which is why neither
+// of them measures anything with it, and why the payload is asked instead. A
+// drag grabs a control by the middle (see GuiEditorControlTile::beginDrag), so
+// the middle of the payload is where the pointer is.
+//-----------------------------------------------------------------------------
+
+function GuiEditorBrain::cursorFrom(%this, %payload)
+{
+ %at = %payload.getGlobalPosition();
+ %size = %payload.getExtent();
+
+ return mFloor(getWord(%at, 0) + (getWord(%size, 0) / 2)) SPC
+ mFloor(getWord(%at, 1) + (getWord(%size, 1) / 2));
+}
+
+function GuiEditorBrain::isOverCanvas(%this, %point)
+{
+ %canvas = %this.canvasRect();
+ %x = getWord(%point, 0);
+ %y = getWord(%point, 1);
+
+ return %x >= getWord(%canvas, 0) && %y >= getWord(%canvas, 1) &&
+ %x < getWord(%canvas, 0) + getWord(%canvas, 2) &&
+ %y < getWord(%canvas, 1) + getWord(%canvas, 3);
+}
+
+// "x y width height" of the Gui being edited.
+function GuiEditorBrain::canvasRect(%this)
+{
+ return %this.root.getGlobalPosition() SPC %this.root.getExtent();
+}
+
+// Where a control goes when the gesture never said - which is what a click on a
+// palette tile is: the middle of the container being worked in, or of as much of
+// that container as the canvas can show.
+//
+// The clipping is not fussiness. A Gui is authored at its own size, usually
+// 1024x768, and the canvas frame is a few hundred pixels wide, so a container
+// runs off the side of it more often than not - and the middle of one that does
+// is behind the palette or the explorer, where the control cannot be seen or
+// dragged. Clipping first means a container that fits, which is every container
+// in a Gui the size of the canvas, still gets its own exact middle.
+function GuiEditorBrain::centredPlacement(%this, %payload)
+{
+ %target = %this.getCurrentAddSet();
+ if(!isObject(%target))
+ {
+ %target = %this.root;
+ }
+
+ %room = %this.visiblePartOf(%target);
+ %size = %payload.getExtent();
+
+ %x = getWord(%room, 0) + ((getWord(%room, 2) - getWord(%size, 0)) / 2);
+ %y = getWord(%room, 1) + ((getWord(%room, 3) - getWord(%size, 1)) / 2);
+
+ return mFloor(%x) SPC mFloor(%y);
+}
+
+// What a control and the canvas share, as "x y width height". A container with
+// nothing on the canvas at all answers the whole canvas: there is no good
+// placement inside it, and off-screen is not a better answer than visible.
+function GuiEditorBrain::visiblePartOf(%this, %ctrl)
+{
+ %canvas = %this.canvasRect();
+ %at = %ctrl.getGlobalPosition();
+ %size = %ctrl.getExtent();
+
+ %left = mFloor(mGetMax(getWord(%at, 0), getWord(%canvas, 0)));
+ %top = mFloor(mGetMax(getWord(%at, 1), getWord(%canvas, 1)));
+ %right = mFloor(mGetMin(getWord(%at, 0) + getWord(%size, 0),
+ getWord(%canvas, 0) + getWord(%canvas, 2)));
+ %bottom = mFloor(mGetMin(getWord(%at, 1) + getWord(%size, 1),
+ getWord(%canvas, 1) + getWord(%canvas, 3)));
+
+ if(%right <= %left || %bottom <= %top)
+ {
+ return %canvas;
+ }
+
+ return %left SPC %top SPC (%right - %left) SPC (%bottom - %top);
+}
+
+// Everything about a control arriving in the document except where it lands.
+//
+// A drop knows where the cursor was and so places the control in global
+// coordinates, twice, because the container it landed in may have moved it. A
+// paste has no cursor: it knows the position the control held in its old parent,
+// which is a local one, and sets it before calling this - so the properties pane
+// reads the position the control is going to keep. Everything else is the same
+// for both, and lives here rather than in each caller: the undo record, the
+// theme, the selection, and the events the Explorer tree and the panes listen
+// for.
+function GuiEditorBrain::acceptControl(%this, %payload)
+{
+ // The add itself is the undo step, recorded from onAddNewCtrl below.
+ %this.addNewCtrl(%payload);
+
+ // Between the two, so the page is in the book before the theme walks it and
+ // after the book's own add has been recorded. A tab book with no pages is a
+ // book with nothing to put anything in, and the palette no longer offers the
+ // page that would fix that.
+ //
+ // The page is added with a plain add(), which announces nothing, so nothing
+ // records it: undo of the drop puts the book in the trash with its page
+ // inside, which is one step, which is what the user did.
+ if(%payload.isMemberOfClass("GuiTabBookCtrl") && %payload.getCount() == 0)
+ {
+ %this.newTabPage(%payload);
+ }
+
+ // And the same for a menu bar, for the same reason and more sharply: the
+ // palette has never offered a GuiMenuItemCtrl, so before the "+" there was no
+ // way whatsoever to put anything in one.
+ if(%payload.isMemberOfClass("GuiMenuBarCtrl") && %payload.getCount() == 0)
+ {
+ %this.newMenuItem(%payload);
+ }
+
+ %this.adoptControl(%payload);
+}
+
+// The half of arriving that is the same however a control got here: the theme it
+// takes on, and the events that tell the Explorer tree and the panes about it.
+//
+// Split out because a tab page arrives by neither of the routes above. Its book
+// makes it, from the "+" tab, and it needs all of this and none of the
+// placement.
+//
+// The undo record is deliberately NOT here, because what counts as one step
+// differs by caller: a dropped control is a step of its own, and a page seeded
+// inside a book that is itself arriving is part of that book arriving.
+function GuiEditorBrain::adoptControl(%this, %ctrl)
+{
+ // A control arrives wearing whatever its C++ constructor named - a
+ // GuiWindowCtrl names five, from GuiWindowProfile down - so put it on the
+ // Gui's theme straight away. Themed on arrival is the whole point: the drop
+ // is the last time anyone should have to think about which profile a button
+ // wants.
+ //
+ // This has to run after the control has a parent, because the parent decides
+ // which category it takes (a control sitting directly on the root is the
+ // Gui's backdrop and gets Panel, not Label). But adding it is also what
+ // announces the selection, so everything that inspects the control has
+ // already read it wearing the constructor's profiles. Rethemed tells them to
+ // look again.
+ //
+ // None of it is recorded: theming a control that is arriving is part of it
+ // arriving, not a second thing the user did. Undo puts the whole control in
+ // the trash, where it keeps its profiles and its position, so redo has
+ // nothing to put back but the control itself.
+ %theme = GuiEditor.themeByName(GuiEditor.themeName);
+ if(isObject(%theme))
+ {
+ GuiEditor.undoRecorder.suspend();
+ GuiEditor.themeApplier.applyToBranch(%ctrl, %theme, false);
+ GuiEditor.undoRecorder.resume();
+
+ %this.postEvent("Rethemed", %ctrl);
+ }
+
+ %this.setFirstResponder();
+ %this.postEvent("AddControl", %ctrl);
+ %this.postEvent("Inspect", %ctrl);
+}
+
+//-----------------------------------------------------------------------------
+// Tab pages.
+//
+// A GuiTabPageCtrl is the one control the palette does not offer, because it is
+// the one control that means nothing anywhere but inside a GuiTabBookCtrl. A
+// book makes its own instead: one when the book is dropped, and one for every
+// click on the "+" tab it draws at the end of its strip while the Gui is being
+// authored.
+//-----------------------------------------------------------------------------
+
+// GuiTabBookCtrl::requestNewPage, from a click on the "+" tab.
+function GuiEditorBrain::onAddTabPage(%this, %book)
+{
+ if(!isObject(%book))
+ {
+ return;
+ }
+
+ GuiEditor.undoRecorder.begin("Add Tab Page", "");
+ %page = %this.newTabPage(%book);
+ GuiEditor.undoRecorder.recordAdd(%page, "Add Tab Page");
+ %this.adoptControl(%page);
+ GuiEditor.undoRecorder.end();
+
+ // By index, never by name: captions are not identities - two pages can both
+ // read "Page 3" once one has been deleted - and selectPageName takes the
+ // first match.
+ %book.selectPage(%book.getCount() - 1);
+
+ // The add set first, because setting it clears the selection. Pointing it at
+ // the new page means the next control dropped lands in the page that was
+ // just made, which is the only reason anyone clicks "+".
+ %this.setCurrentAddSet(%page);
+ %this.selectList(%page);
+}
+
+// The one place a page is made, so that a page seeded with its book and a page
+// added later are the same object.
+function GuiEditorBrain::newTabPage(%this, %book)
+{
+ %number = %this.freePageNumber(%book);
+
+ %page = new GuiTabPageCtrl()
+ {
+ Text = "Page " @ %number;
+ };
+
+ // What C++ addNewPage names, so a book built in a project with no theme
+ // loaded still gets a page that draws like one. adoptControl writes over it
+ // a moment later wherever there IS a theme.
+ if(isObject(GuiTabPageProfile))
+ {
+ %page.setProfile(GuiTabPageProfile);
+ }
+
+ %book.add(%page);
+
+ return %page;
+}
+
+// The lowest number no tab in the book is already using. Counting the pages
+// instead would repeat one: three pages, delete "Page 2", and the next page
+// would be a second "Page 3".
+//
+// Bounded rather than open: among the numbers 1 to N+1 at least one is free of N
+// pages, so the loop always finds an answer inside it.
+function GuiEditorBrain::freePageNumber(%this, %book)
+{
+ %count = %book.getCount();
+
+ for(%n = 1; %n <= (%count + 1); %n++)
+ {
+ %taken = false;
+ for(%i = 0; %i < %count; %i++)
+ {
+ if(%book.getObject(%i).getText() $= ("Page " @ %n))
+ {
+ %taken = true;
+ break;
+ }
+ }
+
+ if(!%taken)
+ {
+ return %n;
+ }
+ }
+
+ return %count + 1;
+}
+
+//-----------------------------------------------------------------------------
+// Menu items.
+//
+// The same arrangement as tab pages above, one level deeper. A GuiMenuItemCtrl
+// is not in the palette either, and it nests: a bar holds menus, and each menu
+// holds the commands. So the bar draws a "+" after the last menu, and an open
+// menu draws a "+" at the foot of its list, and both arrive here.
+//-----------------------------------------------------------------------------
+
+// GuiMenuBarCtrl::requestNewMenuItem, from a click on either "+". %parent is the
+// menu to put it in, or empty for a top-level one.
+function GuiEditorBrain::onAddMenuItem(%this, %bar, %parent)
+{
+ if(!isObject(%bar))
+ {
+ return;
+ }
+
+ if(!isObject(%parent))
+ {
+ %parent = %bar;
+ }
+
+ GuiEditor.undoRecorder.begin("Add Menu Item", "");
+ %item = %this.newMenuItem(%parent);
+ GuiEditor.undoRecorder.recordAdd(%item, "Add Menu Item");
+ %this.adoptControl(%item);
+ GuiEditor.undoRecorder.end();
+
+ // The add set first, because setting it clears the selection. Selecting the
+ // new item is also what opens the menu it went into - the bar works out which
+ // menu to show from the selection - so a menu made by the bar's "+" is
+ // already open and waiting for its first command.
+ %this.setCurrentAddSet(%item);
+ %this.selectList(%item);
+}
+
+// The one place a menu item is made, so that an item seeded with its bar and an
+// item added later are the same object.
+function GuiEditorBrain::newMenuItem(%this, %parent)
+{
+ %number = %this.freeMenuNumber(%parent);
+
+ %item = new GuiMenuItemCtrl()
+ {
+ Text = "Menu " @ %number;
+ };
+
+ // Added to its parent before it is given anything of its own: a menu item
+ // learns which bar it belongs to from the parent it arrives in, and reads
+ // that back the moment it gains a child.
+ %parent.add(%item);
+
+ return %item;
+}
+
+// The lowest number no item in this parent is already using. Numbering is per
+// parent, so each menu's commands count from 1 rather than carrying on from the
+// bar's. Counting the children instead would repeat one: three items, delete the
+// second, and the next would be a second "Menu 3".
+//
+// Bounded rather than open: among the numbers 1 to N+1 at least one is free of N
+// items, so the loop always finds an answer inside it.
+function GuiEditorBrain::freeMenuNumber(%this, %parent)
+{
+ %count = %parent.getCount();
+
+ for(%n = 1; %n <= (%count + 1); %n++)
+ {
+ %taken = false;
+ for(%i = 0; %i < %count; %i++)
+ {
+ if(%parent.getObject(%i).getText() $= ("Menu " @ %n))
+ {
+ %taken = true;
+ break;
+ }
+ }
+
+ if(!%taken)
+ {
+ return %n;
+ }
+ }
+
+ return %count + 1;
+}
+
function GuiEditorBrain::finishControlDropped(%this, %payload, %x, %y)
{
%payload.setPositionGlobal(%x, %y);
@@ -122,33 +517,136 @@
%this.toggleMenuItems();
}
+// Something else asked for the selection to go. The Explorer tree does, from its
+// own Delete key.
function GuiEditorBrain::onObjectRemoved(%this, %ctrl)
{
%this.startRadioSilence();
%this.deleteSelection();
%this.endRadioSilence();
- %this.toggleMenuItems();
+
+ // Then say that it went, which the receiving half of this bus does not
+ // normally do. It has to here: postEvent does not deliver to whoever posted,
+ // so the tree that asked for this delete has told its own window nothing,
+ // and the window is what refreshes it. Announcing from here rather than at
+ // each caller is what makes every route into a delete - this one, the Delete
+ // key on the canvas, and Cut - leave the panes agreeing with the document.
+ //
+ // onDelete carries the menu update with it, so there is none of its own.
+ %this.onDelete();
+}
+
+//-----------------------------------------------------------------------------
+// Undo. The C++ edit control has always announced every edit it makes at the
+// moment it makes it - guiEditCtrl.cc calls all of these, and each one sits
+// beside a bare "// undo" comment marking where the recording used to happen.
+// Nothing implemented them until now.
+//
+// The pairs matter: what a drag or a nudge did is only known once it is over,
+// so the pre half remembers where everything was and the post half works out
+// what actually moved. A mouse-down that only selected records nothing.
+//-----------------------------------------------------------------------------
+
+// Mouse-down on a selection, before a drag-move or a handle-resize, and the
+// mouse-up that ends it. The pair is the whole gesture, and the gesture is one
+// undo step however many times the C++ moves the selection inside it - and
+// whatever else it does in there, which includes reparenting the selection into
+// whatever container the pointer wandered over. See
+// GuiEditorUndoRecorder::beginGesture.
+function GuiEditorBrain::onPreEdit(%this, %selection)
+{
+ GuiEditor.undoRecorder.beginGesture(%selection);
+}
+
+// The selection is handed over again here and deliberately not used: what the
+// gesture has to be measured against is the snapshot taken when it began.
+function GuiEditorBrain::onPostEdit(%this, %selection)
+{
+ GuiEditor.undoRecorder.endGesture();
+}
+
+// Arrow-key and menu nudges, which the C++ gives a callback of their own
+// precisely so that a run of them can be folded into one action.
+function GuiEditorBrain::onPreSelectionNudged(%this, %selection)
+{
+ GuiEditor.undoRecorder.snapshot(%selection);
+}
+
+function GuiEditorBrain::onPostSelectionNudged(%this, %selection)
+{
+ GuiEditor.undoRecorder.commitGeometry("", "nudge");
+}
+
+// Fired before the controls are moved to the trash, which is the only moment
+// where each one still knows the parent and index it has to go back to.
+function GuiEditorBrain::onTrashSelection(%this, %selection)
+{
+ GuiEditor.undoRecorder.recordDeleteSelection(%selection);
+}
+
+// Fired after the control has been put in the add set.
+function GuiEditorBrain::onAddNewCtrl(%this, %ctrl)
+{
+ GuiEditor.undoRecorder.recordAdd(%ctrl, "");
+}
+
+// Put the selection on the controls a replay changed, and tell everyone.
+function GuiEditorBrain::restoreSelection(%this, %list)
+{
+ %this.selectList(%list);
+}
+
+// Select exactly these controls, and say so.
+//
+// The announcement has to be made here, because addSelection makes none:
+// it is the receiving half of the bus - what this class calls, under radio
+// silence, when the tree or the pane has already announced a selection - so it
+// changes the C++ selection and says nothing. Calling it on its own leaves the
+// canvas drawing handles round a control the properties pane has never heard
+// of, and the clearSelection ahead of it has already emptied the pane.
+function GuiEditorBrain::selectList(%this, %list)
+{
+ // Already the selection, with values that changed underneath it - which is
+ // the commonest undo there is: change a setting, press Ctrl+Z. Re-announcing
+ // would rebuild the whole properties pane, when all it needs is to re-read
+ // the control it is already showing.
+ if(%list $= %this.selectionList())
+ {
+ %this.postEvent("Replayed");
+ return;
+ }
+
+ %this.clearSelection();
+
+ for(%i = 0; %i < getWordCount(%list); %i++)
+ {
+ %ctrl = getWord(%list, %i);
+ %this.addSelection(%ctrl);
+
+ // What the C++ would have called had it done the selecting, so there is
+ // one definition of what announcing a selection means.
+ %this.onAddSelected(%ctrl);
+ }
+}
+
+function GuiEditorBrain::selectionList(%this)
+{
+ %set = %this.getSelected();
+ %list = "";
+
+ for(%i = 0; %i < %set.getCount(); %i++)
+ {
+ %ctrl = %set.getObject(%i);
+ %list = (%list $= "") ? %ctrl : (%list SPC %ctrl);
+ }
+
+ return %list;
}
+// Everything in Layout and Select, and the half of Edit that acts on controls,
+// answers one question: how much is selected. The menus know which items belong
+// to which threshold; all this has to say is the number.
function GuiEditorBrain::toggleMenuItems(%this)
{
- %count = %this.getSelected().getCount();
- EditorCore.menuBar.setMenuActive("Deselect", %count != 0);
- EditorCore.menuBar.setMenuActive("Nudge Up", %count != 0);
- EditorCore.menuBar.setMenuActive("Nudge Down", %count != 0);
- EditorCore.menuBar.setMenuActive("Nudge Left", %count != 0);
- EditorCore.menuBar.setMenuActive("Nudge Right", %count != 0);
- EditorCore.menuBar.setMenuActive("Expand Height", %count != 0);
- EditorCore.menuBar.setMenuActive("Shrink Height", %count != 0);
- EditorCore.menuBar.setMenuActive("Expand Width", %count != 0);
- EditorCore.menuBar.setMenuActive("Shrink Width", %count != 0);
- EditorCore.menuBar.setMenuActive("Align Top", %count > 1);
- EditorCore.menuBar.setMenuActive("Align Bottom", %count > 1);
- EditorCore.menuBar.setMenuActive("Align Left", %count > 1);
- EditorCore.menuBar.setMenuActive("Align Right", %count > 1);
- EditorCore.menuBar.setMenuActive("Center Horizontally", %count > 1);
- EditorCore.menuBar.setMenuActive("Space Vertically", %count > 2);
- EditorCore.menuBar.setMenuActive("Space Horizontally", %count > 2);
- EditorCore.menuBar.setMenuActive("Bring to Front", %count == 1);
- EditorCore.menuBar.setMenuActive("Push to Back", %count == 1);
+ GuiEditor.menus.refreshSelection(%this.getSelected().getCount());
}
\ No newline at end of file
diff --git a/editor/GuiEditor/scripts/GuiEditorClipboard.cs b/editor/GuiEditor/scripts/GuiEditorClipboard.cs
new file mode 100644
index 000000000..d416efd0c
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiEditorClipboard.cs
@@ -0,0 +1,541 @@
+
+//-----------------------------------------------------------------------------
+// The Gui Editor's clipboard. Owned by GuiEditor; the only thing that holds a
+// copied control, and the only thing that puts one back.
+//
+// Copying controls is the subject rather than the stash in particular, which is
+// why Duplicate lives here too: it is a copy that goes straight back where it
+// came from without ever being held.
+//
+// A copy is a real copy, made at the moment Ctrl+C is pressed: the selection is
+// deep-cloned into a SimGroup of this object's own, outside the document. That
+// is what makes the clipboard a snapshot rather than a reference - editing or
+// deleting the original afterwards does not change what will be pasted - and it
+// is why a paste clones a second time, out of the stash, so the stash survives
+// and can be pasted again.
+//
+// SimObject::deepClone is what does the copying (simObject.cc). Two things it
+// promises are the reason a clipboard can be this small: it copies everything -
+// fields, dynamic fields, the whole child tree, and a frame set's frame tree -
+// and it runs no script lifecycle on the copy, so a control whose class builds
+// children in onAdd is copied with the children it has rather than gaining a
+// second set of them.
+//
+// What it deliberately does not copy is the name, because two controls
+// answering to one name is a bug. The originals' names are carried across on
+// the copy as a dynamic field, and paste turns them back into real names, made
+// unique against the document.
+//
+// Holds live controls, so it has to be emptied when the profiles they wear are
+// about to be freed - see GuiEditor::detachTheme, which does the same for the
+// undo stack and for the same reason.
+//-----------------------------------------------------------------------------
+
+function GuiEditorClipboard::onAdd(%this)
+{
+ // Where copies live. A SimGroup owns what it holds, so deleting it at
+ // teardown frees every copied tree with it.
+ %this.stash = new SimGroup();
+
+ %this.entryCount = 0;
+
+ // Paste placement, counted per container: a paste into the container the copy
+ // came from steps away from the original so it can be seen, and each further
+ // paste into that container steps again. Kept per container rather than as one
+ // running count so that pasting into a second container and coming back
+ // carries on where this one left off, instead of landing on what is already
+ // there.
+ %this.stepCount = 0;
+}
+
+function GuiEditorClipboard::onRemove(%this)
+{
+ if(isObject(%this.stash))
+ {
+ %this.stash.delete();
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Copying.
+//-----------------------------------------------------------------------------
+
+function GuiEditorClipboard::copy(%this, %selection)
+{
+ if(!isObject(%selection) || %selection.getCount() == 0)
+ {
+ return false;
+ }
+
+ %roots = %this.topLevel(%selection);
+ if(%roots $= "")
+ {
+ return false;
+ }
+
+ %this.clear();
+
+ for(%i = 0; %i < getWordCount(%roots); %i++)
+ {
+ %ctrl = getWord(%roots, %i);
+ %copy = %ctrl.deepClone();
+ if(!isObject(%copy))
+ {
+ continue;
+ }
+
+ // Out of the Gui group a fresh control registers itself into
+ // (guiControl.cc onAdd) and into the stash, which is where it stays.
+ %this.stash.add(%copy);
+ %this.stampNames(%ctrl, %copy);
+
+ %n = %this.entryCount;
+ %this.entry[%n] = %copy;
+ %this.entrySource[%n] = %ctrl.getParent();
+ %this.entryCount = %n + 1;
+ }
+
+ %this.stepCount = 0;
+ %this.refreshMenu();
+
+ return %this.entryCount > 0;
+}
+
+// The controls a copy actually has to take: those in the selection that no other
+// selected control already contains. Selecting a panel and a button inside it is
+// one copy, not two - the button is coming along inside the panel, and copying
+// it again would paste a second one.
+//
+// Found by walking the document rather than reading the selection in order,
+// which also settles the order: the entries come out in document order, so the
+// pasted controls sit in the same z-order as the originals.
+function GuiEditorClipboard::topLevel(%this, %selection)
+{
+ if(!isObject(%this.owner.rootGui))
+ {
+ return "";
+ }
+
+ return %this.collectTopLevel(%this.owner.rootGui, %selection);
+}
+
+function GuiEditorClipboard::collectTopLevel(%this, %parent, %selection)
+{
+ %list = "";
+
+ for(%i = 0; %i < %parent.getCount(); %i++)
+ {
+ %child = %parent.getObject(%i);
+
+ if(%selection.isMember(%child))
+ {
+ // Taken whole, and the walk stops here: anything selected below it is
+ // part of what was taken.
+ %list = (%list $= "") ? %child : (%list SPC %child);
+ continue;
+ }
+
+ %deeper = %this.collectTopLevel(%child, %selection);
+ if(%deeper !$= "")
+ {
+ %list = (%list $= "") ? %deeper : (%list SPC %deeper);
+ }
+ }
+
+ return %list;
+}
+
+// Remember what every control in the copied tree was called, since deepClone
+// deliberately leaves names behind. Kept on the copy itself rather than in a
+// table here, so a tree of any shape carries its own answers; paste consumes the
+// field and clears it.
+//
+// Walked by index, which pairs the two trees: a deep clone copies children in
+// order, so the source's nth child and the copy's nth child are the same
+// control.
+function GuiEditorClipboard::stampNames(%this, %source, %copy)
+{
+ %name = %source.getName();
+ if(%name !$= "")
+ {
+ %copy.clipName = %name;
+ }
+
+ %count = %source.getCount();
+ for(%i = 0; %i < %count && %i < %copy.getCount(); %i++)
+ {
+ %this.stampNames(%source.getObject(%i), %copy.getObject(%i));
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Pasting.
+//-----------------------------------------------------------------------------
+
+function GuiEditorClipboard::paste(%this)
+{
+ if(%this.isEmpty())
+ {
+ return false;
+ }
+
+ %addSet = %this.owner.brain.getCurrentAddSet();
+ if(!isObject(%addSet))
+ {
+ %addSet = %this.owner.rootGui;
+ }
+
+ %offset = %this.nextOffset(%addSet);
+ %pasted = "";
+
+ // However many controls it puts back, a paste is one thing the user did and
+ // so one thing they can take back. The adds inside record themselves into
+ // this transaction (GuiEditorUndoRecorder::recordAdd, from the brain's
+ // onAddNewCtrl).
+ %this.owner.undoRecorder.begin("Paste", "");
+
+ for(%i = 0; %i < %this.entryCount; %i++)
+ {
+ %entry = %this.entry[%i];
+ if(!isObject(%entry))
+ {
+ continue;
+ }
+
+ %copy = %entry.deepClone();
+ if(!isObject(%copy))
+ {
+ continue;
+ }
+
+ // Not everything can live everywhere: a tab page pasted onto a panel is
+ // a page nothing will ever draw a tab for. The clipboard is left alone,
+ // so selecting a tab book and pasting again does what was meant. The
+ // clone goes, because nothing else is holding it.
+ if(!%copy.canBeChildOf(%addSet))
+ {
+ %copy.delete();
+ continue;
+ }
+
+ // Both before the control arrives, so they are part of it arriving rather
+ // than a second edit on top of it - and so that everything the arrival
+ // announces reads the name and the position the control is going to keep.
+ %this.applyNames(%copy);
+ %copy.Position = %this.pastePosition(%copy, %offset);
+
+ // Through the brain, which is where theming on arrival, the undo record,
+ // the selection and the AddControl event that refreshes the Explorer tree
+ // all live. The control palette's click-to-place goes through the same
+ // door for the same reason; the only thing a paste does differently is
+ // place the control itself, which is why that half is not in here.
+ %this.owner.brain.acceptControl(%copy);
+
+ %pasted = (%pasted $= "") ? %copy : (%pasted SPC %copy);
+ }
+
+ %this.owner.undoRecorder.end();
+
+ // Each arrival announced its own control as the selection, so the last one
+ // would otherwise be the only one selected.
+ if(%pasted !$= "")
+ {
+ %this.owner.brain.selectList(%pasted);
+ }
+
+ return %pasted !$= "";
+}
+
+// How far this paste steps away from where the copy was taken.
+//
+// Pasting into the container the control came from puts it one grid step off, so
+// it is not hidden exactly behind the original; pasting somewhere else keeps the
+// position it had. Every further paste into a container steps again, counted per
+// container, so nothing a paste puts down is ever laid exactly on top of
+// something an earlier paste put there.
+function GuiEditorClipboard::nextOffset(%this, %addSet)
+{
+ %grid = %this.owner.brain.getGridSize();
+ if(%grid <= 0)
+ {
+ %grid = 10;
+ }
+
+ for(%i = 0; %i < %this.stepCount; %i++)
+ {
+ if(%this.stepParent[%i] == %addSet)
+ {
+ %this.stepValue[%i]++;
+ return %grid * %this.stepValue[%i];
+ }
+ }
+
+ // First paste into this container. Starting at one step out only makes sense
+ // where the original is; anywhere else the position it had is free.
+ %n = %this.stepCount;
+ %this.stepParent[%n] = %addSet;
+ %this.stepValue[%n] = (%addSet == %this.entrySource[0]) ? 1 : 0;
+ %this.stepCount = %n + 1;
+
+ return %grid * %this.stepValue[%n];
+}
+
+// The position the copy takes in its new parent: the one it had in its old one,
+// plus however far this paste is stepping.
+//
+// Local, and set before the control is added, which is the whole reason paste
+// does its own placing rather than handing a point to onControlDropped. A global
+// position would have to account for the border inset of the container it is
+// landing in - and that inset is not knowable in advance: a control's
+// mRenderInsetLT is written by its parent when the parent renders it
+// (guiControl.cc renderChild), so a control that has never been drawn in this
+// container does not have one yet.
+//
+// A container that places its own children is then free to overrule this, which
+// is right: a chain or a grid decides where its children sit, and a control
+// pasted into one belongs wherever the container puts it.
+function GuiEditorClipboard::pastePosition(%this, %copy, %offset)
+{
+ %at = %copy.getPosition();
+
+ return (getWord(%at, 0) + %offset) SPC (getWord(%at, 1) + %offset);
+}
+
+// Turn the copied names back into real ones, made unique. A control that had no
+// name stays nameless.
+function GuiEditorClipboard::applyNames(%this, %ctrl)
+{
+ %wanted = %ctrl.clipName;
+ if(%wanted !$= "")
+ {
+ %ctrl.setEditFieldValue("name", %this.freeName(%wanted));
+ }
+
+ // Assigning "" is how a dynamic field is removed, so the marker leaves no
+ // trace on the pasted control.
+ %ctrl.clipName = "";
+
+ for(%i = 0; %i < %ctrl.getCount(); %i++)
+ {
+ %this.applyNames(%ctrl.getObject(%i));
+ }
+}
+
+// okButton -> okButton2 -> okButton3. A name that already ends in a number
+// counts on from it rather than growing another digit, so a copy of okButton2 is
+// okButton3 and not okButton22.
+function GuiEditorClipboard::freeName(%this, %wanted)
+{
+ if(!%this.nameTaken(%wanted))
+ {
+ return %wanted;
+ }
+
+ %stem = %wanted;
+ %next = 2;
+
+ %end = strlen(%wanted) - 1;
+ while(%end >= 0 && %this.isDigit(getSubStr(%wanted, %end, 1)))
+ {
+ %end--;
+ }
+
+ // Not a name that is nothing but digits, which has no stem to count from.
+ if(%end >= 0 && %end < (strlen(%wanted) - 1))
+ {
+ %stem = getSubStr(%wanted, 0, %end + 1);
+ %next = getSubStr(%wanted, %end + 1, strlen(%wanted)) + 1;
+ }
+
+ for(%i = %next; %i < %next + 1000; %i++)
+ {
+ %try = %stem @ %i;
+ if(!%this.nameTaken(%try))
+ {
+ return %try;
+ }
+ }
+
+ return "";
+}
+
+function GuiEditorClipboard::isDigit(%this, %char)
+{
+ return strpos("0123456789", %char) != -1;
+}
+
+// Is anything in the document called this?
+//
+// The document is the whole question: Sim cannot answer it, because the editor
+// runs in editor mode, where assignName stashes a control's name instead of
+// registering it (simObject.cc) - so isObject() on the name would say no for
+// every control the user has ever named in here. A control in the trash is not
+// in the document and its name is free, which is what lets a cut and paste keep
+// the name it had.
+function GuiEditorClipboard::nameTaken(%this, %name)
+{
+ if(%name $= "" || !isObject(%this.owner.rootGui))
+ {
+ return false;
+ }
+
+ return %this.nameTakenBelow(%this.owner.rootGui, %name);
+}
+
+function GuiEditorClipboard::nameTakenBelow(%this, %parent, %name)
+{
+ for(%i = 0; %i < %parent.getCount(); %i++)
+ {
+ %child = %parent.getObject(%i);
+
+ // strcmp, not $=: $= is case-insensitive, and two controls whose names
+ // differ only in case are two different names to everything else.
+ if(strcmp(%child.getName(), %name) == 0)
+ {
+ return true;
+ }
+
+ if(%this.nameTakenBelow(%child, %name))
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Duplicating.
+//
+// A copy that never lands in the stash: it goes straight back into the parent
+// the original is in, one grid step off, and whatever is on the clipboard at the
+// time is still on it afterwards. That is the whole reason it is not Ctrl+C
+// followed by Ctrl+V.
+//
+// It is here rather than somewhere of its own because four of the five things it
+// needs are already in this file and are exactly right - the reduction that
+// stops a control being copied twice, the name carrying, the uniquifying, and
+// the counting-on from a trailing number.
+//
+// Two things paste has to do that this does not. There is no canBeChildOf test,
+// because the original is already a legal child of that parent. And there is no
+// per-container step count: a duplicate is always one step from its own
+// original, so there is nothing to remember between calls.
+//-----------------------------------------------------------------------------
+
+function GuiEditorClipboard::duplicate(%this, %selection)
+{
+ if(!isObject(%selection) || %selection.getCount() == 0)
+ {
+ return false;
+ }
+
+ %roots = %this.topLevel(%selection);
+ if(%roots $= "")
+ {
+ return false;
+ }
+
+ %grid = %this.owner.brain.getGridSize();
+ if(%grid <= 0)
+ {
+ %grid = 10;
+ }
+
+ %made = "";
+
+ // However many controls it copies, a duplicate is one thing the user did.
+ // The adds inside record themselves into this transaction, the same way a
+ // paste's do.
+ %this.owner.undoRecorder.begin("Duplicate", "");
+
+ for(%i = 0; %i < getWordCount(%roots); %i++)
+ {
+ %ctrl = getWord(%roots, %i);
+
+ %parent = %ctrl.getParent();
+ if(!isObject(%parent))
+ {
+ continue;
+ }
+
+ %copy = %ctrl.deepClone();
+ if(!isObject(%copy))
+ {
+ continue;
+ }
+
+ // Names first, and both halves, because deepClone leaves them behind on
+ // purpose. The copy is not in the document yet, so it cannot collide with
+ // itself while a free name is being looked for.
+ %this.stampNames(%ctrl, %copy);
+ %this.applyNames(%copy);
+
+ %at = %ctrl.getPosition();
+ %copy.Position = (getWord(%at, 0) + %grid) SPC (getWord(%at, 1) + %grid);
+
+ // addNewControl puts the control in the add set (guiEditCtrl.cc), so the
+ // add set is what decides where a duplicate lands. Setting it also clears
+ // the selection, which is why the copies are selected at the end rather
+ // than as they arrive.
+ %this.owner.brain.setCurrentAddSet(%parent);
+
+ // The same door a paste and a palette click go through: theming on
+ // arrival, the undo record, and the events the Explorer tree and the panes
+ // listen for.
+ %this.owner.brain.acceptControl(%copy);
+
+ %made = (%made $= "") ? %copy : (%made SPC %copy);
+ }
+
+ %this.owner.undoRecorder.end();
+
+ if(%made !$= "")
+ {
+ %this.owner.brain.selectList(%made);
+ }
+
+ return %made !$= "";
+}
+
+//-----------------------------------------------------------------------------
+// State, and what the Edit menu is told about it.
+//-----------------------------------------------------------------------------
+
+function GuiEditorClipboard::isEmpty(%this)
+{
+ for(%i = 0; %i < %this.entryCount; %i++)
+ {
+ if(isObject(%this.entry[%i]))
+ {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+function GuiEditorClipboard::clear(%this)
+{
+ for(%i = 0; %i < %this.entryCount; %i++)
+ {
+ if(isObject(%this.entry[%i]))
+ {
+ %this.entry[%i].delete();
+ }
+ %this.entry[%i] = "";
+ %this.entrySource[%i] = "";
+ }
+
+ %this.entryCount = 0;
+ %this.stepCount = 0;
+ %this.refreshMenu();
+}
+
+function GuiEditorClipboard::refreshMenu(%this)
+{
+ if(isObject(%this.owner) && isObject(%this.owner.menus))
+ {
+ %this.owner.menus.refreshPaste(%this.isEmpty() ? 0 : 1);
+ }
+}
diff --git a/editor/GuiEditor/scripts/GuiEditorColorWindow.cs b/editor/GuiEditor/scripts/GuiEditorColorWindow.cs
deleted file mode 100644
index 264e77a51..000000000
--- a/editor/GuiEditor/scripts/GuiEditorColorWindow.cs
+++ /dev/null
@@ -1,67 +0,0 @@
-
-function GuiEditorColorWindow::onAdd(%this)
-{
- %ext = %this.getExtent();
- %this.grid = new GuiGridCtrl()
- {
- HorizSizing = "width";
- VertSizing = "height";
- Position = "7 10";
- Extent = (%ext.x - 23) SPC (%ext.y - 43);
- CellSizeX = "40";
- CellSizeY = "40";
- CellModeX = "Absolute";
- CellModeY = "Absolute";
- MaxColCount = "4";
- IsExtentDynamic = "1";
- OrderMode = "LRTB";
- };
- ThemeManager.setProfile(%this.grid, "EmptyProfile");
- %this.add(%this.grid);
-
- //%this.addColorCtrl(1, "Pallet");
- //%this.addColorCtrl(6, "BlendColor");
- //%this.addColorCtrl(2, "HorizColor");
- //%this.addColorCtrl(3, "VertColor");
- //%this.addColorCtrl(4, "HorizBrightnessColor");
- //%this.addColorCtrl(5, "VertBrightnessColor");
- //%this.addColorCtrl(7, "HorizAlpha");
- //%this.addColorCtrl(8, "VertAlpha");
- //%this.addColorCtrl(9, "Dropper");
- %this.addColorCtrl(10, "Popup");
- %this.addColorCtrl(11, "Popup");
- %this.addColorCtrl(12, "Popup");
- %this.addColorCtrl(13, "Popup");
-}
-
-function GuiEditorColorWindow::addColorCtrl(%this, %i, %mode)
-{
- if(%mode $= "Popup")
- {
- %this.colorCtrl[%i] = new GuiColorPopupCtrl()
- {
- HorizSizing = "width";
- VertSizing = "height";
- };
- ThemeManager.setProfile(%this.colorCtrl[%i], "colorPickerProfile");
- ThemeManager.setProfile(%this.colorCtrl[%i], "emptyProfile", "backgroundProfile");
- ThemeManager.setProfile(%this.colorCtrl[%i], "colorPopupProfile", "popupProfile");
- ThemeManager.setProfile(%this.colorCtrl[%i], "emptyProfile", "pickerProfile");
- ThemeManager.setProfile(%this.colorCtrl[%i], "colorPickerSelectorProfile", "selectorProfile");
- %this.grid.add(%this.colorCtrl[%i]);
-
- return;
- }
-
- %this.colorCtrl[%i] = new GuiColorPickerCtrl()
- {
- HorizSizing = "width";
- VertSizing = "height";
- DisplayMode = %mode;
- ShowSelector = "1";
- SelectorGap = "4";
- };
- ThemeManager.setProfile(%this.colorCtrl[%i], "colorPickerProfile");
- ThemeManager.setProfile(%this.colorCtrl[%i], "colorPickerSelectorProfile", "selectorProfile");
- %this.grid.add(%this.colorCtrl[%i]);
-}
\ No newline at end of file
diff --git a/editor/GuiEditor/scripts/GuiEditorConfirmSaveDialog.cs b/editor/GuiEditor/scripts/GuiEditorConfirmSaveDialog.cs
new file mode 100644
index 000000000..5f748f44c
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiEditorConfirmSaveDialog.cs
@@ -0,0 +1,110 @@
+
+//-----------------------------------------------------------------------------
+// What stands between a modified Gui and the four commands that would discard
+// it: New, Open, Close Project and Exit.
+//
+// Three answers rather than the Profile Editor's two. Its confirm dialog asks
+// about themes, which are files the user can put back by reverting; this asks
+// about the document they are in the middle of making, and the answer they
+// usually want is "save it, then carry on with what I asked for". Making them
+// Cancel, press Ctrl+S and repeat themselves is not a real third option.
+//
+// It owns none of the decision. GuiEditor holds the command that was
+// interrupted; each button here only says which way to go. See
+// GuiEditor::guardDocument.
+//-----------------------------------------------------------------------------
+
+function GuiEditorConfirmSaveDialog::init(%this, %width, %height)
+{
+ %window = %this.getObject(0);
+ %content = %window.getObject(0);
+
+ %this.feedback = new GuiControl()
+ {
+ HorizSizing = "right";
+ VertSizing = "bottom";
+ Position = "12 12";
+ Extent = (%width - 24) SPC (%height - 86);
+ text = %this.message;
+ textWrap = true;
+ };
+ ThemeManager.setProfile(%this.feedback, "infoProfile");
+ %content.add(%this.feedback);
+
+ // Right to left in the order they escalate: abandon what you asked for,
+ // go through with it, or write the file first.
+ %this.cancelButton = new GuiButtonCtrl()
+ {
+ HorizSizing = "right";
+ VertSizing = "bottom";
+ Position = (%width - 336) SPC (%height - 62);
+ Extent = "100 30";
+ Text = "Cancel";
+ Command = %this.getID() @ ".onCancel();";
+ };
+ ThemeManager.setProfile(%this.cancelButton, "buttonProfile");
+ %content.add(%this.cancelButton);
+
+ %this.discardButton = new GuiButtonCtrl()
+ {
+ HorizSizing = "right";
+ VertSizing = "bottom";
+ Position = (%width - 226) SPC (%height - 62);
+ Extent = "100 30";
+ Text = "Discard";
+ Command = %this.getID() @ ".onDiscard();";
+ };
+ ThemeManager.setProfile(%this.discardButton, "buttonProfile");
+ %content.add(%this.discardButton);
+
+ %this.saveButton = new GuiButtonCtrl()
+ {
+ HorizSizing = "right";
+ VertSizing = "bottom";
+ Position = (%width - 116) SPC (%height - 64);
+ Extent = "100 34";
+ Text = "Save";
+ Command = %this.getID() @ ".onSave();";
+ };
+ ThemeManager.setProfile(%this.saveButton, "primaryButtonProfile");
+ %content.add(%this.saveButton);
+}
+
+function GuiEditorConfirmSaveDialog::onCancel(%this)
+{
+ GuiEditor.dropPendingCommand();
+ %this.closeNow();
+}
+
+function GuiEditorConfirmSaveDialog::onDiscard(%this)
+{
+ %this.closeNow();
+ GuiEditor.runPendingCommand();
+}
+
+// Closed before the save starts, because a Gui that has never been saved sends
+// this straight to the Save As dialog and two of these stacked is one too many.
+//
+// Nothing is resumed here. SaveGui may or may not end in a file being written -
+// Save As has its own Cancel - so the command that was interrupted is left
+// waiting, and only a save that reaches the end of SaveCore releases it.
+function GuiEditorConfirmSaveDialog::onSave(%this)
+{
+ %this.closeNow();
+ GuiEditor.SaveGui();
+}
+
+// The window X, which is the same answer as Cancel: no to the question asked.
+function GuiEditorConfirmSaveDialog::onClose(%this)
+{
+ %this.onCancel();
+}
+
+// Not the shared EditorCore.dialog slot: this dialog can be followed by the Save
+// As dialog within the scheduled delay, and the two would race for it. The same
+// reason GuiProfileEditorConfirmDialog does it this way.
+function GuiEditorConfirmSaveDialog::closeNow(%this)
+{
+ Canvas.popDialog(%this);
+ EditorCore.schedule(100, "deleteDialogObject", %this);
+}
diff --git a/editor/GuiEditor/scripts/GuiEditorControlGroup.cs b/editor/GuiEditor/scripts/GuiEditorControlGroup.cs
new file mode 100644
index 000000000..ee123f508
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiEditorControlGroup.cs
@@ -0,0 +1,152 @@
+//-----------------------------------------------------------------------------
+// One collapsible section of the control palette -- "Basics", "Layout" and so
+// on -- holding a grid of GuiEditorControlTiles.
+//
+// The same shape the Asset Manager uses for its asset dictionaries
+// (AssetLibraryWindow::addDictionary, AssetDictionary.cs): a GuiPanelCtrl whose header
+// is the toggle, with the real content in a grid inside it.
+//
+// The tiles go in that inner grid and never directly on the panel.
+// GuiExpandCtrl::toggleHiddenChildren force-writes mVisible on every DIRECT
+// child of a panel whenever it expands, collapses or resizes -- so a tile
+// parented to the panel would have its visibility rewritten out from under
+// whatever set it. Grandchildren are left alone.
+//
+// Both view modes are this one grid with different cell metrics rather than two
+// layouts. GuiGridCtrl treats CellSizeX as the NARROWEST a column may be, not
+// its width: it fits as many columns as the pane affords and shares the
+// remainder evenly (see GuiEditorInspectorPane::makeCellGrid). So grid mode asks
+// for 100 and gets two or three columns depending on how wide the frame is
+// dragged, and rows mode asks for something wider than the pane, which can only
+// ever be one column.
+//
+// The creator sets Text and owner inline, calls addTile() per entry, then
+// setMode(). There is no onRemove: the tiles were added to the grid and the grid
+// to this panel, so deleting the panel frees the lot.
+//-----------------------------------------------------------------------------
+
+// Width and height are separate numbers because they mean different things: the
+// width is the NARROWEST a column may be and the reflow shares out the rest,
+// while the height is exactly what a tile gets.
+//
+// A grid tile needs 108 of INNER height -- 56 of picture, a 44-pixel band for
+// the name under it and 8 of air between and above. The 8 on top of that is the
+// tile's own border: itemSelectProfile insets 3 pixels a side on the base theme
+// and 4 on Torque Suit, and a cell sized to the interior would leave the fatter
+// of the two clipping the bottom line of every name. The tile centers its
+// picture in whatever room the inset actually leaves, so a theme that spends
+// less than 4 gets slightly more air rather than a layout that drifts.
+$GuiEditorControlGroup::gridCell = 100;
+$GuiEditorControlGroup::gridCellHeight = 116;
+$GuiEditorControlGroup::rowHeight = 40;
+$GuiEditorControlGroup::headerHeight = 24;
+
+function GuiEditorControlGroup::onAdd(%this)
+{
+ %this.tileCount = 0;
+
+ %this.grid = new GuiGridCtrl()
+ {
+ HorizSizing = "width";
+ Position = "0" SPC $GuiEditorControlGroup::headerHeight;
+ Extent = "200 4";
+ // Variable across, absolute down. Variable is what makes CellSizeX a
+ // minimum -- as many columns as fit, remainder shared -- and that is the
+ // whole reflow. Vertically it would instead size each row to its tallest
+ // child, which in row mode means a 116-pixel tile in a 40-pixel row.
+ CellModeX = "variable";
+ CellModeY = "absolute";
+ CellSizeX = $GuiEditorControlGroup::gridCell;
+ CellSizeY = $GuiEditorControlGroup::gridCellHeight;
+ CellSpacingX = 4;
+ CellSpacingY = 4;
+ MaxColCount = 0;
+ MaxRowCount = 0;
+ OrderMode = "lrtb";
+
+ // Without this the grid keeps the height it was built at, and the panel
+ // it lives in measures itself against that -- so a section would collapse
+ // to a sliver or leave a hole under its last row.
+ IsExtentDynamic = true;
+ };
+ ThemeManager.setProfile(%this.grid, "emptyProfile");
+ %this.add(%this.grid);
+}
+
+function GuiEditorControlGroup::addTile(%this, %key)
+{
+ %tile = new GuiButtonCtrl()
+ {
+ class = "GuiEditorControlTile";
+ HorizSizing = "width";
+ VertSizing = "height";
+ Position = "0 0";
+ Extent = $GuiEditorControlGroup::gridCell SPC $GuiEditorControlGroup::gridCellHeight;
+ Text = "";
+ key = %key;
+ owner = %this;
+ };
+ ThemeManager.setProfile(%tile, "itemSelectProfile");
+ %this.grid.add(%tile);
+
+ %this.tile[%this.tileCount] = %tile;
+ %this.tileCount++;
+
+ return %tile;
+}
+
+//-----------------------------------------------------------------------------
+// Modes.
+//-----------------------------------------------------------------------------
+
+function GuiEditorControlGroup::setMode(%this, %mode)
+{
+ %this.mode = %mode;
+ %width = getWord(%this.getExtent(), 0);
+
+ if(%mode $= "rows")
+ {
+ // One column, by asking for a cell wider than there is room for.
+ %this.grid.CellSizeX = %width;
+ %this.grid.CellSizeY = $GuiEditorControlGroup::rowHeight;
+ }
+ else
+ {
+ %this.grid.CellSizeX = $GuiEditorControlGroup::gridCell;
+ %this.grid.CellSizeY = $GuiEditorControlGroup::gridCellHeight;
+ }
+
+ // The grid lays out on resize, so nudge it before the tiles read their own
+ // extents -- otherwise each one measures the cell it had in the other mode.
+ %this.grid.resize(0, $GuiEditorControlGroup::headerHeight, %width, 4);
+
+ for(%i = 0; %i < %this.tileCount; %i++)
+ {
+ %this.tile[%i].setMode(%mode);
+ }
+}
+
+// A GuiPanelCtrl learns its own height only from parentResized -- its
+// constructor defaults to 64x64 whatever Extent it was handed, and a chain
+// positions its children without ever resizing them. Nudging the width by a
+// pixel and back forces exactly one parentResized through the whole subtree and
+// leaves the widths where they started. Same trick as
+// GuiEditorInspectorPane::forceLayout.
+function GuiEditorControlGroup::forceLayout(%this)
+{
+ %w = getWord(%this.getExtent(), 0);
+ %h = getWord(%this.getExtent(), 1);
+ %this.resize(0, 0, %w + 1, %h);
+ %this.resize(0, 0, %w, %h);
+
+ // A panel caches the height it opens to, measured from its children when it
+ // was last opened. Switching modes changes every one of those, so the cache
+ // has to be thrown away -- closing and reopening is what does it. Same move
+ // as AssetDictionary::fixSize, and skipped while collapsed so a section the
+ // user shut does not spring back open.
+ if(%this.getExpanded())
+ {
+ %this.setExpanded(false);
+ %this.setExpanded(true);
+ }
+}
diff --git a/editor/GuiEditor/scripts/GuiEditorControlIcons.cs b/editor/GuiEditor/scripts/GuiEditorControlIcons.cs
new file mode 100644
index 000000000..f0c41c8f5
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiEditorControlIcons.cs
@@ -0,0 +1,246 @@
+//-----------------------------------------------------------------------------
+// The control palette's entries and their icons. GENERATED - do not hand-edit.
+//
+// GuiEditor:controlIcons16, controlIcons64 and controlIcons128 are the same 30
+// drawings at three sizes in the same 8x4 grid, so one index names the same icon
+// in all of them and a tile can change size without changing its Frame. The
+// sizes are source resolution, not tile size: the palette's grid view draws from
+// the 128 sheet, its row view from the 64, and the Explorer tree from the 16.
+//
+// An entry is not always a class. A bare GuiControl is the wrapper, the backdrop,
+// the line of text and the modal scrim, so it holds four entries that share a
+// class and differ by the category they drop pre-stamped with. That is why this
+// is a table and not a list of constants.
+//
+// Frame 0 is the fallback, deliberately. frameFor answers 0 for any key it does
+// not know, and an unset Frame field reads as 0 too, so both failures land on a
+// legible "unknown control" rather than on an arbitrary wrong picture. A control
+// class added to the engine after this file was generated still reaches the
+// palette -- see GuiEditorControlListWindow::populate -- it simply wears the
+// question mark until someone draws it.
+//
+// Frame index is the order the sheet packs them in, which is deliberately NOT
+// the order the palette shows them: display order comes from the group column
+// and the group list below. That way regrouping the palette is a rerun that
+// leaves the sheets untouched, and a reflowed index cannot repoint anything.
+// Nothing should ever write a raw frame number; ask frameFor.
+//-----------------------------------------------------------------------------
+
+function GuiEditorControlIcons::onAdd(%this)
+{
+ // The palette's collapsible sections, in the order they stack.
+ %this.groupList = "Basics" TAB "Layout" TAB "Input & Data" TAB "Advanced";
+
+ // Classes the palette will not offer, whatever the engine registers. The
+ // first group is editor and engine plumbing; the last three are real controls
+ // that nobody places by hand -- GuiDragAndDropCtrl is built at runtime to
+ // carry a drag payload, GuiMenuItemCtrl means nothing outside a menu bar, and
+ // a GuiTabPageCtrl is made by its book, from the + tab the book draws while
+ // the Gui is being authored.
+ %this.refusedNames = "GuiCanvas GuiDragAndDropCtrl GuiGraphCtrl GuiMenuItemCtrl GuiMessageVectorCtrl GuiParticleGraphInspector GuiSceneObjectCtrl GuiTabPageCtrl";
+ %this.refusedPrefixes = "GuiConsole GuiEdit GuiInspector";
+ %count = getWordCount(%this.refusedNames);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %this.refused[getWord(%this.refusedNames, %i)] = true;
+ }
+
+ // key TAB class TAB category TAB group TAB label
+ %table =
+ "unknown" TAB "" TAB "" TAB "" TAB "Unknown" NL
+ "GuiControl:Empty" TAB "GuiControl" TAB "Empty" TAB "Basics" TAB "Empty" NL
+ "GuiControl:Panel" TAB "GuiControl" TAB "Panel" TAB "Basics" TAB "Panel" NL
+ "GuiControl:Label" TAB "GuiControl" TAB "Label" TAB "Basics" TAB "Label" NL
+ "GuiControl:Overlay" TAB "GuiControl" TAB "Overlay" TAB "Advanced" TAB "Overlay" NL
+ "GuiButtonCtrl" TAB "GuiButtonCtrl" TAB "" TAB "Basics" TAB "Button" NL
+ "GuiCheckBoxCtrl" TAB "GuiCheckBoxCtrl" TAB "" TAB "Basics" TAB "Check Box" NL
+ "GuiRadioCtrl" TAB "GuiRadioCtrl" TAB "" TAB "Input & Data" TAB "Radio Button" NL
+ "GuiDropDownCtrl" TAB "GuiDropDownCtrl" TAB "" TAB "Basics" TAB "Drop Down" NL
+ "GuiColorPopupCtrl" TAB "GuiColorPopupCtrl" TAB "" TAB "Advanced" TAB "Color Popup" NL
+ "GuiTextEditCtrl" TAB "GuiTextEditCtrl" TAB "" TAB "Basics" TAB "Text Edit" NL
+ "GuiTextEditSliderCtrl" TAB "GuiTextEditSliderCtrl" TAB "" TAB "Input & Data" TAB "Number Box" NL
+ "GuiSliderCtrl" TAB "GuiSliderCtrl" TAB "" TAB "Input & Data" TAB "Slider" NL
+ "GuiProgressCtrl" TAB "GuiProgressCtrl" TAB "" TAB "Input & Data" TAB "Progress" NL
+ "GuiColorPickerCtrl" TAB "GuiColorPickerCtrl" TAB "" TAB "Advanced" TAB "Color Picker" NL
+ "GuiSpriteCtrl" TAB "GuiSpriteCtrl" TAB "" TAB "Basics" TAB "Sprite" NL
+ "SceneWindow" TAB "SceneWindow" TAB "" TAB "Advanced" TAB "Scene Window" NL
+ "GuiListBoxCtrl" TAB "GuiListBoxCtrl" TAB "" TAB "Input & Data" TAB "List Box" NL
+ "GuiTreeViewCtrl" TAB "GuiTreeViewCtrl" TAB "" TAB "Input & Data" TAB "Tree View" NL
+ "GuiMenuBarCtrl" TAB "GuiMenuBarCtrl" TAB "" TAB "Advanced" TAB "Menu Bar" NL
+ "GuiChainCtrl" TAB "GuiChainCtrl" TAB "" TAB "Layout" TAB "Chain" NL
+ "GuiGridCtrl" TAB "GuiGridCtrl" TAB "" TAB "Layout" TAB "Grid" NL
+ "GuiScrollCtrl" TAB "GuiScrollCtrl" TAB "" TAB "Layout" TAB "Scroll" NL
+ "GuiFrameSetCtrl" TAB "GuiFrameSetCtrl" TAB "" TAB "Advanced" TAB "Frame Set" NL
+ "GuiPanelCtrl" TAB "GuiPanelCtrl" TAB "" TAB "Layout" TAB "Panel" NL
+ "GuiExpandCtrl" TAB "GuiExpandCtrl" TAB "" TAB "Layout" TAB "Expand" NL
+ "GuiTabBookCtrl" TAB "GuiTabBookCtrl" TAB "" TAB "Layout" TAB "Tab Book" NL
+ "GuiTabPageCtrl" TAB "GuiTabPageCtrl" TAB "" TAB "Layout" TAB "Tab Page" NL
+ "GuiWindowCtrl" TAB "GuiWindowCtrl" TAB "" TAB "Layout" TAB "Window" NL
+ "GuiInputCtrl" TAB "GuiInputCtrl" TAB "" TAB "Advanced" TAB "Input";
+
+ %this.keyList = "";
+ %count = getRecordCount(%table);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %rec = getRecord(%table, %i);
+ %key = trim(getField(%rec, 0));
+
+ %this.frame[%key] = %i;
+ %this.ctrlClass[%key] = trim(getField(%rec, 1));
+ %this.category[%key] = trim(getField(%rec, 2));
+ %this.group[%key] = trim(getField(%rec, 3));
+ %this.label[%key] = trim(getField(%rec, 4));
+
+ // The fallback is not something anyone drags, so it stays out of both the
+ // flat list and every group. It is what an unknown key resolves TO.
+ if(%i > 0)
+ {
+ %this.keyList = (%this.keyList $= "") ? %key : (%this.keyList TAB %key);
+
+ // Which CLASSES the table accounts for, as opposed to which keys. The
+ // two differ for exactly one class: GuiControl is covered four times
+ // over, and by no key spelled "GuiControl".
+ %this.covered[%this.ctrlClass[%key]] = true;
+
+ // A refused class keeps its row and everything the row carries -- its
+ // frame, its label, its place in covered[] -- and only loses its
+ // group. The row has to stay: frame IS the row index, so removing one
+ // repoints every icon below it onto the wrong art. And covered[] has
+ // to stay true, or the sweep over the class registry in
+ // GuiEditorControlListWindow::addUndrawnClasses offers the class back
+ // in an "Undrawn" group with a question mark for an icon.
+ //
+ // So the group list is the palette's view, and the key list is the
+ // table's. Only the first one refuses anything.
+ if(!%this.refused[%this.ctrlClass[%key]])
+ {
+ %group = %this.group[%key];
+ %held = %this.groupKeys[%group];
+ %this.groupKeys[%group] = (%held $= "") ? %key : (%held TAB %key);
+ }
+ }
+ }
+}
+
+// Every entry, tab separated, in frame order. The fallback is not among them.
+function GuiEditorControlIcons::keys(%this)
+{
+ return %this.keyList;
+}
+
+// The collapsible sections the palette builds, in the order they stack.
+function GuiEditorControlIcons::groups(%this)
+{
+ return %this.groupList;
+}
+
+// The entries in one section, tab separated. Their order within a section is
+// frame order, which is already grouped by kind.
+function GuiEditorControlIcons::keysInGroup(%this, %group)
+{
+ return %this.groupKeys[%group];
+}
+
+function GuiEditorControlIcons::groupFor(%this, %key)
+{
+ return %this.group[%key];
+}
+
+// Whether the palette can place a class at all. Generated from the same rule the
+// sheet is built with, so the sweep for classes with no icon cannot disagree
+// with the table above about what counts as placeable.
+//
+// GuiEditorControlSpec carries its own copy for its drift guard. That one
+// answers "should this class be in the spec table"; this one answers "should the
+// palette show it" -- same rule today, different questions, and this is the copy
+// generated rather than typed.
+function GuiEditorControlIcons::isPlaceableClass(%this, %name)
+{
+ if(%name $= "" || %this.refused[%name])
+ {
+ return false;
+ }
+
+ // SceneWindow is the one placeable control not named for the Gui hierarchy.
+ if(%name !$= "SceneWindow" && getSubStr(%name, 0, 3) !$= "Gui")
+ {
+ return false;
+ }
+
+ %count = getWordCount(%this.refusedPrefixes);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %prefix = getWord(%this.refusedPrefixes, %i);
+ if(getSubStr(%name, 0, strlen(%prefix)) $= %prefix)
+ {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+// Frame 0 - the question mark - for anything this table has never heard of.
+function GuiEditorControlIcons::frameFor(%this, %key)
+{
+ %frame = %this.frame[%key];
+ return (%frame $= "") ? 0 : %frame;
+}
+
+// The class to instantiate for an entry, and the profile category to stamp it
+// with. An empty category means the class pins its own and there is nothing to
+// choose - see GuiEditorThemeApplier::buildClassTable.
+function GuiEditorControlIcons::classFor(%this, %key)
+{
+ // ctrlClass, not class: "class" is a real SimObject field, and an array named
+ // for it is a collision waiting for whoever adds the next lookup.
+ %name = %this.ctrlClass[%key];
+ return (%name $= "") ? %key : %name;
+}
+
+function GuiEditorControlIcons::categoryFor(%this, %key)
+{
+ return %this.category[%key];
+}
+
+// What the row view writes beside the icon. Falls back to the key, which for
+// everything but the four GuiControl entries is the class name.
+function GuiEditorControlIcons::labelFor(%this, %key)
+{
+ %label = %this.label[%key];
+ return (%label $= "") ? %key : %label;
+}
+
+// True when this key came from the table rather than from the runtime sweep over
+// enumerateConsoleClasses. An unknown entry still works; it just has no icon.
+function GuiEditorControlIcons::isKnown(%this, %key)
+{
+ return %this.frame[%key] !$= "";
+}
+
+// Whether some entry builds this class, which is not the same question as
+// isKnown. A bare GuiControl has four entries and none of them is keyed
+// "GuiControl", so asking isKnown about the class name says no and the sweep for
+// undrawn classes would offer a fifth, iconless copy of a control the palette
+// already shows four ways.
+function GuiEditorControlIcons::coversClass(%this, %name)
+{
+ return %this.covered[%name] !$= "";
+}
+
+// The sheet to draw from at a given tile size. Each threshold is that sheet's
+// own resolution, not a midpoint: past it the sheet would have to be enlarged,
+// and enlarging is what looks soft. Taking the next one up and shrinking it
+// costs nothing and stays sharp.
+function GuiEditorControlIcons::sheetFor(%this, %tileSize)
+{
+ if(%tileSize > 64)
+ {
+ return "GuiEditor:controlIcons128";
+ }
+ if(%tileSize > 16)
+ {
+ return "GuiEditor:controlIcons64";
+ }
+ return "GuiEditor:controlIcons16";
+}
diff --git a/editor/GuiEditor/scripts/GuiEditorControlListBox.cs b/editor/GuiEditor/scripts/GuiEditorControlListBox.cs
deleted file mode 100644
index a056b9d4d..000000000
--- a/editor/GuiEditor/scripts/GuiEditorControlListBox.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-
-function GuiEditorControlListBox::onTouchDragged(%this, %index, %text)
-{
- %position = GuiEditor.brain.getGlobalPosition();
- %cursorpos = Canvas.getCursorPos();
-
- %class = %this.getItemText(%this.getSelectedItem());
- %payload = eval("return new " @ %class @ "();");
- if(!isObject(%payload))
- return;
-
- %xOffset = (getWord(%payload.extent, 0) / 2) + getWord(%position, 0);
- %yOffset = (getWord(%payload.extent, 1) / 2) + getWord(%position, 1);
-
- // position where the drag will start, to prevent visible jumping.
- %xPos = getWord(%cursorpos, 0) - %xOffset;
- %yPos = getWord(%cursorpos, 1) - %yOffset;
-
- %dragCtrl = new GuiDragAndDropCtrl() {
- canSaveDynamicFields = "0";
- Profile = "GuiDragAndDropProfile";
- HorizSizing = "right";
- VertSizing = "bottom";
- Position = %xPos SPC %yPos;
- extent = %payload.extent;
- MinExtent = "32 32";
- canSave = "1";
- Visible = "1";
- hovertime = "1000";
- Text = %text;
- deleteOnMouseUp = true;
- };
-
- %dragCtrl.add(%payload);
- GuiEditor.brain.add(%dragCtrl);
-
- %dragCtrl.startDragging(%xOffset, %yOffset);
-}
\ No newline at end of file
diff --git a/editor/GuiEditor/scripts/GuiEditorControlListWindow.cs b/editor/GuiEditor/scripts/GuiEditorControlListWindow.cs
index 321cb1d6b..f5fe22c32 100644
--- a/editor/GuiEditor/scripts/GuiEditorControlListWindow.cs
+++ b/editor/GuiEditor/scripts/GuiEditorControlListWindow.cs
@@ -1,62 +1,282 @@
//GuiEditorControlListWindow.cs
+//
+// The control palette. Two view buttons at the top, then the placeable controls
+// as collapsible groups of icon tiles.
+//
+// What this replaced: a GuiListBoxCtrl of raw class names, which told a person
+// nothing about what any of them looked like. The class names now live in the
+// tooltips and the pictures do the work.
+//
+// controls GuiEditorControlIcons, generated alongside the icon sheets
+// groups one GuiEditorControlGroup per section, stacked in a chain
+// tiles one GuiEditorControlTile per entry, inside each group's grid
+
+$GuiEditorControlListWindow::modeBarHeight = 28;
function GuiEditorControlListWindow::onAdd(%this)
{
- %this.scroller = new GuiScrollCtrl()
- {
- HorizSizing="width";
- VertSizing="height";
- Position="0 0";
- Extent="242 355";
- hScrollBar="alwaysOff";
- vScrollBar="alwaysOn";
- constantThumbHeight="0";
- showArrowButtons="1";
- scrollBarThickness="14";
+ %this.mode = "grid";
+ %this.groupCount = 0;
+
+ %this.buildModeRow();
+
+ // Built filling the whole content rect, then moved down under the mode bar by
+ // fitScroller. Fill is the only way to find out how big that rect is: it is
+ // the window's extent less the title bar and whatever borders the profile
+ // asks for, and script cannot ask for any of those numbers.
+ %this.scroller = new GuiScrollCtrl()
+ {
+ HorizSizing = "fill";
+ VertSizing = "fill";
+ Position = "0 0";
+ Extent = "242 327";
+ hScrollBar = "alwaysOff";
+ vScrollBar = "dynamic";
+ constantThumbHeight = "0";
+ showArrowButtons = "1";
+ scrollBarThickness = "14";
};
- ThemeManager.setProfile(%this.scroller, "emptyProfile");
- ThemeManager.setProfile(%this.scroller, "thumbProfile", "ThumbProfile");
- ThemeManager.setProfile(%this.scroller, "trackProfile", "TrackProfile");
- ThemeManager.setProfile(%this.scroller, "scrollArrowProfile", "ArrowProfile");
+ ThemeManager.setProfile(%this.scroller, "scrollingPanelProfile");
+ ThemeManager.setProfile(%this.scroller, "scrollingPanelThumbProfile", "ThumbProfile");
+ ThemeManager.setProfile(%this.scroller, "scrollingPanelTrackProfile", "TrackProfile");
+ ThemeManager.setProfile(%this.scroller, "scrollingPanelArrowProfile", "ArrowProfile");
%this.add(%this.scroller);
+ %this.fitScroller();
+
+ // Fill across, and nothing about widths anywhere below.
+ //
+ // The scroller's horizontal bar is alwaysOff, so across is an axis that does
+ // not scroll: the room is bounded and the engine knows exactly what it is --
+ // the inner rect, less the vertical bar when that is showing. Fill asks for
+ // precisely that, and keeps asking as the frame is dragged and as the bar
+ // comes and goes. Down is left alone; that axis scrolls, so the chain is as
+ // tall as its groups and GuiScrollCtrl refuses fill there.
+ %this.groupChain = new GuiChainCtrl()
+ {
+ HorizSizing = "fill";
+ Position = "0 0";
+ Extent = "228 4";
+ IsVertical = true;
+ ChildSpacing = 2;
+ };
+ ThemeManager.setProfile(%this.groupChain, "emptyProfile");
+ %this.scroller.add(%this.groupChain);
+
+ %this.populate();
+}
+
+// Put the scroller under the mode bar without ever naming a border thickness.
+//
+// A fixed bar above a stretching pane has no sizing flag of its own: "fill"
+// takes the whole inner rect and throws the position away, so the bar ends up
+// underneath it, and "height" keeps the gaps the authored extent happened to
+// start with -- which means guessing the title height and the border sizes, the
+// exact guess that left every one of these windows clipping its last eight
+// pixels when the title bar grew from 20 to 28.
+//
+// So let the engine measure instead: the scroller is built filling, one resize
+// pass makes that real, and what it reports back IS the content rect. Take the
+// bar off the top of it and switch to "height", which from then on holds the top
+// edge where it was put and lets the bottom follow the window.
+function GuiEditorControlListWindow::fitScroller(%this)
+{
+ %w = getWord(%this.getExtent(), 0);
+ %h = getWord(%this.getExtent(), 1);
+
+ // Nudge the width by a pixel and back: one parentResized through every child,
+ // widths unchanged. Same move as GuiEditorInspectorPane::forceLayout.
+ %this.resize(0, 0, %w + 1, %h);
+ %this.resize(0, 0, %w, %h);
- %this.listBox = new GuiListBoxCtrl()
- {
- class = "GuiEditorControlListBox";
- HorizSizing="width";
- VertSizing="height";
- Position="0 0";
- AllowMultipleSelections = "0";
- fitParentWidth = "1";
- };
- ThemeManager.setProfile(%this.listBox, "listBoxProfile");
- %this.scroller.add(%this.listBox);
+ %inner = %this.scroller.getExtent();
+ %bar = $GuiEditorControlListWindow::modeBarHeight;
- %this.populate();
+ %this.scroller.HorizSizing = "width";
+ %this.scroller.VertSizing = "height";
+ %this.scroller.resize(0, %bar, getWord(%inner, 0), getWord(%inner, 1) - %bar);
}
function GuiEditorControlListWindow::onRemove(%this)
{
- if(isObject(%this.scroller))
- {
- %this.scroller.delete();
- }
+ // The mode row and the scroller are this window's two children; the groups
+ // and their tiles hang off the scroller and go with it.
+ if(isObject(%this.modeRow))
+ {
+ %this.modeRow.delete();
+ }
+ if(isObject(%this.scroller))
+ {
+ %this.scroller.delete();
+ }
}
+//-----------------------------------------------------------------------------
+// The view switch. EditorChoiceRow is already "a radio group that looks like
+// a segmented control" -- a row of toggle buttons of which exactly one is down,
+// which is exactly this. It carries a caption by default; two icons say enough
+// on their own, so the label width goes to nothing.
+//-----------------------------------------------------------------------------
+
+function GuiEditorControlListWindow::buildModeRow(%this)
+{
+ %this.modeRow = new GuiControl()
+ {
+ class = "EditorChoiceRow";
+ HorizSizing = "width";
+ Position = "4 2";
+ labelText = "";
+
+ // 4, not 0: build() sizes its caption at labelWidth - 4, so a zero here
+ // asks for a control four pixels wide in the wrong direction.
+ labelWidth = 4;
+ owner = %this;
+ fieldName = "mode";
+ };
+ %this.add(%this.modeRow);
+
+ %this.modeRow.addChoice("grid", $EditorIcon::grid_2x2, "Show controls as a grid of large icons");
+ %this.modeRow.addChoice("rows", $EditorIcon::list_bullets, "Show controls as a compact list");
+ %this.modeRow.build();
+ %this.modeRow.setValue(%this.mode);
+}
+
+function GuiEditorControlListWindow::onChoiceRowChanged(%this, %row)
+{
+ %this.setMode(%row.getValue());
+}
+
+function GuiEditorControlListWindow::setMode(%this, %mode)
+{
+ %this.mode = %mode;
+
+ for(%i = 0; %i < %this.groupCount; %i++)
+ {
+ %this.group[%i].setMode(%mode);
+ }
+
+ %this.relayout();
+}
+
+// Each group has to be told to remeasure, and then the chain has to be told its
+// children changed height. Neither happens on its own: a chain positions its
+// children without resizing them, and a panel only learns its height from a
+// parentResized it would otherwise never receive.
+//
+// No widths here. The chain fills the scroller, and the groups follow the chain,
+// so how much room the vertical bar leaves is the engine's answer to give -- see
+// GuiScrollCtrl::getInnerRect and preventUnsizedModes. This used to measure that
+// room in script and hand it out, which held only until the frame was next
+// dragged: "width" sizing adds the parent's CHANGE to a child's own width, so an
+// authored number is offset forever and never replaced. tests/smoke/palette.cs
+// sweeps the window across 31 widths rather than checking one, because a single
+// static check is exactly what all three script attempts passed.
+function GuiEditorControlListWindow::relayout(%this)
+{
+ for(%i = 0; %i < %this.groupCount; %i++)
+ {
+ %this.group[%i].forceLayout();
+ }
+
+ %w = getWord(%this.groupChain.getExtent(), 0);
+ %h = getWord(%this.groupChain.getExtent(), 1);
+ %this.groupChain.resize(0, 0, %w, %h);
+}
+
+//-----------------------------------------------------------------------------
+// Filling it.
+//
+// The generated table decides the order and the grouping. Then anything the
+// engine registers that the table has never heard of is swept into a group of
+// its own wearing the question-mark icon -- so a control class added after the
+// icons were generated still appears and can still be placed. It just has no
+// picture yet.
+//-----------------------------------------------------------------------------
+
function GuiEditorControlListWindow::populate(%this)
{
- %controls = enumerateConsoleClasses("GuiControl");
- %this.listBox.clearItems();
- for(%i = 0; %i < getFieldCount(%controls); %i++)
+ %icons = GuiEditor.controlIcons;
+ %groups = %icons.groups();
+
+ for(%g = 0; %g < getFieldCount(%groups); %g++)
+ {
+ %name = getField(%groups, %g);
+ %group = %this.addGroup(%name);
+
+ %keys = %icons.keysInGroup(%name);
+ for(%i = 0; %i < getFieldCount(%keys); %i++)
+ {
+ %group.addTile(getField(%keys, %i));
+ }
+
+ // A GuiExpandCtrl starts collapsed (mExpanded is false in the
+ // constructor), and it measures its open height from the children it has
+ // at the moment it opens -- so this has to come after the tiles, not with
+ // the panel.
+ %group.setExpanded(true);
+ }
+
+ %this.addUndrawnClasses();
+ %this.setMode(%this.mode);
+}
+
+function GuiEditorControlListWindow::addGroup(%this, %title)
+{
+ %group = new GuiPanelCtrl()
{
- %field = getField(%controls, %i);
+ class = "GuiEditorControlGroup";
- if(%field !$= "GuiCanvas" && (%field $= "SceneWindow" || getSubStr(%field, 0, 3) $= "Gui") &&
- getSubStr(%field, 0, 10) !$= "GuiConsole" && getSubStr(%field, 0, 7) !$= "GuiEdit" &&
- getSubStr(%field, 0, 12) !$= "GuiInspector" && %field !$= "GuiMessageVectorCtrl" &&
- %field !$= "GuiParticleGraphInspector" && %field !$= "GuiGraphCtrl" && %field !$= "GuiSceneObjectCtrl")
- {
- %this.listBox.addItem(%field);
- }
+ // Width, NOT fill, however much a group is meant to be exactly as wide as
+ // the chain. A GuiPanelCtrl sizes itself to its children --
+ // GuiExpandCtrl::parentResized ends by writing mExpandedExtent straight
+ // into mBounds.extent -- and a direct write like that goes around resize,
+ // which is the only thing that honours fill. Asked to fill, a panel is
+ // clamped and then immediately overwrites the clamp with its own answer.
+ HorizSizing = "width";
+ Position = "0 0";
+ Extent = "242" SPC $GuiEditorControlGroup::headerHeight;
+ MinExtent = "80" SPC $GuiEditorControlGroup::headerHeight;
+ Text = %title;
+ command = "";
+ owner = %this;
+ };
+ ThemeManager.setProfile(%group, "panelProfile");
+ %this.groupChain.add(%group);
+
+ %this.group[%this.groupCount] = %group;
+ %this.groupCount++;
+
+ return %group;
+}
+
+// The runtime sweep, kept as a tail rather than as the whole list. Both the
+// filter and the table come from GuiEditorControlIcons, which is generated from
+// the same rule the sheets are built with -- so the sweep cannot disagree with
+// the table about what counts as placeable, and this does not have to reach
+// across into another window to ask.
+function GuiEditorControlListWindow::addUndrawnClasses(%this)
+{
+ %icons = GuiEditor.controlIcons;
+ %classes = enumerateConsoleClasses("GuiControl");
+ %group = 0;
+
+ for(%i = 0; %i < getFieldCount(%classes); %i++)
+ {
+ %name = trim(getField(%classes, %i));
+
+ // coversClass, not isKnown: the four GuiControl entries are keyed
+ // "GuiControl:Empty" and so on, so asking whether "GuiControl" is a known
+ // KEY says no and would add a fifth, iconless copy of it.
+ if(!%icons.isPlaceableClass(%name) || %icons.coversClass(%name))
+ {
+ continue;
+ }
+
+ // Built only if something turns up, so the ordinary case shows four
+ // groups rather than five with an empty one.
+ if(!isObject(%group))
+ {
+ %group = %this.addGroup("Undrawn");
+ }
+ %group.addTile(%name);
}
-}
\ No newline at end of file
+}
diff --git a/editor/GuiEditor/scripts/GuiEditorControlSpec.cs b/editor/GuiEditor/scripts/GuiEditorControlSpec.cs
new file mode 100644
index 000000000..04af549a6
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiEditorControlSpec.cs
@@ -0,0 +1,1056 @@
+
+//-----------------------------------------------------------------------------
+// The context model behind the Gui Editor's properties pane: which of a
+// control's fields actually matter for the kind of control it is.
+//
+// The sibling of GuiProfileEditorFieldSpec, and the same idea. A GuiControl
+// registers a field for everything any control might want, but a given class
+// reads a fraction of it: a GuiChainCtrl never draws its own text, so the nine
+// text fields it inherits do nothing, and a GuiTabPageCtrl's position and
+// extent are overwritten by its book on every layout pass. Editing either looks
+// like it works and silently reverts.
+//
+// Unlike the profile spec there is no Show All. A profile field that a category
+// never reads could still be read by some other category, so hiding it is a
+// filter; here a hidden field is one the engine provably never looks at for
+// this class, so there is nothing to reveal.
+//
+// Every entry below is derived from the class's render and layout paths, not
+// from what the field name suggests. The surprising ones carry the source line
+// that settles them.
+//
+// Four traits per class:
+//
+// textRole What the control's own "text" field does.
+// render drawn through GuiControl::renderText -- every
+// text field applies.
+// caption drawn on the control's behalf by something else
+// (a panel's header button, a tab book drawing its
+// page's text with mTabProfile). The string matters;
+// the control's own layout fields do not.
+// placeholder drawn only while nothing is selected
+// (guiDropDownCtrl.cc, getSelectedItem() == -1).
+// Same fields as render, different label.
+// proxy renderText runs, but with someone else's string
+// (list and tree item text). Layout fields live,
+// text and textID dead.
+// inherited no onRender override, so GuiControl::onRender
+// draws mText. Live, but nobody wants text on a
+// grid -- it goes in a collapsed section rather
+// than the header.
+// none never drawn.
+// flags fontAdjust reads mFontSizeAdjust outside renderText, so the
+// field is live even where the text block is not.
+// easing calls GuiEasingSupport::getFillColor, which is what
+// makes the four ease* fields do anything.
+// command fires Command on interaction rather than only
+// through an accelerator, so it earns its own section.
+// bare registers no GuiControl fields at all.
+// hides Fields this class must drop from the shared rows despite its
+// textRole -- the per-class exceptions.
+// sections The class's own collapsible sections, declared below.
+//
+// A fifth trait, geometryMode, is a property of the control's PARENT rather
+// than its class, so it is computed per control instead of tabled.
+//
+// Fields absent from everything here are never shown: canSave (its write
+// function is defaultProtectedNotWriteFn, so it never even serializes),
+// canSaveDynamicFields, and parentGroup.
+//
+// TypeGuiCursor fields were once in that list too, because GuiCursor had no
+// category and GuiProfileTheme had no cursor table. Both now exist, so a cursor
+// slot joins the Variants scheme on the same terms as a profile slot: the theme
+// fills it silently, and a row appears only where the theme holds more than one
+// cursor for that job. They stay "hidden" to the generic field walk regardless,
+// so an unremarkable window shows no cursor rows at all.
+//
+// The spec is pure data with no UI; the pane owns one and asks it what to show.
+//-----------------------------------------------------------------------------
+
+function GuiEditorControlSpec::onAdd(%this)
+{
+ // class TAB textRole TAB flags TAB per-class hides
+ %table =
+ "GuiControl" TAB "render" TAB "" TAB "" NL
+
+ // Buttons. Easing is narrower than the class tree suggests: only
+ // GuiButtonCtrl and GuiDropDownCtrl actually call getFillColor.
+ // GuiCheckBoxCtrl::onRender draws its box through renderInnerControl and
+ // never renders a universal rect for itself, so Radio inherits a dead
+ // set too.
+ "GuiButtonCtrl" TAB "render" TAB "command easing" TAB "" NL
+ "GuiCheckBoxCtrl" TAB "render" TAB "command" TAB "" NL
+ "GuiRadioCtrl" TAB "render" TAB "command" TAB "" NL
+ "GuiDropDownCtrl" TAB "placeholder" TAB "command easing" TAB "" NL
+ "GuiColorPopupCtrl" TAB "none" TAB "command" TAB "" NL
+
+ // A text edit reads mTextWrap (it is the multi-line control) and
+ // getAlignmentType, but never mVAlignment or mTextExtend.
+ "GuiTextEditCtrl" TAB "render" TAB "command" TAB "vAlign textExtend" NL
+ "GuiTextEditSliderCtrl" TAB "render" TAB "command" TAB "vAlign textExtend" NL
+
+ // The slider draws its value with dglDrawText, not renderText, so the
+ // text block is dead -- but guiSliderCtrl.cc still sizes that draw with
+ // getFont(mFontSizeAdjust).
+ "GuiSliderCtrl" TAB "none" TAB "command fontAdjust" TAB "" NL
+ "GuiColorPickerCtrl" TAB "render" TAB "command" TAB "" NL
+ "GuiProgressCtrl" TAB "render" TAB "" TAB "" NL
+ "GuiSpriteCtrl" TAB "none" TAB "" TAB "" NL
+
+ // Lists render item text through renderText with their own profile, so
+ // the layout fields are live even though "text" is never drawn. Not
+ // textExtend: it would resize the whole list to fit one item.
+ "GuiListBoxCtrl" TAB "proxy" TAB "" TAB "" NL
+ "GuiTreeViewCtrl" TAB "proxy" TAB "" TAB "BindToGuiEditor" NL
+
+ "GuiMenuBarCtrl" TAB "none" TAB "" TAB "" NL
+ "GuiMenuItemCtrl" TAB "caption" TAB "command bare" TAB "" NL
+
+ // Layout containers. The chain draws only a "+" in edit mode.
+ "GuiChainCtrl" TAB "none" TAB "" TAB "" NL
+ "GuiGridCtrl" TAB "inherited" TAB "" TAB "" NL
+ "GuiScrollCtrl" TAB "none" TAB "" TAB "" NL
+ "GuiFrameSetCtrl" TAB "none" TAB "easing" TAB "" NL
+
+ // A panel hands its text to the header button it owns; the button
+ // renders it, so the panel's own layout fields never run.
+ "GuiPanelCtrl" TAB "caption" TAB "" TAB "" NL
+ "GuiExpandCtrl" TAB "inherited" TAB "" TAB "" NL
+
+ // The book draws each page's text as the tab label, using mTabProfile
+ // and the BOOK's mFontSizeAdjust -- so the layout fields belong to the
+ // book and the string belongs to the page.
+ "GuiTabBookCtrl" TAB "proxy" TAB "" TAB "textWrap" NL
+ "GuiTabPageCtrl" TAB "caption" TAB "" TAB "" NL
+
+ "GuiWindowCtrl" TAB "render" TAB "" TAB "" NL
+ "GuiDragAndDropCtrl" TAB "inherited" TAB "" TAB "" NL
+ "GuiInputCtrl" TAB "inherited" TAB "command" TAB "" NL
+ "SceneWindow" TAB "none" TAB "" TAB "";
+
+ %this.classNames = "";
+ %count = getRecordCount(%table);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %rec = getRecord(%table, %i);
+ %name = trim(getField(%rec, 0));
+
+ // "class" is a real SimObject field, so the table uses textRole.
+ %this.textRole[%name] = trim(getField(%rec, 1));
+ %this.flags[%name] = trim(getField(%rec, 2));
+ %this.hides[%name] = trim(getField(%rec, 3));
+ %this.sectionKeyList[%name] = "";
+
+ %this.classNames = (%this.classNames $= "") ? %name : (%this.classNames SPC %name);
+ }
+
+ %this.buildSections();
+ %this.buildHeaderValues();
+ %this.buildLabels();
+}
+
+//-----------------------------------------------------------------------------
+// The class-specific sections. Declared in full per class rather than
+// accumulated up an inheritance chain: a section list reads as documentation of
+// what the control is, and the handful of repeated lines cost less than a
+// chain-walking rule that every future exception would have to fight.
+//-----------------------------------------------------------------------------
+
+function GuiEditorControlSpec::buildSections(%this)
+{
+ %this.addSection("GuiCheckBoxCtrl", "Box", "Check Box",
+ "boxOffset boxExtent textOffset textExtent");
+ %this.addSection("GuiRadioCtrl", "Box", "Radio Button",
+ "boxOffset boxExtent textOffset textExtent");
+
+ %this.addSection("GuiDropDownCtrl", "DropDown", "Drop Down", "maxHeight");
+ %this.addSection("GuiDropDownCtrl", "Scrollbar", "Scroll Bar",
+ "constantThumbHeight showArrowButtons scrollBarThickness");
+
+ %this.addSection("GuiColorPopupCtrl", "Popup", "Popup",
+ "popupSize barHeight showAlphaBar swatchColumns showColorValues valueMode valueBoxHeight");
+
+ %this.addSection("GuiTextEditCtrl", "Input", "Input",
+ "maxLength password returnCausesTab sinkAllKeyEvents");
+ %this.addSection("GuiTextEditCtrl", "Commands", "Commands",
+ "returnCommand escapeCommand");
+ %this.addSection("GuiTextEditSliderCtrl", "Input", "Input",
+ "maxLength password returnCausesTab sinkAllKeyEvents");
+ %this.addSection("GuiTextEditSliderCtrl", "Commands", "Commands",
+ "returnCommand escapeCommand");
+ %this.addSection("GuiTextEditSliderCtrl", "Number", "Number", "format");
+
+ %this.addSection("GuiSliderCtrl", "Slider", "Slider", "ticks");
+ %this.addSection("GuiColorPickerCtrl", "Picker", "Picker",
+ "PickColor ShowSelector ActionOnMove");
+ %this.addSection("GuiProgressCtrl", "Progress", "Progress", "animationTime");
+
+ // Image, Animation and Bitmap are three ways to say the same thing, so the
+ // pane shows one of the three at a time; sourceMode below picks which.
+ %this.addSection("GuiSpriteCtrl", "Fit", "Fit",
+ "fullSize imageSize constrainProportions clampImage tileImage positionOffset");
+ %this.addSection("GuiSpriteCtrl", "Color", "Color", "imageColor");
+
+ %this.addSection("GuiListBoxCtrl", "List", "List",
+ "AllowMultipleSelections FitParentWidth");
+ // IndentSize reads 0 for "one row height", which is the step the tree has
+ // always used; IconImage empty means a row draws no picture and spends no
+ // width on one. Claimed here rather than left to the Other section so the
+ // three arrive together, under the heading that explains them.
+ %this.addSection("GuiTreeViewCtrl", "Tree", "Tree",
+ "AllowReorder IndentSize IconImage IconSize");
+ %this.addSection("GuiTreeViewCtrl", "List", "List",
+ "AllowMultipleSelections FitParentWidth");
+
+ %this.addSection("GuiMenuBarCtrl", "Scrollbar", "Scroll Bar",
+ "constantThumbHeight showArrowButtons scrollBarThickness");
+ // No section for GuiMenuItemCtrl. Everything it has is in the header, in
+ // GuiEditorMenuItemBlock: a menu item has no profile, no geometry and no
+ // tooltip, so a header with a caption in it and three empty sections below
+ // was most of what the pane showed. Toggle, Radio and IsOn went with it -
+ // they are one decision, and the block draws them as one.
+
+ %this.addSection("GuiGridCtrl", "Grid", "Grid",
+ "CellSpacingX CellSpacingY MaxColCount MaxRowCount OrderMode IsExtentDynamic");
+ %this.addSection("GuiScrollCtrl", "Scrollbar", "Scroll Bar",
+ "constantThumbHeight showArrowButtons scrollBarThickness");
+
+ %this.addSection("GuiPanelCtrl", "Expand", "Expand", "easeExpand easeTimeExpand");
+ %this.addSection("GuiExpandCtrl", "Expand", "Expand", "easeExpand easeTimeExpand");
+
+ %this.addSection("GuiTabBookCtrl", "Tabs", "Tabs", "MinTabWidth");
+
+ // The six window toggles have no section: they are an icon row beside Title
+ // Height in the header, where six switches take one line instead of six.
+ // See GuiEditorControlSpec::windowToggles.
+ %this.addSection("GuiWindowCtrl", "Grips", "Resize Grips",
+ "resizeRightWidth resizeBottomHeight");
+
+ %this.addSection("GuiDragAndDropCtrl", "Drag", "Drag", "deleteOnMouseUp");
+
+ %this.addSection("SceneWindow", "SceneInput", "Scene Input",
+ "lockMouse UseWindowInputEvents UseObjectInputEvents");
+ %this.addSection("SceneWindow", "Background", "Background",
+ "UseBackgroundColor BackgroundColor");
+ %this.addSection("SceneWindow", "Scrollbar", "Scroll Bar",
+ "constantThumbHeight showArrowButtons scrollBarThickness");
+}
+
+function GuiEditorControlSpec::addSection(%this, %class, %key, %title, %fields)
+{
+ %this.sectionTitleFor[%class, %key] = %title;
+ %this.sectionFieldsFor[%class, %key] = %fields;
+
+ %list = %this.sectionKeyList[%class];
+ %this.sectionKeyList[%class] = (%list $= "") ? %key : (%list SPC %key);
+}
+
+function GuiEditorControlSpec::sectionKeys(%this, %class)
+{
+ return %this.sectionKeyList[%class];
+}
+
+function GuiEditorControlSpec::sectionTitle(%this, %class, %key)
+{
+ return %this.sectionTitleFor[%class, %key];
+}
+
+function GuiEditorControlSpec::sectionFields(%this, %class, %key)
+{
+ return %this.sectionFieldsFor[%class, %key];
+}
+
+// Whether the class gets the Items section: the static rows a list is authored
+// with, saved with the Gui as TAML custom nodes.
+//
+// A function rather than a fifth column in the table above, because two classes
+// out of thirty do not earn one; and a list of two rather than an ancestry test,
+// because GuiTreeViewCtrl derives from GuiListBoxCtrl and must NOT have it. A
+// tree's rows are generated from a root object, so anything typed into them
+// would be gone the moment the tree next built itself - which is why
+// GuiTreeViewCtrl::writesItems returns false on the engine side too.
+function GuiEditorControlSpec::hasItemList(%this, %class)
+{
+ return %class $= "GuiListBoxCtrl" || %class $= "GuiDropDownCtrl";
+}
+
+//-----------------------------------------------------------------------------
+// The principal value: the one or two fields that are the whole point of the
+// control, promoted out of their section and into the always-visible header.
+//-----------------------------------------------------------------------------
+
+function GuiEditorControlSpec::buildHeaderValues(%this)
+{
+ %this.headerValues["GuiCheckBoxCtrl"] = "stateOn";
+ %this.headerValues["GuiRadioCtrl"] = "stateOn groupNum";
+ %this.headerValues["GuiColorPopupCtrl"] = "baseColor";
+ %this.headerValues["GuiTextEditCtrl"] = "inputMode";
+ %this.headerValues["GuiTextEditSliderCtrl"] = "inputMode range increment";
+ %this.headerValues["GuiSliderCtrl"] = "range value";
+ %this.headerValues["GuiColorPickerCtrl"] = "BaseColor DisplayMode";
+ %this.headerValues["GuiProgressCtrl"] = "Variable";
+ %this.headerValues["GuiChainCtrl"] = "IsVertical ChildSpacing";
+ %this.headerValues["GuiGridCtrl"] = "CellModeX CellModeY CellSizeX CellSizeY";
+ %this.headerValues["GuiScrollCtrl"] = "hScrollBar vScrollBar";
+ %this.headerValues["GuiFrameSetCtrl"] = "DividerThickness";
+ %this.headerValues["GuiTabBookCtrl"] = "TabPosition";
+ %this.headerValues["GuiWindowCtrl"] = "titleHeight";
+
+ // GuiMenuItemCtrl is deliberately absent too: IsOn is one of the three fields
+ // GuiEditorMenuItemBlock draws as a single choice, so it is not a value the
+ // generic header block has anything to say about.
+
+ // GuiSpriteCtrl is deliberately absent: its principal value is three
+ // mutually exclusive fields rather than a fixed list, so the pane asks
+ // spriteSourceFields below instead of reading a header value here.
+}
+
+function GuiEditorControlSpec::headerValueFields(%this, %class)
+{
+ return %this.headerValues[%class];
+}
+
+// The three ways a GuiSpriteCtrl can name its picture. Only one is ever in
+// effect, so offering all three at once invites setting two and wondering which
+// won. The pane shows the fields of the mode the control is currently in.
+function GuiEditorControlSpec::spriteSourceFields(%this, %mode)
+{
+ switch$(%mode)
+ {
+ case "Animation": return "Animation";
+ case "Bitmap": return "Bitmap singleFrameBitmap";
+ }
+ return "Image Frame NamedFrame";
+}
+
+function GuiEditorControlSpec::spriteSourceModes(%this)
+{
+ return "Image" TAB "Animation" TAB "Bitmap";
+}
+
+// Which of the three a control is actually using, judged by what it holds.
+function GuiEditorControlSpec::spriteSourceModeOf(%this, %ctrl)
+{
+ if(%ctrl.Animation !$= "")
+ {
+ return "Animation";
+ }
+ if(%ctrl.Image $= "" && %ctrl.Bitmap !$= "")
+ {
+ return "Bitmap";
+ }
+ return "Image";
+}
+
+//-----------------------------------------------------------------------------
+// The shared field groups. These rows are built once and filtered with
+// setVisible, because every class has them -- only whether they mean anything
+// changes.
+//-----------------------------------------------------------------------------
+
+// Drawn by GuiControl::renderText, so they live or die with textRole.
+function GuiEditorControlSpec::textFields(%this)
+{
+ return "text textID textWrap textExtend align vAlign fontSizeAdjust overrideFontColor fontColor";
+}
+
+// The subset a proxy role keeps: renderText still runs, just with a string the
+// control did not get from its text field.
+function GuiEditorControlSpec::textLayoutFields(%this)
+{
+ return "textWrap textExtend align vAlign fontSizeAdjust overrideFontColor fontColor";
+}
+
+function GuiEditorControlSpec::geometryFields(%this)
+{
+ return "Position Extent HorizSizing VertSizing MinExtent";
+}
+
+function GuiEditorControlSpec::layoutFields(%this)
+{
+ return "MinExtent isContainer";
+}
+
+function GuiEditorControlSpec::tooltipFields(%this)
+{
+ return "tooltip tooltipWidth hovertime";
+}
+
+function GuiEditorControlSpec::scriptingFields(%this)
+{
+ return "class superclass internalName Variable Command AltCommand Accelerator";
+}
+
+function GuiEditorControlSpec::commandFields(%this)
+{
+ return "Command AltCommand Variable Accelerator";
+}
+
+function GuiEditorControlSpec::localizationFields(%this)
+{
+ return "langTableMod textID";
+}
+
+// Each easing beside the time it takes, because that is how they are read: the
+// section runs them in pairs and the grid puts two on a line.
+function GuiEditorControlSpec::easingFields(%this)
+{
+ return "easeFillColorHL easeTimeFillColorHL easeFillColorSL easeTimeFillColorSL";
+}
+
+// The toggles the header draws as icons instead of rows: what the control does
+// when the game runs.
+function GuiEditorControlSpec::runtimeToggles(%this)
+{
+ return "Visible Active useInput";
+}
+
+// The two fields the pane deliberately does not offer AT ALL. They are editor
+// working state -- neither is ever written to a file -- and they live in the
+// Explorer tree's two columns, where a whole branch can be read at once.
+//
+// This still has to name them. buildOtherSection sweeps up every persist field
+// no section claimed, so dropping them from here would not remove them from the
+// pane: it would move them into "Other" as two generic checkboxes, which is the
+// exact thing moving them out was meant to stop.
+function GuiEditorControlSpec::editorToggles(%this)
+{
+ return "hidden locked";
+}
+
+// A window's six switches, which are what its title bar offers and what it lets
+// you do to it. Six captioned checkboxes were a section of their own; six icons
+// are a row that fits beside Title Height.
+function GuiEditorControlSpec::windowToggles(%this)
+{
+ return "canMove canClose canMinimize canMaximize resizeWidth resizeHeight";
+}
+
+// Which of the header's three runtime switches a class actually has.
+//
+// Not simply "all of them unless bare". A bare class calls
+// SimObject::initPersistFields rather than GuiControl's, and may then register
+// some of GuiControl's names again for itself - which GuiMenuItemCtrl does with
+// Active and Visible (see GuiMenuItemCtrl::initPersistFields). Those two are
+// real fields on it and the switches work; useInput is not, and does not.
+function GuiEditorControlSpec::stateToggles(%this, %class)
+{
+ if(%this.hasFlag(%class, "bare"))
+ {
+ return (%class $= "GuiMenuItemCtrl") ? "Visible Active" : "";
+ }
+
+ return "Visible Active useInput";
+}
+
+// Never shown anywhere, for any class.
+function GuiEditorControlSpec::deadFields(%this)
+{
+ return "canSave canSaveDynamicFields parentGroup";
+}
+
+// Space-delimited membership. Wrapping both sides in spaces keeps text from
+// matching inside textWrap.
+function GuiEditorControlSpec::listHas(%this, %list, %item)
+{
+ return strstr(" " @ %list @ " ", " " @ %item @ " ") >= 0;
+}
+
+//-----------------------------------------------------------------------------
+// Class lookup. An unrecognized class -- a C++ subclass added after this table
+// was written -- is treated as unknown, and an unknown class shows everything
+// rather than less. The pane puts its own fields in a single "Other" section,
+// which lands it back at roughly what the native inspector did.
+//-----------------------------------------------------------------------------
+
+function GuiEditorControlSpec::isKnownClass(%this, %class)
+{
+ return %class !$= "" && %this.textRole[%class] !$= "";
+}
+
+function GuiEditorControlSpec::textRoleFor(%this, %class)
+{
+ return %this.isKnownClass(%class) ? %this.textRole[%class] : "render";
+}
+
+function GuiEditorControlSpec::hasFlag(%this, %class, %flag)
+{
+ if(!%this.isKnownClass(%class))
+ {
+ // An unknown class gets the permissive answer everywhere except "bare",
+ // which would strip its entire header.
+ return %flag !$= "bare";
+ }
+ return %this.listHas(%this.flags[%class], %flag);
+}
+
+function GuiEditorControlSpec::classHides(%this, %class, %field)
+{
+ return %this.isKnownClass(%class) && %this.listHas(%this.hides[%class], %field);
+}
+
+//-----------------------------------------------------------------------------
+// Geometry. This one is not a property of the control's class but of its
+// parent's: four containers write their children's bounds outright, so the
+// fields they own must be read-only wherever the child happens to sit. It has
+// to be recomputed when a control is reparented, not only when it is selected.
+//-----------------------------------------------------------------------------
+
+// full the control owns its position, extent and both sizing enums
+// none the parent owns all of it
+// chainV a vertical chain owns the Y position and the vertical sizing
+// chainH a horizontal chain owns the X position and the horizontal sizing
+// bar the control owns nothing but its height -- see below
+function GuiEditorControlSpec::geometryModeOf(%this, %ctrl)
+{
+ if(!isObject(%ctrl))
+ {
+ return "full";
+ }
+
+ // The one class that overrules its own geometry rather than its parent's.
+ // GuiMenuBarCtrl::resize throws away the position it is handed and passes
+ // (0,0), and onRender resizes the bar to the clip rect's width on every
+ // frame it draws -- but it keeps mBounds.extent.y, so the bar's height is
+ // its own and is the only geometry there is to set on it.
+ if(%ctrl.getClassName() $= "GuiMenuBarCtrl")
+ {
+ return "bar";
+ }
+
+ // A tab page's geometry is not its parent's doing alone -- the class is
+ // only ever a child of a book -- but the answer is the same either way.
+ %parent = %ctrl.getParent();
+ if(!isObject(%parent))
+ {
+ return "full";
+ }
+
+ switch$(%parent.getClassName())
+ {
+ // guiGridCtrl.cc resize(): every child is resized into its cell.
+ case "GuiGridCtrl": return "none";
+
+ // guiFrameSetCtrl.cc: each frame resizes the control it holds.
+ case "GuiFrameSetCtrl": return "none";
+
+ // guiTabBookCtrl.cc: a page is forced to (0,0) and the page rect.
+ case "GuiTabBookCtrl": return "none";
+
+ // guiChainCtrl.cc positionChildren(): only the stacking axis is taken.
+ // The cross axis keeps the position the child was given, and the extent
+ // is the child's own on both axes -- the chain reads it to lay out.
+ // onChildAdded also rewrites a "center" sizing on the stacking axis,
+ // which is why that enum goes too.
+ case "GuiChainCtrl": return %parent.IsVertical ? "chainV" : "chainH";
+ }
+
+ // A GuiScrollCtrl is the container people expect to own its children and
+ // does not: it scrolls them and leaves their bounds alone.
+ return "full";
+}
+
+// Grey or gone. Where the PARENT owns a field the control still has one, and
+// blanking it would leave no way to see where the parent put it -- so those are
+// greyed, with a tooltip saying why. Where the control's own class overwrites a
+// field every frame there is nothing to look at, so the row goes entirely.
+function GuiEditorControlSpec::isGeometryFieldShown(%this, %mode, %field)
+{
+ if(%mode $= "bar")
+ {
+ return %field $= "Extent";
+ }
+ return true;
+}
+
+// True when the control, sitting where it is, may edit this geometry field.
+function GuiEditorControlSpec::isGeometryFieldLive(%this, %mode, %field)
+{
+ if(%mode $= "none")
+ {
+ return false;
+ }
+ if(%mode $= "bar")
+ {
+ // Only the extent, and only half of it -- see liveExtentAxes.
+ return %field $= "Extent";
+ }
+ if(%mode $= "chainV")
+ {
+ return %field !$= "VertSizing";
+ }
+ if(%mode $= "chainH")
+ {
+ return %field !$= "HorizSizing";
+ }
+ return true;
+}
+
+// Which position axes the control still controls: "" , "x", "y" or "xy".
+function GuiEditorControlSpec::livePositionAxes(%this, %mode)
+{
+ switch$(%mode)
+ {
+ case "none": return "";
+ case "bar": return "";
+ case "chainV": return "x";
+ case "chainH": return "y";
+ }
+ return "xy";
+}
+
+// The same question for the extent. A menu bar is the one control that owns one
+// axis of its size and not the other: its width is rewritten to its parent's on
+// every frame it draws, and its height is never touched.
+function GuiEditorControlSpec::liveExtentAxes(%this, %mode)
+{
+ switch$(%mode)
+ {
+ case "none": return "";
+ case "bar": return "y";
+ }
+ return "xy";
+}
+
+//-----------------------------------------------------------------------------
+// The single filter predicate for the shared rows. Everything the pane hides or
+// shows among them goes through here, so the rules live in one place.
+//-----------------------------------------------------------------------------
+
+function GuiEditorControlSpec::isFieldVisible(%this, %class, %field)
+{
+ if(%this.listHas(%this.deadFields(), %field))
+ {
+ return false;
+ }
+
+ if(%this.classHides(%class, %field))
+ {
+ return false;
+ }
+
+ // A menu item calls SimObject::initPersistFields directly rather than
+ // GuiControl's, so it has none of these fields to show.
+ %bare = %this.hasFlag(%class, "bare");
+
+ %role = %this.textRoleFor(%class);
+
+ if(%this.listHas(%this.textFields(), %field))
+ {
+ if(%field $= "text" || %field $= "textID")
+ {
+ // Every role but proxy has a string of its own worth editing.
+ return %role !$= "none" && %role !$= "proxy";
+ }
+
+ // The layout half needs renderText to actually run on this control.
+ if(%role $= "caption")
+ {
+ return false;
+ }
+
+ // A placeholder is one line by definition. It is what a drop-down draws
+ // while nothing is chosen and it stops being drawn the moment something
+ // is, so wrapping it -- or sizing the control to fit it -- describes a
+ // control that changes shape when it is used.
+ if(%role $= "placeholder" && (%field $= "textWrap" || %field $= "textExtend"))
+ {
+ return false;
+ }
+ if(%field $= "fontSizeAdjust" && %this.hasFlag(%class, "fontAdjust"))
+ {
+ return true;
+ }
+ return %role $= "render" || %role $= "placeholder" ||
+ %role $= "inherited" || %role $= "proxy";
+ }
+
+ if(%bare)
+ {
+ return false;
+ }
+
+ if(%field $= "isContainer")
+ {
+ // Dead where the control cannot draw children: GuiControl's
+ // setIsContainerFn forces the field false for those, so offering it
+ // would be a switch wired to nothing.
+ return false;
+ }
+
+ if(%this.listHas(%this.easingFields(), %field))
+ {
+ return %this.hasFlag(%class, "easing");
+ }
+
+ return true;
+}
+
+// isContainer needs the control, not just its class -- rendersChildren() is a
+// C++ answer fixed by the class, read rather than tabled so it cannot drift.
+function GuiEditorControlSpec::isContainerFieldVisible(%this, %ctrl)
+{
+ return isObject(%ctrl) && !%this.hasFlag(%ctrl.getClassName(), "bare") &&
+ %ctrl.rendersChildren();
+}
+
+// Where the class's text block goes. The block itself is one component holding
+// every field GuiControl::renderText reads, so the only question left here is
+// which of the two copies of it -- the header's or the Text section's -- the
+// class wants.
+//
+// header the string, or the type it is drawn in, is a principal property.
+// Proxy joins the three header roles: a list draws its items with
+// this control's font, and the size of that is worth reaching for
+// even though the "text" field itself is never drawn.
+// section live, but not what anyone opens the pane for. A grid can draw text
+// and nobody asks it to; a slider draws none but still sizes its
+// value with fontSizeAdjust.
+// none nothing in the block applies.
+function GuiEditorControlSpec::textBlockHome(%this, %class)
+{
+ %role = %this.textRoleFor(%class);
+ if(%role $= "inherited")
+ {
+ return "section";
+ }
+ if(%role $= "none")
+ {
+ return %this.hasFlag(%class, "fontAdjust") ? "section" : "none";
+ }
+ return "header";
+}
+
+// The text block owns every field in textFields() except textID, which belongs
+// to Localization and is the only one of the nine that is not about how the
+// text is drawn.
+//
+// These three of them are ordinary field rows, so they go in the pane's shared
+// registry and are filtered and loaded with everything else. The other four are
+// two toggle icons and two segmented rows, which the block owns outright.
+//
+// overrideFontColor is in neither group: it has no row at all. The font color
+// swatch is both fields, because picking a color IS turning the override on.
+function GuiEditorControlSpec::textBlockRowFields(%this)
+{
+ return "text fontSizeAdjust fontColor";
+}
+
+//-----------------------------------------------------------------------------
+// Profile category. Every class but one is pinned by
+// GuiEditorThemeApplier::buildClassTable -- a check box wants a CheckBox
+// profile, and there is nothing to ask about. A bare GuiControl is the
+// exception: in 4.0 it is the wrapper, the backdrop, the line of text, the
+// paragraph and the modal scrim, and which of those it is cannot be read off
+// the class. The applier guesses from what the control holds when it is dropped
+// (categoryForControl), which is a good default and a guess all the same, so
+// the pane offers these four and lets the answer be corrected.
+//
+// The choice needs no storage: a profile carries the category it was stamped
+// for, so what the control wears is the record of what it was told to be.
+//-----------------------------------------------------------------------------
+
+function GuiEditorControlSpec::categoryChoices(%this, %class)
+{
+ return (%class $= "GuiControl") ? "Empty Panel Label Overlay" : "";
+}
+
+// Which of the choices a profile's category names, spelled the way the choices
+// spell it -- or "" if it names none of them.
+//
+// The case has to be thrown away to match and kept to answer. A category is
+// stored with StringTable->insert, which interns case-insensitively and hands
+// back whichever spelling reached the table first, so a profile stamped for
+// "Empty" reads back as "empty" if anything else interned that word in lower
+// case first. listHas cannot be used here: it is built on strstr, which unlike
+// $= is case-sensitive.
+function GuiEditorControlSpec::matchCategory(%this, %choices, %category)
+{
+ if(%category $= "")
+ {
+ return "";
+ }
+
+ %count = getWordCount(%choices);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %choice = getWord(%choices, %i);
+ if(%choice $= %category)
+ {
+ return %choice;
+ }
+ }
+ return "";
+}
+
+// What to call the text field. A drop-down only ever shows it while nothing is
+// selected, and a panel and a tab page hand theirs to something else to draw.
+function GuiEditorControlSpec::textLabelFor(%this, %class)
+{
+ switch$(%this.textRoleFor(%class))
+ {
+ case "placeholder": return "Placeholder";
+ case "caption": return (%class $= "GuiTabPageCtrl") ? "Tab Caption" : "Header Text";
+ }
+ return "Text";
+}
+
+//-----------------------------------------------------------------------------
+// Dynamic fields. A GuiFrameSetCtrl serializes its whole frame tree into
+// dynamic fields (guiFrameSetCtrl.cc writeFields), and hand-editing those can
+// leave a Gui that will not load. Nothing else in the engine does this.
+//-----------------------------------------------------------------------------
+
+function GuiEditorControlSpec::dynamicHidePrefixes(%this, %class)
+{
+ if(%class $= "GuiFrameSetCtrl")
+ {
+ return "frame";
+ }
+ return "";
+}
+
+function GuiEditorControlSpec::hidesDynamicField(%this, %class, %name)
+{
+ %prefixes = %this.dynamicHidePrefixes(%class);
+ %count = getWordCount(%prefixes);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %prefix = getWord(%prefixes, %i);
+ if(strlwr(getSubStr(%name, 0, strlen(%prefix))) $= strlwr(%prefix))
+ {
+ return true;
+ }
+ }
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Field presentation. getFieldType answers with a console type's class name --
+// what ConsoleType's first argument spells -- so the mapping is from those, not
+// from the TypeXxx constants the engine uses in initPersistFields.
+//-----------------------------------------------------------------------------
+
+function GuiEditorControlSpec::kindForType(%this, %type)
+{
+ switch$(%type)
+ {
+ case "bool": return "bool";
+ case "int" or "char": return "number";
+
+ // Separate kinds for the real-numbered fields, because the row rounds a
+ // whole-number one on the way out. A slider's value is a TypeF32 between
+ // its two bounds and fontSizeAdjust is a multiplier around 1: rounding
+ // either is the difference between the field working and not.
+ case "float": return "decimal";
+ case "enumval": return "enum";
+ case "Point2I": return "point";
+ case "Point2F" or "Vector2": return "pointf";
+ case "ColorI" or "ColorF" or "FluidColorI": return "color";
+ case "filename": return "file";
+ case "assetIdString": return "asset";
+ case "GuiProfile": return "profile";
+ // Both stay hidden from the generic path that fills the Other section.
+ // A border profile has nothing to offer there at all -- the Profile
+ // Editor owns borders and a control never names one. A cursor does, but
+ // only sometimes: buildVariantsSection adds its row deliberately, and
+ // only where the theme holds a choice. Answering "dropdown" here would
+ // instead put an empty one on every window, which is the noise the
+ // Variants rule exists to prevent.
+ case "GuiCursor" or "GuiBProfile": return "hidden";
+ }
+ return "text";
+}
+
+// Human labels for the fields whose registered name reads badly in a caption.
+// Anything absent falls back to the field name, which is usually right.
+function GuiEditorControlSpec::buildLabels(%this)
+{
+ %this.label["HorizSizing"] = "Horizontal Sizing";
+ %this.label["VertSizing"] = "Vertical Sizing";
+ %this.label["MinExtent"] = "Minimum Extent";
+ %this.label["useInput"] = "Accepts Input";
+ %this.label["isContainer"] = "Accepts Children";
+ %this.label["hovertime"] = "Hover Time";
+ %this.label["tooltip"] = "Tooltip";
+ %this.label["tooltipWidth"] = "Tooltip Width";
+ %this.label["class"] = "Class";
+ %this.label["langTableMod"] = "Language Table";
+ %this.label["textID"] = "Text ID";
+ %this.label["fontSizeAdjust"] = "Font Size Adjust";
+ %this.label["overrideFontColor"] = "Override Font Color";
+ %this.label["align"] = "Horizontal Align";
+ %this.label["vAlign"] = "Vertical Align";
+ %this.label["textWrap"] = "Wrap Text";
+ %this.label["textExtend"] = "Extend To Fit Text";
+ %this.label["stateOn"] = "Checked";
+ %this.label["groupNum"] = "Radio Group";
+ %this.label["IsVertical"] = "Vertical";
+ %this.label["ChildSpacing"] = "Child Spacing";
+ %this.label["IsExtentDynamic"] = "Grow To Fit";
+ %this.label["constantThumbHeight"] = "Constant Thumb";
+ %this.label["scrollBarThickness"] = "Bar Thickness";
+ %this.label["showArrowButtons"] = "Arrow Buttons";
+ %this.label["hScrollBar"] = "Horizontal Bar";
+ %this.label["vScrollBar"] = "Vertical Bar";
+ %this.label["maxHeight"] = "Max Height";
+ %this.label["titleHeight"] = "Title Height";
+ %this.label["resizeRightWidth"] = "Right Grip Width";
+ %this.label["resizeBottomHeight"] = "Bottom Grip Height";
+ %this.label["DividerThickness"] = "Divider Thickness";
+ %this.label["MinTabWidth"] = "Min Tab Width";
+ %this.label["TabPosition"] = "Tab Position";
+ %this.label["AllowMultipleSelections"] = "Multiple Selection";
+ %this.label["FitParentWidth"] = "Fit Parent Width";
+ %this.label["AllowReorder"] = "Allow Reorder";
+ %this.label["animationTime"] = "Animation Time";
+ %this.label["easeExpand"] = "Expand Easing";
+ %this.label["easeTimeExpand"] = "Expand Time";
+ %this.label["easeFillColorHL"] = "Hover Easing";
+ %this.label["easeFillColorSL"] = "Press Easing";
+ %this.label["easeTimeFillColorHL"] = "Hover Time";
+ %this.label["easeTimeFillColorSL"] = "Press Time";
+ %this.label["sinkAllKeyEvents"] = "Sink Key Events";
+ %this.label["returnCausesTab"] = "Return Tabs";
+ %this.label["returnCommand"] = "Return Command";
+ %this.label["escapeCommand"] = "Escape Command";
+ %this.label["maxLength"] = "Max Length";
+ %this.label["inputMode"] = "Input Mode";
+ %this.label["deleteOnMouseUp"] = "Delete On Drop";
+ %this.label["lockMouse"] = "Lock Mouse";
+ %this.label["UseWindowInputEvents"] = "Window Input Events";
+ %this.label["UseObjectInputEvents"] = "Object Input Events";
+ %this.label["UseBackgroundColor"] = "Use Background Color";
+ %this.label["BackgroundColor"] = "Background Color";
+ %this.label["constrainProportions"] = "Keep Proportions";
+ %this.label["positionOffset"] = "Position Offset";
+ %this.label["singleFrameBitmap"] = "Single Frame";
+ %this.label["NamedFrame"] = "Named Frame";
+ %this.label["imageColor"] = "Image Color";
+ %this.label["imageSize"] = "Image Size";
+ %this.label["fullSize"] = "Full Size";
+ %this.label["clampImage"] = "Clamp Image";
+ %this.label["tileImage"] = "Tile Image";
+ %this.label["baseColor"] = "Color";
+ %this.label["BaseColor"] = "Color";
+ %this.label["PickColor"] = "Picked Color";
+ %this.label["DisplayMode"] = "Display Mode";
+ %this.label["ActionOnMove"] = "Action On Move";
+ %this.label["ShowSelector"] = "Show Selector";
+ %this.label["showAlphaBar"] = "Alpha Bar";
+ %this.label["showColorValues"] = "Color Values";
+ %this.label["swatchColumns"] = "Swatch Columns";
+ %this.label["valueBoxHeight"] = "Value Box Height";
+ %this.label["valueMode"] = "Value Mode";
+ %this.label["popupSize"] = "Popup Size";
+ %this.label["barHeight"] = "Bar Height";
+ %this.label["boxOffset"] = "Box Offset";
+ %this.label["boxExtent"] = "Box Extent";
+ %this.label["textOffset"] = "Text Offset";
+ %this.label["textExtent"] = "Text Extent";
+ %this.label["CellModeX"] = "Column Mode";
+ %this.label["CellModeY"] = "Row Mode";
+ %this.label["CellSizeX"] = "Column Size";
+ %this.label["CellSizeY"] = "Row Size";
+ %this.label["CellSpacingX"] = "Column Spacing";
+ %this.label["CellSpacingY"] = "Row Spacing";
+ %this.label["MaxColCount"] = "Max Columns";
+ %this.label["MaxRowCount"] = "Max Rows";
+ %this.label["OrderMode"] = "Order";
+ %this.label["canMove"] = "Move";
+ %this.label["canClose"] = "Close";
+ %this.label["canMinimize"] = "Minimize";
+ %this.label["canMaximize"] = "Maximize";
+ %this.label["resizeWidth"] = "Resize Width";
+ %this.label["resizeHeight"] = "Resize Height";
+ %this.label["internalName"] = "Internal Name";
+ %this.label["superclass"] = "Super Class";
+ %this.label["AltCommand"] = "Alt Command";
+ %this.label["IsOn"] = "On";
+
+ // The secondary profile slots, named for the part they style rather than
+ // for the field. These are the Variants rows; the slot-to-category mapping
+ // they are filtered by lives in GuiEditorThemeApplier::buildFieldTable.
+ %this.label["tooltipProfile"] = "Tooltip";
+ %this.label["contentProfile"] = "Content";
+ %this.label["closeButtonProfile"] = "Close Button";
+ %this.label["minButtonProfile"] = "Minimise Button";
+ %this.label["maxButtonProfile"] = "Maximise Button";
+ %this.label["scrollProfile"] = "Scroller";
+ %this.label["thumbProfile"] = "Thumb";
+ %this.label["trackProfile"] = "Track";
+ %this.label["arrowProfile"] = "Arrows";
+ %this.label["listBoxProfile"] = "List";
+ %this.label["tabBookProfile"] = "Tab Book";
+ %this.label["tabProfile"] = "Tab";
+ %this.label["tabPageProfile"] = "Tab Page";
+ %this.label["dropButtonProfile"] = "Drop Button";
+ %this.label["menuProfile"] = "Menu";
+ %this.label["menuItemProfile"] = "Menu Item";
+ %this.label["menuContentProfile"] = "Menu Content";
+ %this.label["popupProfile"] = "Popup";
+ %this.label["pickerProfile"] = "Picker";
+ %this.label["selectorProfile"] = "Selector";
+ %this.label["valueProfile"] = "Value Boxes";
+ %this.label["backgroundProfile"] = "Background";
+
+ // Cursor slots. Named for the job rather than the direction the field is:
+ // "nWSECursor" describes the arrow's art, "Corner" describes what hovering
+ // there does, and only one of those is a question the person laying out a
+ // window is asking.
+ %this.label["editCursor"] = "Text Cursor";
+ %this.label["leftRightCursor"] = "Horizontal Resize";
+ %this.label["upDownCursor"] = "Vertical Resize";
+ %this.label["nWSECursor"] = "Corner Resize";
+}
+
+function GuiEditorControlSpec::labelFor(%this, %field)
+{
+ %label = %this.label[%field];
+ return (%label $= "") ? %field : %label;
+}
+
+//-----------------------------------------------------------------------------
+// Drift guard. The editor never calls this; the smoke test does, so a control
+// class added to the engine cannot quietly fall through to the unknown-class
+// fallback without anyone noticing.
+//
+// %engineNames is what enumerateConsoleClasses("GuiControl") returns: a
+// tab-separated list, whose first entry it repeats. Duplicates cost nothing
+// here, since each name is only looked up. The classes the Gui Editor's palette
+// refuses are not this table's business, so they are dropped with the same rule
+// the palette uses (GuiEditorControlListWindow::populate).
+//-----------------------------------------------------------------------------
+
+function GuiEditorControlSpec::isPlaceableClass(%this, %name)
+{
+ if(%name $= "GuiCanvas" || %name $= "GuiMessageVectorCtrl" ||
+ %name $= "GuiParticleGraphInspector" || %name $= "GuiGraphCtrl" ||
+ %name $= "GuiSceneObjectCtrl")
+ {
+ return false;
+ }
+ if(%name !$= "SceneWindow" && getSubStr(%name, 0, 3) !$= "Gui")
+ {
+ return false;
+ }
+ return getSubStr(%name, 0, 10) !$= "GuiConsole" &&
+ getSubStr(%name, 0, 7) !$= "GuiEdit" &&
+ getSubStr(%name, 0, 12) !$= "GuiInspector";
+}
+
+function GuiEditorControlSpec::findMissingClasses(%this, %engineNames)
+{
+ %missing = "";
+ %count = getFieldCount(%engineNames);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %name = trim(getField(%engineNames, %i));
+ if(%name $= "" || !%this.isPlaceableClass(%name) || %this.textRole[%name] !$= "")
+ {
+ continue;
+ }
+ %missing = (%missing $= "") ? %name : (%missing SPC %name);
+ }
+ return %missing;
+}
diff --git a/editor/GuiEditor/scripts/GuiEditorControlTile.cs b/editor/GuiEditor/scripts/GuiEditorControlTile.cs
new file mode 100644
index 000000000..7f9fffe29
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiEditorControlTile.cs
@@ -0,0 +1,370 @@
+//-----------------------------------------------------------------------------
+// One entry in the control palette: a picture of a control you can drag onto the
+// canvas, or click to drop into whatever container is current.
+//
+// The tile holds a palette KEY rather than a class name. For all but four
+// entries those are the same string, but a bare GuiControl appears four times --
+// as the wrapper, the backdrop, the line of text and the modal scrim -- and the
+// key is what tells them apart. GuiEditorControlIcons turns a key into a class,
+// a profile category, a label and a frame.
+//
+// Two shapes, set by setMode and driven by the group's grid changing its cell
+// size underneath. Nothing here re-reads the extent on resize: the children
+// carry sizing flags that keep them placed, so dragging the frame narrower
+// reflows the grid and the tiles follow without script.
+//
+// grid a 56px picture with the name under it, wrapping to two lines
+// rows a 32px picture at the left with the name beside it
+//
+// The creator sets key and owner inline, then calls setMode.
+//-----------------------------------------------------------------------------
+
+// Both sizes come off the 64 sheet -- sheetFor only reaches for the 128 one
+// above 64. Rows mode is at that sheet's own resolution and takes it 1:1; grid
+// mode shrinks it by an eighth, which is a gentler downscale than 128 to 56
+// would be and holds the thin strokes better.
+$GuiEditorControlTile::gridArt = 56;
+$GuiEditorControlTile::rowArt = 32;
+$GuiEditorControlTile::rowTextLeft = 40;
+
+// How much of a grid tile is kept clear for the name: two lines of the largest
+// label font any shipped theme uses -- Torque Suit's fontSize - 2, which is 20
+// -- plus the two pixels labelProfile pads above and below.
+//
+// This is a budget the icon is placed against, not a box the text is put in.
+// The caption itself fills the tile and bottom-aligns, so a third line would
+// grow UP into the picture rather than off the bottom. Nothing in the table can
+// reach three: the longest label wraps to two at this width, and the only other
+// source of names is the Undrawn sweep, whose entries are raw class names -- one
+// unbreakable word, kept to a single line and clipped across, with the full name
+// in the tooltip either way.
+$GuiEditorControlTile::gridCaption = 44;
+
+// How far the pointer must travel before a press counts as a drag rather than a
+// click. Without it a shaky click starts a drag, and the two gestures do
+// different things now that a click also places a control.
+$GuiEditorControlTile::dragSlop = 5;
+
+function GuiEditorControlTile::onAdd(%this)
+{
+ %icons = GuiEditor.controlIcons;
+
+ %this.text = "";
+ %this.dragged = false;
+
+ // The label reads "Check Box"; the tooltip says GuiCheckBoxCtrl. The class
+ // name is what someone types in script, so it should not disappear from the
+ // palette just because the tile is showing a friendlier one. Both modes show
+ // the label, so the tooltip is the only place the class name appears.
+ %this.tooltip = %icons.classFor(%this.key);
+
+ %this.icon = new GuiSpriteCtrl()
+ {
+ Position = "0 0";
+ Extent = "16 16";
+ constrainProportions = "1";
+ fullSize = "0";
+ UseInput = false;
+ };
+ ThemeManager.setProfile(%this.icon, "spriteProfile");
+ %this.add(%this.icon);
+
+ %this.caption = new GuiControl()
+ {
+ Position = "0 0";
+ Extent = "16 16";
+ Text = %icons.labelFor(%this.key);
+ align = "left";
+ vAlign = "middle";
+ Visible = false;
+ UseInput = false;
+ };
+ ThemeManager.setProfile(%this.caption, "labelProfile");
+ %this.add(%this.caption);
+
+ ThemeManager.setProfile(%this, "tipProfile", "TooltipProfile");
+
+ %this.startListening(ThemeManager);
+ %this.refreshTint();
+}
+
+//-----------------------------------------------------------------------------
+// Color.
+//
+// The sheets are greyscale, drawn to be modulated rather than to be shown as
+// they are -- so the tint is not decoration, it is what makes the picture
+// legible. Left alone a GuiSpriteCtrl blends with opaque white, which is why
+// this was invisible on the theme the editor starts in: that one draws its text
+// white too, so an icon nobody had tinted looked exactly right. On the light
+// theme the same untinted art is a white smear on a pale panel.
+//
+// The color comes from the tile's OWN profile rather than from the caption's,
+// because the icon is drawn on the tile. The two name the same color in every
+// theme that ships, and the profile a thing is drawn on is the one that should
+// decide when they stop agreeing.
+//
+// It has to be re-read on a theme change: ThemeManager swaps the profile object
+// on everything that registered one, which is enough for backgrounds and text,
+// but a color copied onto a sprite is a copy and stays behind.
+//-----------------------------------------------------------------------------
+
+function GuiEditorControlTile::refreshTint(%this)
+{
+ %this.icon.setImageColor(ThemeManager.activeTheme.itemSelectProfile.fontColor);
+}
+
+function GuiEditorControlTile::onThemeChange(%this, %theme)
+{
+ %this.refreshTint();
+}
+
+//-----------------------------------------------------------------------------
+// Layout.
+//-----------------------------------------------------------------------------
+
+function GuiEditorControlTile::setMode(%this, %mode)
+{
+ %this.mode = %mode;
+ %w = getWord(%this.getExtent(), 0);
+ %h = getWord(%this.getExtent(), 1);
+
+ // One caption serves both modes, so each branch sets every property the other
+ // one touches. Leaving a mode to inherit what the last one happened to write
+ // is how a switch back stops being a switch back.
+ if(%mode $= "rows")
+ {
+ %art = $GuiEditorControlTile::rowArt;
+ %left = $GuiEditorControlTile::rowTextLeft;
+
+ // anchorLeft holds the left edge and lets the right one move, so the
+ // picture stays put as the column widens; the caption takes the slack.
+ %this.icon.HorizSizing = "anchorLeft";
+ %this.icon.VertSizing = "center";
+ %this.icon.setExtent(%art, %art);
+ %this.icon.setPosition(4, (%h - %art) / 2);
+
+ %this.caption.HorizSizing = "width";
+ %this.caption.VertSizing = "center";
+ %this.caption.setExtent(%w - %left - 4, %art);
+ %this.caption.setPosition(%left, (%h - %art) / 2);
+ %this.caption.align = "left";
+ %this.caption.vAlign = "middle";
+
+ // A row is one line tall, so wrapping there would only ever hide the
+ // tail of a name the row has the width to show.
+ %this.caption.textWrap = false;
+ %this.caption.setVisible(true);
+ }
+ else
+ {
+ %art = $GuiEditorControlTile::gridArt;
+ %band = $GuiEditorControlTile::gridCaption;
+
+ // The caption takes the whole inner rect and bottom-aligns its text
+ // inside it, so the ENGINE decides where the floor of the tile is.
+ // Nothing here has to know what the tile's own profile insets, which is
+ // a per-theme number -- 3 pixels a side on the base theme, 4 on Torque
+ // Suit -- that script has no way to ask for. Positioning a short band
+ // against the outer extent instead would hang it below the inner rect
+ // and renderChild would clip the last line's descenders off.
+ //
+ // Bottom alignment is also what makes a row read level: a one-line name
+ // like "Check Box" sits on the same line as the second line of "Radio
+ // Button" beside it, rather than floating half a line higher.
+ %this.caption.HorizSizing = "fill";
+ %this.caption.VertSizing = "fill";
+ %this.caption.align = "center";
+ %this.caption.vAlign = "bottom";
+ %this.caption.textWrap = true;
+ %this.caption.setVisible(true);
+ %this.caption.applySizing();
+
+ // And that fill is how the inset gets measured. What the caption now
+ // reports IS the inner rect, so the picture can be centered in the room
+ // left above the band whatever a theme's border costs, instead of
+ // sitting at a fixed offset a fatter border would push into the text.
+ %innerH = getWord(%this.caption.getExtent(), 1);
+
+ // "center" is resolved against the inner rect too, so the picture and
+ // the name below it share one center line. Doing this arithmetically
+ // off %w would put the icon half a border to the right of its label.
+ %this.icon.HorizSizing = "center";
+ %this.icon.VertSizing = "anchorTop";
+ %this.icon.setExtent(%art, %art);
+ %this.icon.setPosition(0, (%innerH - %band - %art) / 2);
+ %this.icon.applySizing();
+ }
+
+ // The sheet is picked from the size the art is DRAWN at, not from the mode,
+ // so the two stay in step if either number changes.
+ %this.icon.setImage(GuiEditor.controlIcons.sheetFor(%art));
+ %this.icon.setImageFrame(GuiEditor.controlIcons.frameFor(%this.key));
+ %this.icon.imageSize = %art SPC %art;
+}
+
+//-----------------------------------------------------------------------------
+// Making one. Shared by both gestures so a dragged control and a clicked one
+// cannot drift apart.
+//-----------------------------------------------------------------------------
+
+function GuiEditorControlTile::makePayload(%this)
+{
+ %class = GuiEditor.controlIcons.classFor(%this.key);
+ %payload = eval("return new " @ %class @ "();");
+ if(!isObject(%payload))
+ {
+ return 0;
+ }
+
+ // Only the four GuiControl faces ask for a category; every other class pins
+ // its own in GuiEditorThemeApplier::buildClassTable, so the field stays unset
+ // and the applier's own answer stands. It is consumed by the first theme
+ // pass -- see applyToBranch.
+ %category = GuiEditor.controlIcons.categoryFor(%this.key);
+ if(%category !$= "")
+ {
+ %payload.paletteCategory = %category;
+ }
+
+ // A caption for the controls that wear one, taken from the palette's own
+ // label so the tile and the control it makes agree. The engine used to seed
+ // "Button" in the constructor, but a caption the author clears has to
+ // survive a save and a constructor default cannot let it: writeField drops
+ // every empty value, so a blank caption is written as an absent one and the
+ // default stands back up on read. Placing a control is the moment that
+ // wants a placeholder, so the placeholder lives here.
+ if(%this.takesCaption(GuiEditor.controlIcons.classFor(%this.key)))
+ {
+ %payload.setText(GuiEditor.controlIcons.labelFor(%this.key));
+ }
+
+ return %payload;
+}
+
+//-----------------------------------------------------------------------------
+// Whether a freshly placed control should arrive with a caption. The button
+// family draws mText as its face and used to inherit "Button" from
+// GuiButtonCtrl -- which is why a checkbox dropped from the palette read
+// "Button" and not "Check Box". A drop down is deliberately absent: it draws
+// mText only while nothing is selected, so its "none" is an empty state and
+// not a caption to be replaced.
+//-----------------------------------------------------------------------------
+
+function GuiEditorControlTile::takesCaption(%this, %class)
+{
+ return %class $= "GuiButtonCtrl" ||
+ %class $= "GuiCheckBoxCtrl" ||
+ %class $= "GuiRadioCtrl";
+}
+
+//-----------------------------------------------------------------------------
+// Dragging. Lifted from the list box this replaced, offsets and all.
+//-----------------------------------------------------------------------------
+
+function GuiEditorControlTile::onTouchDown(%this, %modifier, %position, %clickCount)
+{
+ %this.pressAt = Canvas.getCursorPos();
+ %this.dragged = false;
+}
+
+function GuiEditorControlTile::onTouchDragged(%this, %modifier, %position, %clickCount)
+{
+ if(%this.dragged)
+ {
+ return;
+ }
+
+ %now = Canvas.getCursorPos();
+ %dx = mAbs(getWord(%now, 0) - getWord(%this.pressAt, 0));
+ %dy = mAbs(getWord(%now, 1) - getWord(%this.pressAt, 1));
+ if(%dx < $GuiEditorControlTile::dragSlop && %dy < $GuiEditorControlTile::dragSlop)
+ {
+ return;
+ }
+
+ %this.dragged = true;
+ %this.beginDrag(%now);
+}
+
+function GuiEditorControlTile::beginDrag(%this, %cursorPos)
+{
+ %payload = %this.makePayload();
+ if(!isObject(%payload))
+ {
+ return;
+ }
+
+ %position = GuiEditor.brain.getGlobalPosition();
+ %xOffset = (getWord(%payload.extent, 0) / 2) + getWord(%position, 0);
+ %yOffset = (getWord(%payload.extent, 1) / 2) + getWord(%position, 1);
+
+ // Where the drag starts, so the payload does not jump on the first frame.
+ %xPos = getWord(%cursorPos, 0) - %xOffset;
+ %yPos = getWord(%cursorPos, 1) - %yOffset;
+
+ %dragCtrl = new GuiDragAndDropCtrl()
+ {
+ canSaveDynamicFields = "0";
+ Profile = "GuiDragAndDropProfile";
+ HorizSizing = "anchorLeft";
+ VertSizing = "anchorTop";
+ Position = %xPos SPC %yPos;
+ extent = %payload.extent;
+ MinExtent = "32 32";
+ canSave = "1";
+ Visible = "1";
+ hovertime = "1000";
+ Text = GuiEditor.controlIcons.classFor(%this.key);
+ deleteOnMouseUp = true;
+ };
+
+ %dragCtrl.add(%payload);
+ GuiEditor.brain.add(%dragCtrl);
+ %dragCtrl.startDragging(%xOffset, %yOffset);
+}
+
+//-----------------------------------------------------------------------------
+// Clicking. A click is a drop that never moved.
+//
+// It routes through the brain's own onControlDropped rather than adding the
+// control itself, which is the whole point: that path is where theming,
+// selection, the AddControl event and undo recording all live, and a second way
+// into the document would have to reproduce every one of them and would go stale
+// the first time one changed.
+//-----------------------------------------------------------------------------
+
+function GuiEditorControlTile::onClick(%this)
+{
+ // A button keeps mDepressed through a drag -- it never overrides
+ // onTouchDragged -- so releasing after a drag fires onAction as well. Without
+ // this the gesture would place two controls.
+ if(%this.dragged)
+ {
+ %this.dragged = false;
+ return;
+ }
+
+ %payload = %this.makePayload();
+ if(!isObject(%payload))
+ {
+ return;
+ }
+
+ // Centred in the container being worked in, which the brain knows because it
+ // owns both the add set and the canvas the container has to be visible on.
+ //
+ // onControlDropped reads getGlobalPosition BEFORE addNewCtrl reparents, and
+ // puts the control back there afterwards. Nothing owns the payload yet, so
+ // its Position IS its global position.
+ %payload.Position = GuiEditor.brain.centredPlacement(%payload);
+
+ // Deliberately not onControlDragged first: that picks the add set by
+ // hit-testing the cursor, and a click means "the container I am already
+ // working in", not "whatever is under this point".
+ //
+ // And placeControl rather than onControlDropped, because the cursor test that
+ // one opens with is a drag's question. A click has no cursor, the position
+ // above is already inside the visible part of the container, and a control
+ // that pins its own position never took it anyway.
+ GuiEditor.brain.placeControl(%payload);
+}
diff --git a/editor/GuiEditor/scripts/GuiEditorDynamicFields.cs b/editor/GuiEditor/scripts/GuiEditorDynamicFields.cs
new file mode 100644
index 000000000..772579fb0
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiEditorDynamicFields.cs
@@ -0,0 +1,277 @@
+
+//-----------------------------------------------------------------------------
+// The dynamic-field section of the Gui Editor's properties pane: the fields a
+// script put on a control, which are not registered anywhere and so cannot come
+// from GuiEditorControlSpec.
+//
+// Built fresh rather than ported. GuiInspectorDynamicGroup::createContent is
+// broken in three ways that are not worth carrying over:
+//
+// - it hardcodes the profile names "EditorButton", "GuiButtonProfile" and
+// "GuiTextProfile", which were 3.x script profiles and do not exist in 4.0,
+// so its Add button and the shell around it wear nothing;
+// - it calls registerObject("zAddButton") -- a fixed global name -- so
+// inspecting a second object collides with the first, and in editor mode
+// the name is never registered at all;
+// - the Add button is created in createContent but has to be rescued and
+// pushed back by clearFields on every refresh.
+//
+// What a control holds is filtered, not just listed. A GuiFrameSetCtrl
+// serializes its entire frame tree into dynamic fields (frameID0, frameChild10,
+// frameExtentX2 and so on -- see guiFrameSetCtrl.cc), and hand-editing those
+// can leave a Gui that will not load. GuiEditorControlSpec::hidesDynamicField
+// owns that list.
+//
+// Renaming is deliberately absent: remove and add again. The old inspector
+// offered it, but a rename is a delete and an add anyway, and making it one
+// visible step costs a name box per row that is wrong to touch by accident.
+//
+// A dynamic field cannot hold an empty value. SimFieldDictionary::setFieldValue
+// frees the entry when the value is empty, so "" is not a value a field can
+// have -- it is how a field is removed. That decides the shape of Add: naming a
+// field cannot create it, because there is nothing to create it with. So Add
+// puts up a row for the name and the field appears on the control the moment
+// the row is given a value; a row left empty was never a field. It also makes
+// the bin button and clearing the box the same operation, which is what the
+// engine already believed.
+//
+// The creator sets pane and blockWidth inline, then calls build() once after
+// adding it. Each row reports here rather than to the pane, because a dynamic
+// field is written with setFieldValue on a name this section knows and the pane
+// does not.
+//-----------------------------------------------------------------------------
+
+function GuiEditorDynamicFields::onAdd(%this)
+{
+ ThemeManager.setProfile(%this, "emptyProfile");
+}
+
+function GuiEditorDynamicFields::build(%this)
+{
+ %this.fieldNames = "";
+
+ %this.grid = %this.pane.makeCellGrid(0);
+ %this.add(%this.grid);
+
+ %this.buildAddRow();
+}
+
+// A name box and an Add button. Inline rather than a dialog: naming a field is
+// the whole of the operation, so a modal for it would be ceremony.
+function GuiEditorDynamicFields::buildAddRow(%this)
+{
+ %w = %this.blockWidth;
+ %buttonW = 56;
+
+ %row = new GuiControl()
+ {
+ HorizSizing = "width";
+ Position = "0 0";
+ Extent = %w SPC 30;
+ };
+ ThemeManager.setProfile(%row, "emptyProfile");
+ %this.add(%row);
+ %this.addRow = %row;
+
+ %this.nameBox = new GuiTextEditCtrl()
+ {
+ HorizSizing = "width";
+ Position = "4 4";
+ Extent = (%w - %buttonW - 16) SPC 22;
+ Tooltip = "Name for a new dynamic field";
+ };
+ ThemeManager.setProfile(%this.nameBox, "textEditProfile");
+ ThemeManager.setProfile(%this.nameBox, "tipProfile", "TooltipProfile");
+ %this.nameBox.ReturnCommand = %this.getID() @ ".onAddClicked();";
+ %row.add(%this.nameBox);
+
+ %this.addButton = new GuiButtonCtrl()
+ {
+ HorizSizing = "left";
+ Position = (%w - %buttonW - 4) SPC 4;
+ Extent = %buttonW SPC 22;
+ Text = "Add";
+ Command = %this.getID() @ ".onAddClicked();";
+ };
+ ThemeManager.setProfile(%this.addButton, "buttonProfile");
+ %row.add(%this.addButton);
+}
+
+//-----------------------------------------------------------------------------
+// Binding. The rows are rebuilt per control rather than filtered: unlike a
+// registered field there is no fixed set to hide from, and two controls rarely
+// carry the same dynamic fields.
+//-----------------------------------------------------------------------------
+
+function GuiEditorDynamicFields::bind(%this, %ctrl)
+{
+ // A named-but-empty row belongs to the control it was named on. Moving the
+ // selection abandons it, which is right: it was never a field.
+ if(%ctrl != %this.target)
+ {
+ %this.pendingField = "";
+ }
+
+ %this.target = %ctrl;
+ %this.grid.deleteObjects();
+ %this.fieldNames = "";
+
+ if(!isObject(%ctrl))
+ {
+ return;
+ }
+
+ %class = %ctrl.getClassName();
+ %count = %ctrl.getDynamicFieldCount();
+ for(%i = 0; %i < %count; %i++)
+ {
+ %name = getWord(%ctrl.getDynamicField(%i), 0);
+ if(%name $= "" || %this.pane.spec.hidesDynamicField(%class, %name))
+ {
+ continue;
+ }
+ %this.addFieldRow(%name);
+ }
+
+ // The row for a field that has been named but not yet given a value. It is
+ // last because it is the newest, and it disappears on its own if it is left
+ // empty and the selection moves.
+ if(%this.pendingField !$= "" && %ctrl.getFieldValue(%this.pendingField) $= "")
+ {
+ %this.addFieldRow(%this.pendingField);
+ }
+}
+
+function GuiEditorDynamicFields::addFieldRow(%this, %name)
+{
+ %row = new GuiControl()
+ {
+ class = "EditorFieldRow";
+ Position = "0 0";
+ fieldName = %name;
+ labelText = %name;
+ kind = "text";
+ owner = %this;
+ };
+ %this.grid.add(%row);
+ %row.build();
+
+ // The row's reset button is already a small icon pinned to the cell's right
+ // edge, which is exactly where a remove wants to be, so it wears the bin;
+ // the row calls owner.onFieldRowReset when it is clicked, and for a
+ // dynamic field "reset" means "take it away".
+ %row.resetButton.icon.setImageFrame($EditorIcon::trash);
+ %row.resetButton.Tooltip = "Remove this field";
+ %row.resetButton.setVisible(true);
+
+ %row.setValue(%this.target.getFieldValue(%name));
+
+ %this.row[%name] = %row;
+ %this.fieldNames = (%this.fieldNames $= "") ? %name : (%this.fieldNames SPC %name);
+ return %row;
+}
+
+// Is there anything to show? The section hides itself when a control carries no
+// dynamic fields, which is most of them.
+function GuiEditorDynamicFields::hasFields(%this)
+{
+ return %this.fieldNames !$= "";
+}
+
+//-----------------------------------------------------------------------------
+// Editing.
+//-----------------------------------------------------------------------------
+
+function GuiEditorDynamicFields::onFieldRowCommit(%this, %row)
+{
+ if(!isObject(%this.target) || !%row.hasChanged())
+ {
+ return;
+ }
+
+ // A dynamic field has no setEditFieldValue path, so the recorder writes it
+ // as its own kind of op -- but it goes through the recorder like every other
+ // write the editor makes.
+ %value = %row.getValue();
+ GuiEditor.undoRecorder.writeDynamicField(%this.target, %row.fieldName, %value);
+ %row.markClean();
+ %this.pane.afterCommit();
+
+ if(%row.fieldName $= %this.pendingField && %value !$= "")
+ {
+ // It is a real field now, so it will come back from the control's own
+ // list and no longer needs holding open.
+ %this.pendingField = "";
+ }
+ else if(%value $= "")
+ {
+ // An emptied box is a removed field, the same as the bin button.
+ // Deferred for the same reason: this arrives from inside the row.
+ %this.schedule(0, "rebindDeferred");
+ }
+}
+
+// Remove. A dynamic field is cleared by writing "" to it -- there is no delete
+// -- and the row goes with it, so this rebinds rather than trying to unpick one
+// cell from the grid.
+function GuiEditorDynamicFields::onFieldRowReset(%this, %row)
+{
+ if(!isObject(%this.target))
+ {
+ return;
+ }
+
+ GuiEditor.undoRecorder.writeDynamicField(%this.target, %row.fieldName, "");
+ %this.pane.afterCommit();
+
+ // Deferred: this call arrives from the button inside the row that is about
+ // to be freed, so deleting the grid's children here would pull the control
+ // out from under the event being dispatched.
+ %this.schedule(0, "rebindDeferred");
+}
+
+function GuiEditorDynamicFields::rebindDeferred(%this)
+{
+ %this.bind(%this.target);
+ %this.pane.onDynamicFieldsChanged();
+}
+
+function GuiEditorDynamicFields::onAddClicked(%this)
+{
+ %name = trim(%this.nameBox.getText());
+ if(%name $= "" || !isObject(%this.target))
+ {
+ return;
+ }
+
+ // A name that collides with a registered field would write the real field
+ // instead of making a dynamic one, which is not what the button says.
+ if(%this.target.getFieldType(%name) !$= "")
+ {
+ warn("GuiEditorDynamicFields: '" @ %name @ "' is a built-in field of " @
+ %this.target.getClassName() @ ", not a dynamic one.");
+ return;
+ }
+
+ if(%this.target.getFieldValue(%name) !$= "")
+ {
+ warn("GuiEditorDynamicFields: '" @ %name @ "' is already set on this control.");
+ return;
+ }
+
+ // Named, not created: an empty dynamic field does not exist, so the row is
+ // what Add produces and the first value is what makes it a field.
+ %this.pendingField = %name;
+ %this.nameBox.setText("");
+
+ %this.bind(%this.target);
+ %this.pane.onDynamicFieldsChanged();
+
+ // Put the caret in the new row's box, since typing a value is the whole of
+ // what is left to do.
+ %row = %this.row[%name];
+ if(isObject(%row))
+ {
+ %row.editor.setFirstResponder();
+ }
+}
diff --git a/editor/GuiEditor/scripts/GuiEditorExplorerTree.cs b/editor/GuiEditor/scripts/GuiEditorExplorerTree.cs
index 0b519a1fe..e7899d454 100644
--- a/editor/GuiEditor/scripts/GuiEditorExplorerTree.cs
+++ b/editor/GuiEditor/scripts/GuiEditorExplorerTree.cs
@@ -43,12 +43,77 @@
%this.postEvent("ObjectRemoved", %item);
}
+// Drag-to-reorder in the tree rearranges the real control hierarchy in C++
+// (GuiTreeViewCtrl::reorderFromDrag), and can move several selected controls
+// into several parents in one go. The pair brackets the whole rearrangement:
+// the document's shape is remembered here and read again afterwards, and the
+// difference is the undo step.
+function GuiEditorExplorerTree::onPreReorder(%this)
+{
+ GuiEditor.undoRecorder.snapshotHierarchy(GuiEditor.rootGui);
+}
+
+function GuiEditorExplorerTree::onPostReorder(%this)
+{
+ %this.rescueStranded();
+ GuiEditor.undoRecorder.commitHierarchy("Reparent Control");
+}
+
+// A drag on the canvas puts a control under the pointer, so it lands inside the
+// container it landed in. A drag here has no pointer: nothing supplies a
+// position, and the control keeps the local one it held in its old parent. Move
+// a button at x=400 into a container 100 wide and it is not clipped or half
+// hidden, it is gone - and gone from the canvas is nearly gone for good, because
+// the canvas is where you would reach for it.
+//
+// So each control that moved is offered the chance to come back into view. It is
+// per axis and only for a control that is ENTIRELY outside: see
+// GuiControl::rescuedPosition. Anything still visible, and anything that did not
+// move, is left exactly as it was.
+//
+// Before commitHierarchy, and that ordering is the whole reason this is a
+// separate call rather than something the C++ does inside the reorder. The undo
+// step is built from what layoutOf reads at commit time - position, extent and
+// both sizing fields - so rescuing first folds the correction into the same
+// "Reparent Control" action. One Ctrl+Z then puts the control back in its old
+// parent AT ITS OLD POSITION, which is what the user will expect, rather than
+// leaving it rescued in a parent that no longer wants it there.
+//
+// The selection is the set that moved: reorderFromDrag moves every selected
+// item, and it calls this back before refreshTree, so the rows are still the
+// ones that were dragged.
+function GuiEditorExplorerTree::rescueStranded(%this)
+{
+ %selected = %this.getSelectedItems();
+
+ for(%i = 0; %i < getWordCount(%selected); %i++)
+ {
+ // getSelectedItems answers "-1" for an empty selection rather than an
+ // empty string, so the index has to be looked at before it is used.
+ %index = getWord(%selected, %i);
+ if(%index < 0)
+ {
+ continue;
+ }
+
+ %ctrl = %this.getItemID(%index);
+ if(isObject(%ctrl))
+ {
+ %ctrl.pullIntoView();
+ }
+ }
+}
+
+// refreshItem rather than refreshItemText: a control's picture can change
+// without its row moving. Re-profiling a bare GuiControl from a panel to a label
+// is the same object in the same place wearing a different face, and the
+// Category picker does exactly that.
function GuiEditorExplorerTree::onPostApply(%this, %obj)
{
%index = %this.findItemID(%obj.getId());
if(%index > -1)
{
- %this.refreshItemText(%index);
+ %this.refreshItem(%index);
}
}
@@ -59,4 +124,42 @@
return "Canvas Simulation";
}
return "";
+}
+
+// Which frame of the tree's sheet a row wears. Asked once per row as the tree
+// builds itself, never per draw.
+function GuiEditorExplorerTree::onGetItemIcon(%this, %obj)
+{
+ // The simulated canvas is stage furniture, not a control in the document,
+ // and giving it a picture would say otherwise.
+ if(%obj == GuiEditor.rootGui)
+ {
+ return -1;
+ }
+ return GuiEditor.controlIcons.frameFor(%this.keyFor(%obj));
+}
+
+// A live control back to a palette entry.
+//
+// The two are not the same thing. Everything but a bare GuiControl is keyed by
+// its class, but a GuiControl is the wrapper, the backdrop, the line of text and
+// the modal scrim -- four entries sharing one class and told apart by the
+// profile category they wear. So the class alone cannot pick the picture.
+//
+// The pane already owns that question, and asking it here is what keeps the
+// Category dropdown and the row icon from disagreeing about the same control:
+// currentCategory prefers the category stamped on the profile the control is
+// actually wearing over the guess made from its shape.
+function GuiEditorExplorerTree::keyFor(%this, %ctrl)
+{
+ %class = %ctrl.getClassName();
+ if(%class $= "GuiControl")
+ {
+ return "GuiControl:" @ GuiEditor.inspectorWindow.pane.currentCategory(%ctrl);
+ }
+
+ // frameFor answers 0 -- the question mark -- for anything it has never heard
+ // of, so a class with no icon reads as "no picture for this yet" rather than
+ // as some other control.
+ return %class;
}
\ No newline at end of file
diff --git a/editor/GuiEditor/scripts/GuiEditorExplorerWindow.cs b/editor/GuiEditor/scripts/GuiEditorExplorerWindow.cs
index dd6e30d08..51e824fdf 100644
--- a/editor/GuiEditor/scripts/GuiEditorExplorerWindow.cs
+++ b/editor/GuiEditor/scripts/GuiEditorExplorerWindow.cs
@@ -3,8 +3,10 @@
{
%this.scroller = new GuiScrollCtrl()
{
- HorizSizing="width";
- VertSizing="height";
+ // Fill rather than width/height -- the window's only child wants its
+ // whole content rect. See GuiEditorControlListWindow for why.
+ HorizSizing="fill";
+ VertSizing="fill";
Position="0 0";
Extent="392 355";
hScrollBar="alwaysOff";
@@ -19,18 +21,65 @@
ThemeManager.setProfile(%this.scroller, "scrollArrowProfile", "ArrowProfile");
%this.add(%this.scroller);
- %this.tree = new GuiTreeViewCtrl()
+ // A real class rather than a script class on a plain tree. The two columns of
+ // editor state have to draw in row with each item and be hit tested by the
+ // pixel, and a list box refuses children, so there is nothing script could
+ // have hung them on.
+ //
+ // Note there is no class= here any more, and there must not be: the C++ class
+ // owns the namespace now, so GuiEditorExplorerTree.cs writes into it directly.
+ // Setting class= to the same name makes Namespace::classLinkTo log "cannot
+ // change namespace parent linkage" every time the editor opens.
+ %this.tree = new GuiEditorExplorerTree()
{
- class="GuiEditorExplorerTree";
HorizSizing="width";
VertSizing="height";
Position="0 0";
Extent="228 355";
BindToGuiEditor="1";
+ AllowReorder="1";
+
+ // A narrower step than the default, which is one row height. This tree
+ // spends more of its width on chrome than any other -- two columns, a
+ // triangle and an icon before a single letter of the name -- and at three
+ // levels deep the default step left almost nothing for the text. Twelve
+ // is still a clear step and buys back ten pixels a level.
+ IndentSize = 12;
+
+ // The eye and the padlock. Frames come from EditorIcons.cs, never from
+ // the engine: that file is generated and alphabetical, so a number baked
+ // into C++ would silently become a different picture on the next rebuild.
+ StateIcons = "EditorCore:editorIcons16";
+ EyeFrame = $EditorIcon::eye;
+ LockFrame = $EditorIcon::padlock_closed;
+
+ // And a miniature of each control's own class, between the triangle and
+ // the text. Which frame is onGetItemIcon's answer, asked once per row as
+ // the tree builds -- the sheet is picked by size, same as everywhere else.
+ IconImage = GuiEditor.controlIcons.sheetFor(16);
+ IconSize = 16;
+
+ // Named for the state each describes, because the eye reads inverted --
+ // it is present when the control is NOT hidden. The bodies are the ones
+ // the properties pane used to carry; the headings are new, and say
+ // "Shown in the editor" rather than "Visible" because Visible is a real
+ // runtime flag that still lives in the pane and means something else.
+ ShownTip = "Shown in the editor - On" NL
+ "Drawn on the canvas as it will be in the game.";
+ HiddenTip = "Shown in the editor - Off" NL
+ "Hidden while you work. The control still draws when the game runs -- this only takes it out of the way on the canvas, so you can reach what is behind it. It is not saved with the Gui.";
+ LockedTip = "Locked - On" NL
+ "Cannot be picked or dragged on the canvas. Use this to stop a backdrop swallowing every click meant for what sits on top of it. It is not saved with the Gui.";
+ UnlockedTip = "Locked - Off" NL
+ "Can be picked and dragged on the canvas.";
};
ThemeManager.setProfile(%this.tree, "treeViewProfile");
+ ThemeManager.setProfile(%this.tree, "tipProfile", "TooltipProfile");
%this.scroller.add(%this.tree);
- %this.tree.startListening(GuiEditor.inspectorWindow.inspector);
+ // The window itself, not a control inside it: the properties pane posts
+ // PostApply through its window so the tree can pick up a name that just
+ // changed. (It used to listen to the native GuiInspector, which is gone.)
+ %this.tree.startListening(GuiEditor.inspectorWindow);
}
function GuiEditorExplorerWindow::inspect(%this, %object)
diff --git a/editor/GuiEditor/scripts/GuiEditorHeaderBlock.cs b/editor/GuiEditor/scripts/GuiEditorHeaderBlock.cs
new file mode 100644
index 000000000..244ebee15
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiEditorHeaderBlock.cs
@@ -0,0 +1,458 @@
+
+//-----------------------------------------------------------------------------
+// The always-visible top of the Gui Editor's properties pane: what you need
+// about the selected control without opening anything.
+//
+// There are not twenty-nine of these. The header is one shell with three
+// swappable blocks, and a class only chooses which shape each block takes:
+//
+// identity name, profile and the state toggles. The same for everything
+// except a menu item, which has no GuiControl fields at all.
+// geometry position, extent and the two sizing enums -- but only the parts
+// the control's PARENT has left it. A grid, frame set or tab book
+// owns all of it; a chain owns one axis. See
+// GuiEditorControlSpec::geometryModeOf.
+// text the control's own string, when it has one that is drawn. Named
+// for what it actually is: a window's title, a tab's caption, a
+// drop-down's placeholder.
+// value nothing, or the one or two fields that are the whole point of
+// the control -- a slider's range, a sprite's image.
+//
+// The block owns its widgets and no values: every row reports to the pane, so
+// the pane stays the only thing that writes to the control. The creator sets
+// pane, spec and blockWidth inline, then calls build() once after adding it.
+//
+// The toggle row is four flags that change the Gui a player will run: whether a
+// control draws, whether it responds, whether events reach it, and whether the
+// editor may drop things into it.
+//
+// It used to be six. hidden and locked sat at the front, and they are not that
+// kind of flag at all -- neither is ever written to a file, because both are
+// working state rather than part of the document. Standing them next to the four
+// that ARE the document read as a promise that they were too. They are the
+// Explorer tree's two columns now, where a whole branch's state can be read at a
+// glance and nothing suggests it will be saved.
+//-----------------------------------------------------------------------------
+
+function GuiEditorHeaderBlock::onAdd(%this)
+{
+ ThemeManager.setProfile(%this, "emptyProfile");
+}
+
+function GuiEditorHeaderBlock::build(%this)
+{
+ %this.rowFields = "";
+ %this.valueFields = "";
+
+ %this.buildIdentity();
+ %this.buildToggles();
+ %this.buildGeometry();
+ %this.buildText();
+ %this.buildMenuItem();
+
+ // The value grid starts empty; bindClass fills it, because what belongs
+ // there is the one thing here that changes with the class. Pair-width cells:
+ // a principal value is usually one or two short fields, and a window's title
+ // height wants its six switches beside it rather than under them.
+ %this.valueGrid = %this.pane.makeCellGrid(0, %this.pane.pairWidth);
+ %this.add(%this.valueGrid);
+}
+
+//-----------------------------------------------------------------------------
+// Identity.
+//-----------------------------------------------------------------------------
+
+function GuiEditorHeaderBlock::buildIdentity(%this)
+{
+ %grid = %this.pane.makeCellGrid(0);
+ %this.add(%grid);
+ %this.identityGrid = %grid;
+
+ %this.nameRow = %this.pane.addFieldRow(%grid, "name", "Name", "text", "");
+
+ // Category sits between the name and the profile because that is the order
+ // the two are decided in: what the control is, and then which of the theme's
+ // profiles for that answers it. It is not a field on the control -- the
+ // profile it picks is the record of the choice -- so it is built without a
+ // name in the registry, and the pane intercepts its commits.
+ %this.categoryRow = %this.pane.makeFieldRow(%grid, "category", "Category", "dropdown", "");
+
+ %this.profileRow = %this.pane.addFieldRow(%grid, "Profile", "Profile", "dropdown", "");
+}
+
+//-----------------------------------------------------------------------------
+// State toggles.
+//-----------------------------------------------------------------------------
+
+function GuiEditorHeaderBlock::buildToggles(%this)
+{
+ %w = %this.blockWidth;
+
+ %row = new GuiControl()
+ {
+ HorizSizing = "width";
+ Position = "0 0";
+ Extent = %w SPC 28;
+ };
+ ThemeManager.setProfile(%row, "emptyProfile");
+ %this.add(%row);
+ %this.toggleRow = %row;
+
+ // The four runtime flags, now that the sheet has art for them. They were
+ // captioned checkboxes, which cost so much width that only two fitted and
+ // useInput had to live in the Command section instead. Four icons fit in
+ // less than two captions did.
+ //
+ // Two of the four have a genuine pair (an eye against a struck-through one,
+ // on against off) and two do not, so those reuse one icon and let the
+ // pressed state carry the reading -- the same thing the anchor pins do.
+ //
+ // They start at 4 now. There used to be a gap here, between these and the two
+ // editor-only toggles that ran in front of them; the gap was the boundary
+ // between "changes the game" and "changes your view of it", and with hidden
+ // and locked gone to the Explorer tree there is no boundary left to mark.
+ %this.visibleButton = %this.makeIconToggle(%row, 4, "Visible", "Visible",
+ $EditorIcon::eye, $EditorIcon::invisible_light,
+ "Draws when the game runs. Its children draw with it -- hiding a control hides everything inside it.",
+ "Does not draw when the game runs, and neither do its children. A layout container skips a hidden control entirely, so hiding one can move its siblings.");
+ %this.activeButton = %this.makeIconToggle(%row, 32, "Active", "Active",
+ $EditorIcon::on, $EditorIcon::off,
+ "Responds when the game runs: it takes clicks and keys and draws in its ordinary colors.",
+ "Inert when the game runs. It still draws, in the profile's disabled colors, but ignores every click and key.");
+ %this.inputButton = %this.makeIconToggle(%row, 60, "useInput", "Accepts Input",
+ $EditorIcon::cursor_arrow, $EditorIcon::cursor_arrow,
+ "Touch and key events reach this control.",
+ "Touch and key events pass straight through to whatever is behind it. Turn this off on a backdrop or a label so it cannot swallow clicks meant for something else.");
+ %this.containerButton = %this.makeIconToggle(%row, 88, "isContainer", "Accepts Children",
+ $EditorIcon::folder_open, $EditorIcon::folder,
+ "The editor drops controls into this one when you draw them over it.",
+ "The editor never drops controls into this one. They land in its parent instead, on top of it.");
+}
+
+// A checkbox wearing an icon, which is what a toggle button is here. It holds
+// its own state and refuses to change it while inactive; the pane is told what
+// the value became.
+function GuiEditorHeaderBlock::makeIconToggle(%this, %row, %x, %field, %label, %frameOn, %frameOff, %tipOn, %tipOff)
+{
+ %button = new GuiCheckBoxCtrl()
+ {
+ class = "EditorToggleIcon";
+ Position = %x SPC 2;
+ Extent = "24 24";
+ frameOn = %frameOn;
+ frameOff = %frameOff;
+ tipOn = %tipOn;
+ tipOff = %tipOff;
+ toggleName = %field;
+ toggleLabel = %label;
+ owner = %this;
+ };
+ ThemeManager.setProfile(%button, "iconButtonProfile");
+ ThemeManager.setProfile(%button, "tipProfile", "TooltipProfile");
+ %row.add(%button);
+ return %button;
+}
+
+// A toggle icon changed. It has already flipped itself, so the pane is handed
+// the new value rather than being asked to work it out.
+function GuiEditorHeaderBlock::onToggleIconChanged(%this, %toggle)
+{
+ %this.pane.onToggleChanged(%toggle.toggleName, %toggle.getValue());
+}
+
+//-----------------------------------------------------------------------------
+// A window's six switches. They were a section of six captioned checkboxes;
+// as icons they are one cell, which fits beside Title Height in the value
+// block -- the same trade the state toggles made in the row above.
+//-----------------------------------------------------------------------------
+
+function GuiEditorHeaderBlock::buildWindowToggles(%this, %grid)
+{
+ %size = 24;
+ %gap = 2;
+ %fields = %this.spec.windowToggles();
+
+ %row = new GuiControl()
+ {
+ Position = "0 0";
+ Extent = (getWordCount(%fields) * (%size + %gap)) SPC (%size + 4);
+ };
+ ThemeManager.setProfile(%row, "emptyProfile");
+ %grid.add(%row);
+
+ // field TAB label TAB icon TAB on TAB off
+ %table =
+ "canMove" TAB "Move" TAB $EditorIcon::cursor_drag_arrow TAB
+ "The player can drag the window by its title bar." TAB
+ "The window stays where it is put. Use this for a dialog that should not be moved off what it is explaining." NL
+ "canClose" TAB "Close" TAB $EditorIcon::app_window_cross TAB
+ "The title bar carries a close button." TAB
+ "No close button. The game has to take the window down itself, which is what a modal dialog with its own buttons wants." NL
+ "canMinimize" TAB "Minimize" TAB $EditorIcon::round_minus TAB
+ "The title bar carries a minimise button, which rolls the window up to its title." TAB
+ "No minimise button." NL
+ "canMaximize" TAB "Maximize" TAB $EditorIcon::expand TAB
+ "The title bar carries a maximise button, which fills the window's parent." TAB
+ "No maximise button." NL
+ "resizeWidth" TAB "Resize Width" TAB $EditorIcon::arrow_two_head TAB
+ "The player can drag the window's left and right edges." TAB
+ "The width is fixed at whatever the Gui was saved with." NL
+ "resizeHeight" TAB "Resize Height" TAB $EditorIcon::arrow_two_head_2 TAB
+ "The player can drag the window's top and bottom edges." TAB
+ "The height is fixed at whatever the Gui was saved with.";
+
+ %count = getRecordCount(%table);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %rec = getRecord(%table, %i);
+ %field = getField(%rec, 0);
+ %this.windowButton[%field] = %this.makeIconToggle(%row, %i * (%size + %gap),
+ %field, getField(%rec, 1), getField(%rec, 2), getField(%rec, 2),
+ getField(%rec, 3), getField(%rec, 4));
+ }
+
+ %this.windowToggleRow = %row;
+}
+
+// Load the six from the control, when the bound class has them.
+function GuiEditorHeaderBlock::refreshWindowToggles(%this, %ctrl)
+{
+ if(!isObject(%this.windowToggleRow))
+ {
+ return;
+ }
+
+ %fields = %this.spec.windowToggles();
+ for(%i = 0; %i < getWordCount(%fields); %i++)
+ {
+ %field = getWord(%fields, %i);
+ %this.windowButton[%field].setValue(%ctrl.getFieldValue(%field));
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Geometry. Which of these the control may edit is its parent's answer, so the
+// pane recomputes it on selection AND on reparent, and passes the mode here.
+//-----------------------------------------------------------------------------
+
+function GuiEditorHeaderBlock::buildGeometry(%this)
+{
+ %grid = %this.pane.makeCellGrid(0);
+ %this.add(%grid);
+ %this.geometryGrid = %grid;
+
+ %this.positionRow = %this.pane.addFieldRow(%grid, "Position", "Position", "point", "");
+ %this.extentRow = %this.pane.addFieldRow(%grid, "Extent", "Extent", "point", "");
+
+ // The two sizing enums share one widget, because they are one decision and
+ // their names read backwards -- see GuiEditorAnchorPicker. It goes in the
+ // same grid as a cell of its own so it reflows with everything else, and it
+ // carries no authored width: the grid resizes every cell to the column it
+ // computed, so a width set here would only be overwritten.
+ %this.anchorPicker = new GuiControl()
+ {
+ class = "GuiEditorAnchorPicker";
+ HorizSizing = "width";
+ Position = "0 0";
+ Extent = %this.pane.rowWidth SPC 124;
+ owner = %this;
+ };
+ %grid.add(%this.anchorPicker);
+ %this.anchorPicker.build();
+}
+
+// The picker changed. Both enums are written, because a single click can move
+// either of them and writing only the one that visibly changed would leave the
+// other stale after a Fill is cleared.
+function GuiEditorHeaderBlock::onAnchorChanged(%this, %picker)
+{
+ %this.pane.onSizingChanged(%picker.horizEnum(), %picker.vertEnum());
+}
+
+// Grey rather than hide, and say why. A control sitting in a grid still HAS a
+// position; it is just not the control's to set, and blanking the row would
+// leave no way to see where the grid put it.
+function GuiEditorHeaderBlock::applyGeometryMode(%this, %mode)
+{
+ %spec = %this.spec;
+ %reason = "The parent container sets this.";
+
+ // A field the control's own class rewrites every frame is not greyed but
+ // gone: there is no value to look at and nothing a tooltip could usefully
+ // say about it. A menu bar is always at the top of what it is in.
+ %this.positionRow.setVisible(%spec.isGeometryFieldShown(%mode, "Position"));
+ %this.extentRow.setVisible(%spec.isGeometryFieldShown(%mode, "Extent"));
+ %this.anchorPicker.setVisible(%spec.isGeometryFieldShown(%mode, "HorizSizing"));
+
+ %this.anchorPicker.setAxisEnabled(
+ %spec.isGeometryFieldLive(%mode, "HorizSizing"),
+ %spec.isGeometryFieldLive(%mode, "VertSizing"));
+
+ // Position and Extent are the two fields whose axes can disagree. A vertical
+ // chain stacks its children on Y and copies X straight back from the child;
+ // a menu bar's width is its parent's and its height is its own. The rows'
+ // own setEnabled works on both boxes at once, so the axes are set directly --
+ // the same two widgets it would touch.
+ %this.applyAxes(%this.positionRow, %spec.livePositionAxes(%mode), %reason);
+ %this.applyAxes(%this.extentRow, %spec.liveExtentAxes(%mode), %reason);
+}
+
+function GuiEditorHeaderBlock::applyAxes(%this, %row, %axes, %reason)
+{
+ %liveX = strstr(%axes, "x") >= 0;
+ %liveY = strstr(%axes, "y") >= 0;
+
+ %row.editor.setActive(%liveX);
+ %row.editorY.setActive(%liveY);
+ %row.editor.Tooltip = %liveX ? "" : %reason;
+ %row.editorY.Tooltip = %liveY ? "" : %reason;
+}
+
+//-----------------------------------------------------------------------------
+// Text.
+//-----------------------------------------------------------------------------
+
+// The whole text story is one component now -- the string, the two flags that
+// change what it does to the control, both alignments, and the size and color
+// it is drawn in. The header holds one copy of it and the pane's Text section
+// holds the other; see GuiEditorTextBlock.
+//
+// textGrid, textRow, alignRow and vAlignRow stay as names for what the block
+// owns, because they are how the pane and the smoke tests reach these widgets.
+function GuiEditorHeaderBlock::buildText(%this)
+{
+ %this.textBlock = %this.pane.makeTextBlock(%this, 0);
+
+ %this.textGrid = %this.textBlock;
+ %this.textRow = %this.textBlock.textRow;
+ %this.alignRow = %this.textBlock.alignRow;
+ %this.vAlignRow = %this.textBlock.vAlignRow;
+}
+
+//-----------------------------------------------------------------------------
+// Binding to a class. Everything above exists for every control; this decides
+// which of it applies and what the value block holds.
+//-----------------------------------------------------------------------------
+
+function GuiEditorHeaderBlock::bindClass(%this, %ctrl, %class)
+{
+ %spec = %this.spec;
+ %bare = %spec.hasFlag(%class, "bare");
+
+ // A menu item has no GuiControl fields at all -- it calls
+ // SimObject::initPersistFields rather than GuiControl's -- so it keeps its
+ // name and its caption and loses everything else here.
+ %this.profileRow.setVisible(!%bare);
+
+ // Only one class in the palette is ambiguous enough to need this, and a
+ // class with no profile at all cannot use it either.
+ %this.categoryRow.setVisible(!%bare && %spec.categoryChoices(%class) !$= "");
+ %this.geometryGrid.setVisible(!%bare);
+
+ // Not simply !bare: a bare class has none of GuiControl's fields except the
+ // ones it turns round and registers again, and a menu item does that with
+ // Visible and Active. useInput it genuinely does not have.
+ %states = %spec.stateToggles(%class);
+ %this.visibleButton.setVisible(%spec.listHas(%states, "Visible"));
+ %this.activeButton.setVisible(%spec.listHas(%states, "Active"));
+ %this.inputButton.setVisible(%spec.listHas(%states, "useInput"));
+
+ // isContainer is dead where the control cannot draw children -- GuiControl's
+ // setIsContainerFn forces the field false for those -- so the button goes
+ // rather than sitting there wired to nothing.
+ %this.containerButton.setVisible(%spec.isContainerFieldVisible(%ctrl));
+
+ // The text block is only in the header for the classes whose text is a
+ // principal property of them. A grid can technically draw text; it goes in a
+ // collapsed section instead of the first thing anyone sees. The pane places
+ // it and binds it -- it owns both copies.
+ %this.textBlock.setVisible(%spec.textBlockHome(%class) $= "header");
+
+ // A menu item's own fields, which are the only ones it has. It brings its own
+ // caption box, so the shared text block stands down for it.
+ %menuItem = (%class $= "GuiMenuItemCtrl");
+ %this.menuItemBlock.setVisible(%menuItem);
+ if(%menuItem)
+ {
+ %this.textBlock.setVisible(false);
+ %this.menuItemBlock.bind(%ctrl);
+ }
+
+ %this.buildValueRows(%ctrl, %class);
+ %this.resizeToFit();
+}
+
+//-----------------------------------------------------------------------------
+// The menu item block. Its own file, because a menu item shares almost nothing
+// with anything else in the palette - see GuiEditorMenuItemBlock.
+//-----------------------------------------------------------------------------
+
+function GuiEditorHeaderBlock::buildMenuItem(%this)
+{
+ %this.menuItemBlock = new GuiChainCtrl()
+ {
+ class = "GuiEditorMenuItemBlock";
+ HorizSizing = "width";
+ Position = "0 0";
+ Extent = %this.blockWidth SPC 24;
+ IsVertical = true;
+ pane = %this.pane;
+ spec = %this.spec;
+ blockWidth = %this.blockWidth;
+ Visible = false;
+ };
+ %this.add(%this.menuItemBlock);
+ %this.menuItemBlock.build();
+}
+
+// The principal value: whatever the control is for. Rebuilt rather than
+// filtered, because these fields are the one part of the header that does not
+// exist on every class -- there is no shared row to hide.
+function GuiEditorHeaderBlock::buildValueRows(%this, %ctrl, %class)
+{
+ %this.pane.clearRows(%this.valueFields);
+ %this.valueGrid.deleteObjects();
+ %this.valueFields = "";
+ %this.windowToggleRow = "";
+
+ %fields = %this.spec.headerValueFields(%class);
+
+ // A sprite names its picture three mutually exclusive ways, so the header
+ // shows the one it is actually using rather than all three.
+ if(%class $= "GuiSpriteCtrl")
+ {
+ %fields = %this.spec.spriteSourceFields(%this.spec.spriteSourceModeOf(%ctrl));
+ }
+
+ %count = getWordCount(%fields);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %field = getWord(%fields, %i);
+ %this.pane.addFieldRow(%this.valueGrid, %field,
+ %this.spec.labelFor(%field), %this.pane.kindFor(%ctrl, %field),
+ %this.pane.enumItemsFor(%ctrl, %field));
+ %this.valueFields = (%this.valueFields $= "") ? %field : (%this.valueFields SPC %field);
+ }
+
+ // A window's switches go in the same grid as its Title Height, so the two
+ // share a line rather than costing a section of their own.
+ if(%class $= "GuiWindowCtrl")
+ {
+ %this.buildWindowToggles(%this.valueGrid);
+ %count++;
+ }
+
+ %this.valueGrid.setVisible(%count > 0);
+}
+
+// A GuiChainCtrl positions its children without resizing them, and a grid only
+// learns its height once something lays it out, so nudge the width by a pixel
+// and back to force exactly one parentResized through every child. Same trick
+// GuiProfileEditorProfileForm::build uses, and needed here for the same reason.
+function GuiEditorHeaderBlock::resizeToFit(%this)
+{
+ %w = getWord(%this.getExtent(), 0);
+ %h = getWord(%this.getExtent(), 1);
+ %this.resize(0, 0, %w + 1, %h);
+ %this.resize(0, 0, %w, %h);
+}
diff --git a/editor/GuiEditor/scripts/GuiEditorInspector.cs b/editor/GuiEditor/scripts/GuiEditorInspector.cs
deleted file mode 100644
index 63e024ba0..000000000
--- a/editor/GuiEditor/scripts/GuiEditorInspector.cs
+++ /dev/null
@@ -1,4 +0,0 @@
-function GuiEditorInspector::onPostApply(%this, %obj)
-{
- %this.postEvent("PostApply", %obj);
-}
\ No newline at end of file
diff --git a/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs b/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs
new file mode 100644
index 000000000..d8e1ee478
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs
@@ -0,0 +1,1766 @@
+
+//-----------------------------------------------------------------------------
+// The Gui Editor's properties pane, in place of the generic C++ GuiInspector.
+//
+// The inspector reflected every registered field into flat alphabetical groups,
+// which meant it offered a GuiChainCtrl nine text fields it never draws and a
+// GuiTabPageCtrl four geometry fields its book overwrites on every layout pass.
+// This asks GuiEditorControlSpec what the selected class actually reads and
+// shows that, with the fields that matter most in a header that is always open.
+//
+// Layout is a vertical chain of blocks, the same arrangement
+// GuiProfileEditorProfileForm uses and for the same reason: each block lays its
+// fields out in a GuiGridCtrl, so widening the Properties frame reflows the
+// cells into more columns instead of leaving dead space.
+//
+// Two kinds of block, and the difference matters:
+//
+// shared Every control has these fields, so the rows are built once and
+// filtered with setVisible. Nothing is ever freed, so a selection
+// change can never delete a control the engine is mid-dispatch on.
+// class The class's own sections and the header's value block. These
+// fields do not exist on other classes, so there is no shared row
+// to hide and they are rebuilt when the selected class changes.
+//
+// Rebuilding is safe here in a way it was not in the Profile Editor's preview,
+// because nothing inside this pane can change the target's class: rebuilds only
+// ever arrive from a selection change, which originates on the canvas or in the
+// explorer tree, never on a widget this pane owns. The one exception -- a
+// sprite's source mode, which a commit CAN change -- is deferred to schedule(0)
+// rather than run inside the commit.
+//
+// The pane owns every write to the control; its rows only marshal values. The
+// creator sets paneWidth and window inline, then calls build() once after
+// adding the pane to its scroller.
+//-----------------------------------------------------------------------------
+
+function GuiEditorInspectorPane::onAdd(%this)
+{
+ ThemeManager.setProfile(%this, "emptyProfile");
+
+ // The font color swatch gets the popup that offers the theme's six colors.
+ // EditorFieldRow lives in EditorCore and cannot name a Gui Editor class, so
+ // the pane that wants one says so -- see its header.
+ %this.swatchClass = "GuiProfileEditorColorPopup";
+
+ %this.spec = new ScriptObject()
+ {
+ class = "GuiEditorControlSpec";
+ };
+}
+
+function GuiEditorInspectorPane::onRemove(%this)
+{
+ %this.unbind();
+
+ if(isObject(%this.spec))
+ {
+ %this.spec.delete();
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Construction.
+//-----------------------------------------------------------------------------
+
+function GuiEditorInspectorPane::build(%this)
+{
+ %w = %this.paneWidth;
+
+ // The narrowest a cell column may get. The grids run in Variable mode, so
+ // they fit as many columns as the pane can hold at this width and share the
+ // remainder evenly -- which is what makes dragging the frame wider add
+ // columns rather than whitespace.
+ %this.rowWidth = 220;
+
+ // Half of that, for the sections whose fields are read in pairs: an easing
+ // and the time it takes, a tooltip's width and its delay, a window's title
+ // height and its six switches.
+ %this.pairWidth = 152;
+
+ %this.rowFields = "";
+ %this.panelList = "";
+ %this.classPanels = "";
+ %this.boundClass = "";
+
+ %this.header = new GuiChainCtrl()
+ {
+ class = "GuiEditorHeaderBlock";
+ HorizSizing = "width";
+ Position = "0 0";
+ Extent = %w SPC 40;
+ IsVertical = true;
+ ChildSpacing = 2;
+ blockWidth = %w;
+ pane = %this;
+ spec = %this.spec;
+ };
+ %this.add(%this.header);
+ %this.header.build();
+
+ // The class's own sections live in a chain of their own so that rebuilding
+ // them cannot change where they sit: added straight to the outer chain they
+ // would land after the shared sections every time they were replaced.
+ %this.classChain = new GuiChainCtrl()
+ {
+ HorizSizing = "width";
+ Position = "0 0";
+ Extent = %w SPC 4;
+ IsVertical = true;
+ ChildSpacing = 6;
+ };
+ ThemeManager.setProfile(%this.classChain, "emptyProfile");
+ %this.add(%this.classChain);
+
+ // Variants get a chain of their own because they are rebuilt more often
+ // than the class sections: whether a slot is worth showing depends on what
+ // the theme holds right now, which the Profile Editor can change while the
+ // same control stays selected.
+ %this.variantsChain = new GuiChainCtrl()
+ {
+ HorizSizing = "width";
+ Position = "0 0";
+ Extent = %w SPC 4;
+ IsVertical = true;
+ ChildSpacing = 6;
+ };
+ ThemeManager.setProfile(%this.variantsChain, "emptyProfile");
+ %this.add(%this.variantsChain);
+
+ // The shared sections, in the order the work usually goes.
+ %this.buildTextSection();
+ %this.buildItemsSection();
+
+ // isContainer and useInput are both in the header's icon row now that there
+ // is art for them, so neither has a row here.
+ %this.buildSection("Layout", "Layout", "MinExtent");
+ %this.buildSection("Command", "Command", "Command AltCommand Variable Accelerator");
+
+ // Each easing beside the time it takes: hover on one line, press on the next.
+ %this.buildSection("Animation", "Animation", %this.spec.easingFields(), %this.pairWidth);
+
+ %this.buildTooltipSection();
+ %this.buildSection("Localization", "Localization", "langTableMod textID");
+ %this.buildSection("Scripting", "Scripting", "class superclass internalName");
+
+ // Dynamic fields last, and in a section of their own rather than through
+ // buildSection: what it holds is not a fixed field list, so it owns its own
+ // rows and its own commits.
+ %this.dynamicPanel = %this.makeSectionPanel("Dynamic Fields");
+ %this.add(%this.dynamicPanel);
+
+ %this.dynamicFields = new GuiChainCtrl()
+ {
+ class = "GuiEditorDynamicFields";
+ HorizSizing = "width";
+ Position = "0 24";
+ Extent = %w SPC 4;
+ IsVertical = true;
+ ChildSpacing = 4;
+ blockWidth = %w;
+ pane = %this;
+ };
+ %this.dynamicPanel.add(%this.dynamicFields);
+ %this.dynamicFields.build();
+
+ %this.forceLayout();
+}
+
+// A GuiPanelCtrl learns its collapsed height only from parentResized -- its
+// constructor defaults to 64x64 whatever Extent it was given, and a chain
+// positions its children without ever resizing them. Nudging the width by a
+// pixel and back forces exactly one parentResized through every child and
+// leaves the widths where they started.
+function GuiEditorInspectorPane::forceLayout(%this)
+{
+ %w = %this.paneWidth;
+ %h = getWord(%this.getExtent(), 1);
+ %this.resize(0, 0, %w + 1, %h);
+ %this.resize(0, 0, %w, %h);
+}
+
+// The grid configuration every block here uses, matching what the native
+// inspector gave its group grids. A hidden cell is skipped rather than left as
+// a hole, so filtering closes the gap (GuiGridCtrl::resize).
+// %cellW is the NARROWEST a column may be, not its width: the grid fits as many
+// columns as the pane can hold at that size and shares the remainder evenly. So
+// a section that wants its fields in pairs asks for half a pane rather than
+// arranging them itself -- and still reflows to one column when the frame is
+// dragged narrow. Omit it for the ordinary one-field-per-row width.
+function GuiEditorInspectorPane::makeCellGrid(%this, %y, %cellW)
+{
+ %grid = new GuiGridCtrl()
+ {
+ HorizSizing = "width";
+ Position = "0" SPC %y;
+ Extent = %this.paneWidth SPC 4;
+ CellModeX = "variable";
+ CellModeY = "variable";
+ CellSizeX = (%cellW $= "") ? %this.rowWidth : %cellW;
+ CellSizeY = 48;
+ CellSpacingX = 4;
+ CellSpacingY = 4;
+ MaxColCount = 0;
+ MaxRowCount = 0;
+ OrderMode = "lrtb";
+ IsExtentDynamic = true;
+ };
+ ThemeManager.setProfile(%grid, "emptyProfile");
+ return %grid;
+}
+
+// A collapsible section. Its cells sit in an inner grid rather than directly on
+// the panel: GuiExpandCtrl::toggleHiddenChildren force-writes mVisible on every
+// direct child whenever it expands, collapses or resizes, which would undo the
+// filter. Grandchildren are left alone and the grid skips the hidden ones.
+function GuiEditorInspectorPane::makeSectionPanel(%this, %title)
+{
+ %headerH = 24;
+
+ %panel = new GuiPanelCtrl()
+ {
+ HorizSizing = "width";
+ Text = %title;
+ Position = "0 0";
+ Extent = %this.paneWidth SPC %headerH;
+ MinExtent = "80" SPC %headerH;
+ };
+ ThemeManager.setProfile(%panel, "panelProfile");
+ return %panel;
+}
+
+// The text block's second home. Not a buildSection: it holds one component
+// rather than a list of rows, so its visibility is decided by which home the
+// class wants (GuiEditorControlSpec::textBlockHome) and not by counting visible
+// rows. It stays out of panelList for that reason.
+//
+// The three field rows inside a block are named here instead, with no row
+// behind them yet: applyFilter points them at whichever block is in use, and
+// then the shared filter and the shared value loop reach them like any other
+// field. That is what stops two blocks fighting over one entry in the registry.
+function GuiEditorInspectorPane::buildTextSection(%this)
+{
+ %this.textPanel = %this.makeSectionPanel("Text");
+ %this.add(%this.textPanel);
+
+ %this.sectionText = %this.makeTextBlock(%this.textPanel, 24);
+
+ %fields = %this.spec.textBlockRowFields();
+ %count = getWordCount(%fields);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %field = getWord(%fields, %i);
+ %this.row[%field] = "";
+ %this.rowFields = (%this.rowFields $= "") ? %field : (%this.rowFields SPC %field);
+ }
+}
+
+// The tooltip is a paragraph rather than a caption -- it is where an editor
+// answers a question instead of sending someone looking -- so it gets the same
+// three-line box the text block uses, at the full width of the pane. Its width
+// and its delay are two small numbers that read as a pair, so they share the
+// line beneath it.
+function GuiEditorInspectorPane::buildTooltipSection(%this)
+{
+ %panel = %this.makeSectionPanel("Tooltip");
+ %this.add(%panel);
+
+ %chain = new GuiChainCtrl()
+ {
+ HorizSizing = "width";
+ Position = "0 24";
+ Extent = %this.paneWidth SPC 4;
+ IsVertical = true;
+ ChildSpacing = 2;
+ };
+ ThemeManager.setProfile(%chain, "emptyProfile");
+ %panel.add(%chain);
+
+ %this.panel["Tooltip"] = %panel;
+ %this.panelFields["Tooltip"] = %this.spec.tooltipFields();
+ %this.panelList = (%this.panelList $= "") ? "Tooltip" : (%this.panelList SPC "Tooltip");
+
+ %this.addFieldRow(%chain, "tooltip", %this.spec.labelFor("tooltip"), "multiline", "");
+
+ %grid = %this.makeCellGrid(0, %this.pairWidth);
+ %chain.add(%grid);
+ %this.addFieldRow(%grid, "tooltipWidth", %this.spec.labelFor("tooltipWidth"), "number", "");
+ %this.addFieldRow(%grid, "hovertime", %this.spec.labelFor("hovertime"), "number", "");
+}
+
+// The static rows of a list box or a drop down, directly under the text section
+// because they are what the control says: a list's rows are its caption. Built
+// once and shown per class rather than rebuilt with the class sections, for the
+// reason the header comment gives -- and it stays out of panelList and out of
+// the row registry, because what it holds is not a field.
+function GuiEditorInspectorPane::buildItemsSection(%this)
+{
+ %this.itemsPanel = %this.makeSectionPanel("Items");
+ %this.add(%this.itemsPanel);
+
+ %this.itemsBlock = new GuiChainCtrl()
+ {
+ class = "GuiEditorItemsBlock";
+ HorizSizing = "width";
+ Position = "0 24";
+ Extent = %this.paneWidth SPC 4;
+ IsVertical = true;
+ ChildSpacing = 4;
+ blockWidth = %this.paneWidth;
+ pane = %this;
+ };
+ %this.itemsPanel.add(%this.itemsBlock);
+ %this.itemsBlock.build();
+}
+
+// A row was added or removed, so the section is a different height. Same shape
+// as onDynamicFieldsChanged, and for the same reason.
+function GuiEditorInspectorPane::onItemsChanged(%this)
+{
+ %this.itemsBlock.resize(0, 24, %this.paneWidth, getWord(%this.itemsBlock.getExtent(), 1));
+ %this.forceLayout();
+}
+
+function GuiEditorInspectorPane::makeTextBlock(%this, %parent, %y)
+{
+ %block = new GuiChainCtrl()
+ {
+ class = "GuiEditorTextBlock";
+ HorizSizing = "width";
+ Position = "0" SPC %y;
+ Extent = %this.paneWidth SPC 4;
+ IsVertical = true;
+ ChildSpacing = 2;
+ blockWidth = %this.paneWidth;
+ pane = %this;
+ spec = %this.spec;
+ };
+ %parent.add(%block);
+ %block.build();
+ return %block;
+}
+
+function GuiEditorInspectorPane::buildSection(%this, %key, %title, %fields, %cellW)
+{
+ %panel = %this.makeSectionPanel(%title);
+ %this.add(%panel);
+
+ %grid = %this.makeCellGrid(24, %cellW);
+ %panel.add(%grid);
+
+ %this.panel[%key] = %panel;
+ %this.panelFields[%key] = %fields;
+ %this.panelList = (%this.panelList $= "") ? %key : (%this.panelList SPC %key);
+
+ %count = getWordCount(%fields);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %field = getWord(%fields, %i);
+ %this.addFieldRow(%grid, %field, %this.spec.labelFor(%field),
+ %this.sharedKindFor(%field), %this.sharedEnumItemsFor(%field));
+ }
+}
+
+// Build a row without claiming a name for it. Two rows here are not fields of
+// the control -- the Category picker, and the text block's copy in whichever of
+// its two homes is not in use -- so they must stay out of the registry the
+// filter and the value loop walk.
+function GuiEditorInspectorPane::makeFieldRow(%this, %container, %field, %label, %kind, %enumItems)
+{
+ %row = new GuiControl()
+ {
+ class = "EditorFieldRow";
+
+ // A grid resizes every cell it lays out, which makes the flag moot there;
+ // a chain does not, so a row in one follows the pane's width from here.
+ HorizSizing = "width";
+ Position = "0 0";
+ Extent = getWord(%container.getExtent(), 0) SPC 48;
+ fieldName = %field;
+ labelText = %label;
+ kind = %kind;
+ enumItems = %enumItems;
+ owner = %this;
+ };
+ %container.add(%row);
+ %row.build();
+
+ // This pane has no reset-to-default, so the row's reset button -- which
+ // means "back to the theme's stamped value" and has no analogue here --
+ // never appears. The one exception is the font color row, where the button
+ // means "stop overriding the profile" and the text block asks for it back.
+ %row.resetButton.setVisible(false);
+ return %row;
+}
+
+function GuiEditorInspectorPane::addFieldRow(%this, %container, %field, %label, %kind, %enumItems)
+{
+ %row = %this.makeFieldRow(%container, %field, %label, %kind, %enumItems);
+
+ %this.row[%field] = %row;
+ %this.rowFields = (%this.rowFields $= "") ? %field : (%this.rowFields SPC %field);
+ return %row;
+}
+
+// Forget rows that are about to be deleted, so a later refresh does not reach
+// through a dangling handle.
+function GuiEditorInspectorPane::clearRows(%this, %fields)
+{
+ %count = getWordCount(%fields);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %field = getWord(%fields, %i);
+ %this.row[%field] = "";
+ %this.rowFields = trim(strreplace(" " @ %this.rowFields @ " ", " " @ %field @ " ", " "));
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Field presentation. The shared rows are built before any control is selected,
+// so their kinds come from a small table; a class row can ask the control
+// itself, which is always more accurate.
+//-----------------------------------------------------------------------------
+
+function GuiEditorInspectorPane::sharedKindFor(%this, %field)
+{
+ switch$(%field)
+ {
+ case "MinExtent": return "point";
+ case "isContainer" or "textWrap" or "textExtend" or "overrideFontColor" or "useInput": return "bool";
+ case "tooltipWidth" or "hovertime": return "number";
+
+ // A multiplier on the profile's font size, so 1.25 is an ordinary value
+ // for it and a whole-number row would round every one of them away.
+ case "fontSizeAdjust": return "decimal";
+ case "fontColor": return "color";
+ case "align" or "vAlign": return "enum";
+ case "easeFillColorHL" or "easeFillColorSL": return "enum";
+ case "easeTimeFillColorHL" or "easeTimeFillColorSL": return "number";
+ }
+ return "text";
+}
+
+function GuiEditorInspectorPane::sharedEnumItemsFor(%this, %field)
+{
+ switch$(%field)
+ {
+ // "default" is offered now that the engine's tables expose it -- it is
+ // the value a control starts on, meaning "inherit the profile's".
+ case "align": return "default" TAB "left" TAB "center" TAB "right";
+ case "vAlign": return "default" TAB "top" TAB "middle" TAB "bottom";
+ case "easeFillColorHL" or "easeFillColorSL": return %this.easingItems();
+ }
+ return "";
+}
+
+// The easing names the engine offers, taken from gEasingTable in guiControl.cc.
+function GuiEditorInspectorPane::easingItems(%this)
+{
+ return "Linear" TAB "EaseIn" TAB "EaseOut" TAB "EaseInOut";
+}
+
+// A class row can read its own type off the control, which is exact.
+function GuiEditorInspectorPane::kindFor(%this, %ctrl, %field)
+{
+ %kind = %this.spec.kindForType(%ctrl.getFieldType(%field));
+
+ // A profile slot is a short list of the theme's members for its category,
+ // not a free-text name.
+ if(%kind $= "profile")
+ {
+ return "dropdown";
+ }
+ return %kind;
+}
+
+// An enum's legal values are not exposed to script, so the ones that reach a
+// class row are listed here. Anything missing falls back to a text box, which
+// still edits the field correctly -- it just does not offer the choices.
+function GuiEditorInspectorPane::enumItemsFor(%this, %ctrl, %field)
+{
+ switch$(%field)
+ {
+ // The anchor names, not the originals: these say which edge stays put
+ // rather than which one moves. The old set still loads but is never
+ // offered (guiControl.cc).
+ case "HorizSizing": return "anchorLeft" TAB "anchorRight" TAB "width" TAB "center" TAB "scale" TAB "fill";
+ case "VertSizing": return "anchorTop" TAB "anchorBottom" TAB "height" TAB "center" TAB "scale" TAB "fill";
+ case "hScrollBar" or "vScrollBar": return "alwaysOn" TAB "alwaysOff" TAB "dynamic";
+ case "CellModeX" or "CellModeY": return "absolute" TAB "variable";
+ case "OrderMode": return "lrtb" TAB "tblr";
+ case "TabPosition": return "Top" TAB "Bottom";
+ case "DisplayMode": return "Dropper" TAB "Pallet" TAB "BlendRange" TAB "HueRange" TAB "AlphaRange";
+ case "valueMode": return "RGB" TAB "HSB" TAB "Hex";
+ case "inputMode": return "AllText" TAB "Number" TAB "Decimal" TAB "AlphaNumeric";
+ }
+ return "";
+}
+
+//-----------------------------------------------------------------------------
+// Binding.
+//-----------------------------------------------------------------------------
+
+function GuiEditorInspectorPane::bind(%this, %ctrl)
+{
+ if(!isObject(%ctrl))
+ {
+ %this.unbind();
+ return;
+ }
+
+ // The sizing stash belongs to whichever control it was taken from. Moving
+ // the selection abandons it rather than carrying a stale position onto
+ // something else -- it exists only so that clicking through the modes on
+ // one control can be undone.
+ if(%ctrl != %this.target)
+ {
+ %this.clearStash("h");
+ %this.clearStash("v");
+ }
+
+ %this.target = %ctrl;
+ %class = %ctrl.getClassName();
+
+ if(%class !$= %this.boundClass)
+ {
+ %this.buildClassSections(%class);
+ %this.boundClass = %class;
+ }
+
+ // Always, not only on a class change: the header's value block depends on
+ // the control as well as its class (a sprite shows the source it is using).
+ %this.header.bindClass(%ctrl, %class);
+ %this.buildVariantsSection();
+ %this.dynamicFields.bind(%ctrl);
+ %this.itemsBlock.bind(%this.spec.hasItemList(%class) ? %ctrl : "");
+
+ %this.applyFilter();
+ %this.refresh();
+ %this.forceLayout();
+ %this.setVisible(true);
+}
+
+// Nothing selected. The blocks keep their rows -- rebuilding them is what this
+// pane goes out of its way to avoid -- and the whole pane simply stops drawing,
+// so no stale values are left on show.
+function GuiEditorInspectorPane::unbind(%this)
+{
+ %this.target = "";
+ %this.clearStash("h");
+ %this.clearStash("v");
+ %this.setVisible(false);
+}
+
+// Rebuild the sections that belong to this class alone. The shared sections and
+// everything in the header shell are left standing.
+function GuiEditorInspectorPane::buildClassSections(%this, %class)
+{
+ %count = getWordCount(%this.classPanels);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %key = getWord(%this.classPanels, %i);
+ %this.clearRows(%this.panelFields[%key]);
+ %this.panel[%key] = "";
+ %this.panelFields[%key] = "";
+ }
+ %this.classChain.deleteObjects();
+ %this.classPanels = "";
+
+ %keys = %this.spec.sectionKeys(%class);
+ %keyCount = getWordCount(%keys);
+ for(%i = 0; %i < %keyCount; %i++)
+ {
+ %key = getWord(%keys, %i);
+ %this.buildClassSection(%class, %key);
+ }
+
+ // A class the table has never heard of gets everything it registers that is
+ // not already on show, so an uncovered control degrades to roughly what the
+ // native inspector did rather than to nothing.
+ if(!%this.spec.isKnownClass(%class))
+ {
+ %this.buildOtherSection(%class);
+ }
+}
+
+function GuiEditorInspectorPane::buildClassSection(%this, %class, %key)
+{
+ %fields = %this.spec.sectionFields(%class, %key);
+ %panel = %this.makeSectionPanel(%this.spec.sectionTitle(%class, %key));
+ %this.classChain.add(%panel);
+
+ %grid = %this.makeCellGrid(24);
+ %panel.add(%grid);
+
+ %this.panel[%key] = %panel;
+ %this.panelFields[%key] = %fields;
+ %this.classPanels = (%this.classPanels $= "") ? %key : (%this.classPanels SPC %key);
+
+ %count = getWordCount(%fields);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %field = getWord(%fields, %i);
+ %this.addFieldRow(%grid, %field, %this.spec.labelFor(%field),
+ %this.kindFor(%this.target, %field),
+ %this.enumItemsFor(%this.target, %field));
+ }
+}
+
+// Everything the control registers that nothing above has claimed. Uses the
+// control's own field list, so it needs no knowledge of the class at all.
+function GuiEditorInspectorPane::buildOtherSection(%this, %class)
+{
+ %claimed = %this.rowFields SPC %this.spec.geometryFields() SPC
+ %this.spec.deadFields() SPC %this.spec.runtimeToggles() SPC
+ %this.spec.editorToggles() SPC "name Profile";
+
+ %fields = "";
+ %count = %this.target.getFieldCount();
+ for(%i = 0; %i < %count; %i++)
+ {
+ %field = %this.target.getField(%i);
+ if(%this.spec.listHas(%claimed, %field))
+ {
+ continue;
+ }
+ if(%this.spec.kindForType(%this.target.getFieldType(%field)) $= "hidden")
+ {
+ continue;
+ }
+ %fields = (%fields $= "") ? %field : (%fields SPC %field);
+ }
+
+ if(%fields $= "")
+ {
+ return;
+ }
+
+ %panel = %this.makeSectionPanel("Other");
+ %this.classChain.add(%panel);
+ %grid = %this.makeCellGrid(24);
+ %panel.add(%grid);
+
+ %this.panel["Other"] = %panel;
+ %this.panelFields["Other"] = %fields;
+ %this.classPanels = (%this.classPanels $= "") ? "Other" : (%this.classPanels SPC "Other");
+
+ %fieldCount = getWordCount(%fields);
+ for(%i = 0; %i < %fieldCount; %i++)
+ {
+ %field = getWord(%fields, %i);
+ %this.addFieldRow(%grid, %field, %this.spec.labelFor(%field),
+ %this.kindFor(%this.target, %field),
+ %this.enumItemsFor(%this.target, %field));
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Variants: the control's secondary profile slots, shown only where there is
+// something to choose between.
+//
+// Every slot other than Profile itself -- contentProfile, thumbProfile,
+// closeButtonProfile and the rest -- is assigned by GuiEditorThemeApplier from
+// the Gui's theme, and in the ordinary case there is exactly one profile in the
+// theme for that slot's category. Showing a dropdown with one entry in it is
+// noise, so the row only exists once a second candidate does.
+//
+// The count and the contents are deliberately different sets. An uncategorised
+// standalone -- "Any" in the Profile Editor -- is offered in a row that already
+// exists but never causes one to appear, because otherwise a single "Any"
+// profile would sprout a Variants row on every slot of every control, which is
+// exactly the complexity this pane exists to remove.
+//-----------------------------------------------------------------------------
+
+function GuiEditorInspectorPane::buildVariantsSection(%this)
+{
+ if(isObject(%this.panel["Variants"]))
+ {
+ %this.clearRows(%this.panelFields["Variants"]);
+ %this.panel["Variants"] = "";
+ %this.panelFields["Variants"] = "";
+ %this.panelList = trim(strreplace(" " @ %this.panelList @ " ", " Variants ", " "));
+ }
+ %this.variantsChain.deleteObjects();
+
+ %fields = %this.variantSlots(%this.target);
+ if(%fields $= "")
+ {
+ return;
+ }
+
+ %panel = %this.makeSectionPanel("Variants");
+ %this.variantsChain.add(%panel);
+ %grid = %this.makeCellGrid(24);
+ %panel.add(%grid);
+
+ %this.panel["Variants"] = %panel;
+ %this.panelFields["Variants"] = %fields;
+ %this.panelList = %this.panelList SPC "Variants";
+
+ %count = getWordCount(%fields);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %field = getWord(%fields, %i);
+ %this.addFieldRow(%grid, %field, %this.spec.labelFor(%field), "dropdown", "");
+ }
+}
+
+// The slots worth showing: every TypeGuiProfile field except the control's own
+// Profile, which the header already carries, and only where the narrow
+// candidate set has more than one member.
+//
+// Cursor slots join on the same terms. A themed Gui gets its cursors filled by
+// GuiEditorThemeApplier without being asked, and a theme almost always offers
+// exactly one cursor per category -- so a row appears only once the theme holds
+// a second cursor for that job and there is genuinely a choice to make.
+function GuiEditorInspectorPane::variantSlots(%this, %ctrl)
+{
+ if(!isObject(%ctrl) || %this.spec.hasFlag(%ctrl.getClassName(), "bare"))
+ {
+ return "";
+ }
+
+ %fields = "";
+ %count = %ctrl.getFieldCount();
+ for(%i = 0; %i < %count; %i++)
+ {
+ %field = %ctrl.getField(%i);
+ %type = %ctrl.getFieldType(%field);
+
+ if(%type $= "GuiProfile" && %field !$= "Profile")
+ {
+ if(getWordCount(%this.profileAnchors(%ctrl, %field)) > 1)
+ {
+ %fields = (%fields $= "") ? %field : (%fields SPC %field);
+ }
+ }
+ else if(%type $= "GuiCursor")
+ {
+ if(getWordCount(%this.cursorAnchors(%ctrl, %field)) > 1)
+ {
+ %fields = (%fields $= "") ? %field : (%fields SPC %field);
+ }
+ }
+ }
+ return %fields;
+}
+
+// A cursor slot's candidates: the active theme's members of the slot's cursor
+// category, plus whatever the slot holds now. There is no stand-alone cursor to
+// merge in - a cursor belongs to a theme or to nothing.
+function GuiEditorInspectorPane::cursorAnchors(%this, %ctrl, %field)
+{
+ %category = GuiEditor.themeApplier.cursorCategoryForField(%field);
+ if(%category $= "")
+ {
+ return "";
+ }
+
+ %theme = GuiEditor.themeByName(GuiEditor.themeName);
+ %list = isObject(%theme) ? %theme.getCursors(%category) : "";
+
+ %current = GuiEditor.themeApplier.fieldCursor(%ctrl, %field);
+ if(isObject(%current))
+ {
+ %list = %this.addUnique(%list, %current);
+ }
+
+ return %list;
+}
+
+// Why does this control show the Variants rows it shows? Type
+//
+// GuiEditor.inspectorWindow.pane.dumpSlots();
+//
+// into the console with something selected. For each profile slot it prints the
+// category, what the slot holds, and every candidate with the theme that owns
+// it -- which is the only way to tell a theme member from a standalone that
+// happens to share a name, and the fastest way to find out why a row appeared.
+function GuiEditorInspectorPane::dumpSlots(%this)
+{
+ if(!isObject(%this.target))
+ {
+ echo("dumpSlots: nothing selected.");
+ return;
+ }
+
+ %applier = GuiEditor.themeApplier;
+ %library = GuiEditor.themeLibrary;
+ %themes = %library.getThemes();
+
+ echo("dumpSlots: " @ %this.target.getClassName() @ " '" @ %this.target.getName() @
+ "', active theme '" @ GuiEditor.themeName @ "', " @ getWordCount(%themes) @ " theme(s) loaded.");
+
+ for(%i = 0; %i < getWordCount(%themes); %i++)
+ {
+ %t = getWord(%themes, %i);
+ echo(" theme " @ %t.getName() @ " (" @ %t @ ") members=" @ %t.getProfileCount());
+ }
+ for(%i = 0; %i < %library.standaloneFolder.getCount(); %i++)
+ {
+ %p = %library.standaloneFolder.getObject(%i).target;
+ if(isObject(%p))
+ {
+ echo(" standalone " @ %p.getName() @ " (" @ %p @ ") category='" @ %p.category @ "'");
+ }
+ }
+
+ // themeOf needs the applier's theme list, which it only holds between these.
+ %applier.beginApply();
+ %count = %this.target.getFieldCount();
+ for(%i = 0; %i < %count; %i++)
+ {
+ %field = %this.target.getField(%i);
+ if(%this.target.getFieldType(%field) !$= "GuiProfile")
+ {
+ continue;
+ }
+
+ %cat = %this.categoryForSlot(%this.target, %field);
+ %cur = %applier.fieldProfile(%this.target, %field);
+ %anchors = %this.profileAnchors(%this.target, %field);
+
+ echo(" slot " @ %field @ " category='" @ %cat @ "' current=" @
+ (isObject(%cur) ? %cur.getName() @ "(" @ %cur @ ")" : "none") @
+ " anchors=" @ getWordCount(%anchors) @
+ (getWordCount(%anchors) > 1 ? " <-- ROW SHOWN" : ""));
+
+ for(%a = 0; %a < getWordCount(%anchors); %a++)
+ {
+ %p = getWord(%anchors, %a);
+ %owner = %applier.themeOf(%p);
+ echo(" " @ %p.getName() @ " (" @ %p @ ") from " @
+ (isObject(%owner) ? "theme " @ %owner.getName() : "standalone/unowned"));
+ }
+ }
+ %applier.endApply();
+}
+
+//-----------------------------------------------------------------------------
+// Filtering. Nothing here creates or deletes a control -- it only decides what
+// is visible and what is inert.
+//-----------------------------------------------------------------------------
+
+function GuiEditorInspectorPane::applyFilter(%this)
+{
+ %spec = %this.spec;
+ %class = %this.boundClass;
+
+ // Which copy of the text block this class uses, before anything reads a row:
+ // its three field rows are shared names pointing at whichever block is in
+ // play, so the mapping has to be right before the filter walks them.
+ %home = %spec.textBlockHome(%class);
+ %block = %this.activeTextBlock();
+ %this.mapTextRows(%block);
+
+ %count = getWordCount(%this.rowFields);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %field = getWord(%this.rowFields, %i);
+ %row = %this.row[%field];
+ if(isObject(%row))
+ {
+ %row.setVisible(%spec.isFieldVisible(%class, %field));
+ }
+ }
+
+ // isContainer is the engine's answer rather than the table's: a control
+ // that cannot draw children has the field forced false, so the switch would
+ // be wired to nothing.
+ // isContainer moved to the header's icon row, which decides its own
+ // visibility from the same rule -- see GuiEditorHeaderBlock::bindClass.
+
+ // The block's own parts -- the caption, the two flags and the two alignments
+ // -- are its to filter. The header shows or hides its copy from the same
+ // answer (GuiEditorHeaderBlock::bindClass).
+ %this.textPanel.setVisible(%home $= "section");
+ if(isObject(%block))
+ {
+ %block.bindClass(%this.target, %class);
+ }
+
+ // A section with nothing left to show gets out of the way entirely.
+ //
+ // The class sections as well as the shared ones. They were left out, and a
+ // class section whose every field turned out to be hidden stayed on screen as
+ // a header that could not be opened: the grid drops hidden cells, so it
+ // measures zero high, and GuiPanelCtrl's expanded extent comes out equal to
+ // its collapsed one. Clicking it flipped the arrow and nothing else.
+ %panels = %this.panelList SPC %this.classPanels;
+ %count = getWordCount(%panels);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %key = getWord(%panels, %i);
+ %this.panel[%key].setVisible(%this.anyRowVisible(%this.panelFields[%key]));
+ }
+
+ // The Add row is always worth showing, so the section stays open whenever a
+ // control is bound at all -- unlike the others, an empty one is still the
+ // only way to put a field on the control.
+ %this.dynamicPanel.setVisible(isObject(%this.target));
+
+ // Same for Items, on the two classes that have any: an empty list is exactly
+ // the case the section exists to fix.
+ %this.itemsPanel.setVisible(isObject(%this.target) && %spec.hasItemList(%class));
+
+ %this.header.applyGeometryMode(%spec.geometryModeOf(%this.target));
+}
+
+// A field was added or removed, so the section's height changed under the
+// chain. Nothing above it moved, but the panel has to be told to re-measure.
+function GuiEditorInspectorPane::onDynamicFieldsChanged(%this)
+{
+ %this.dynamicFields.resize(0, 24, %this.paneWidth, getWord(%this.dynamicFields.getExtent(), 1));
+ %this.forceLayout();
+}
+
+// Which of the two text blocks the bound class uses, or nothing where none of
+// it applies (a sprite draws no text and reads no font).
+function GuiEditorInspectorPane::activeTextBlock(%this)
+{
+ switch$(%this.spec.textBlockHome(%this.boundClass))
+ {
+ case "header": return %this.header.textBlock;
+ case "section": return %this.sectionText;
+ }
+ return "";
+}
+
+// Point the three shared names at the block in use. Without this the second
+// block to be built would own the registry and the first would never load or
+// filter -- the rows would be on screen holding whatever the last selection of
+// the other kind left in them.
+function GuiEditorInspectorPane::mapTextRows(%this, %block)
+{
+ %fields = %this.spec.textBlockRowFields();
+ %count = getWordCount(%fields);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %field = getWord(%fields, %i);
+ %this.row[%field] = isObject(%block) ? %block.row[%field] : "";
+ }
+}
+
+function GuiEditorInspectorPane::anyRowVisible(%this, %fields)
+{
+ %count = getWordCount(%fields);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %row = %this.row[getWord(%fields, %i)];
+ if(isObject(%row) && %row.isVisible())
+ {
+ return true;
+ }
+ }
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Loading values. The populating guard keeps every setText / setColorI /
+// setStateOn from echoing straight back through the commits.
+//-----------------------------------------------------------------------------
+
+function GuiEditorInspectorPane::refresh(%this)
+{
+ if(!isObject(%this.target))
+ {
+ return;
+ }
+
+ %this.populating = true;
+
+ // Candidates before values: a drop-down row keeps a selection that is not
+ // in its list by inserting it, so filling the list afterwards would drop
+ // what the control actually wears.
+ %this.refreshProfileChoices();
+
+ %count = getWordCount(%this.rowFields);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %field = getWord(%this.rowFields, %i);
+ %row = %this.row[%field];
+
+ // Profile slots were just loaded by name above. Reading one back off
+ // the control here would undo that: the field answers with whatever
+ // name it holds, and a profile created this session has one the Sim
+ // never registered.
+ //
+ // fontColor is skipped for a different reason: what its swatch shows is
+ // the profile's color while the control is not overriding it, which is
+ // not what the field holds. The block works that out.
+ if(!isObject(%row) || %this.isProfileSlot(%field) || %field $= "fontColor")
+ {
+ continue;
+ }
+ %row.setValue(%this.readField(%field));
+ }
+
+ // What the text block holds beyond its three field rows: two segmented rows,
+ // two toggles, and the font color swatch.
+ %block = %this.activeTextBlock();
+ if(isObject(%block))
+ {
+ %block.load(%this.target);
+ }
+
+ // A menu item's fields are none of them in the registry above -- the block
+ // keeps them out of it deliberately, so the bare rule cannot hide them -- so
+ // it reloads itself. This is the path an undo comes back through.
+ if(%this.header.menuItemBlock.isVisible())
+ {
+ %this.header.menuItemBlock.bind(%this.target);
+ }
+
+ // The static rows, for the same reason: they are not fields, so nothing above
+ // reaches them, and a replayed step can put back a caption, a switch or the
+ // whole order.
+ if(%this.itemsPanel.isVisible())
+ {
+ %this.itemsBlock.refresh();
+ }
+
+ %this.refreshToggles();
+ %this.header.anchorPicker.readEnums(
+ %this.target.getFieldValue("HorizSizing"),
+ %this.target.getFieldValue("VertSizing"));
+
+ %this.populating = false;
+}
+
+// The anchor picker moved. Both enums go in: one click can change either, and
+// clearing a Fill hands the axis back to its pins, which the other field has to
+// hear about too.
+//
+// Then the control is laid out immediately. Center and fill are positions the
+// control should always be in rather than reactions to a size change, so
+// leaving them until the next parent resize means picking one appears to do
+// nothing -- the control sat still until you nudged it.
+//
+// Because those two overwrite geometry the user typed, the position and extent
+// they are about to destroy are kept first, per axis, and handed back when the
+// axis returns to a mode that does not own them. That makes clicking through
+// Fill, Scale and back a lossless way to look at the options. The stash is
+// deliberately shallow: it lives until the selection changes and is then gone,
+// so it never has to be reconciled with anything else that moves the control.
+function GuiEditorInspectorPane::onSizingChanged(%this, %horiz, %vert)
+{
+ if(%this.populating || !isObject(%this.target))
+ {
+ return;
+ }
+
+ // What the whole change starts from. Setting an axis to center or fill hands
+ // that axis's geometry to the engine, which lays the control out then and
+ // there -- so the enum and the move it causes are one change, and undoing
+ // the enum alone would leave the control sitting where centring put it.
+ %recorder = GuiEditor.undoRecorder;
+ %horizBefore = %this.target.getFieldValue("HorizSizing");
+ %vertBefore = %this.target.getFieldValue("VertSizing");
+ %posBefore = %this.target.getPosition();
+ %extentBefore = %this.target.getExtent();
+
+ // Recorded afterwards as the one net change, rather than as each write the
+ // body makes: leaving or entering a mode writes the geometry twice, and an
+ // undo replaying those in reverse would stop on the intermediate values.
+ %recorder.suspend();
+
+ // Order matters, and got this wrong once. The stash has to be taken while
+ // the geometry is still the user's, which is now -- writing the enum alone
+ // moves nothing. The restore has to wait until AFTER the enum is written,
+ // because a control still set to center or fill overrides any position or
+ // extent written to it: the write appeared to land and read straight back
+ // out as the centred value.
+ %this.stashIfNeeded("h", %horiz);
+ %this.stashIfNeeded("v", %vert);
+
+ %this.writeField("HorizSizing", %horiz);
+ %this.writeField("VertSizing", %vert);
+
+ // A no-op for every mode that needs a size delta, which is what we want:
+ // they have nothing to react to. Center and fill apply now.
+ %this.target.applySizing();
+
+ %this.restoreIfNeeded("h", %horiz);
+ %this.restoreIfNeeded("v", %vert);
+
+ %recorder.resume();
+
+ // Geometry first and the enums last, because the ops replay in reverse for
+ // an undo and the enums have to be off center or fill before the position
+ // and extent are written back -- the same rule the body above follows.
+ %recorder.begin("Change Sizing", "");
+ %recorder.recordField(%this.target, "Position", %posBefore, %this.target.getPosition(), false);
+ %recorder.recordField(%this.target, "Extent", %extentBefore, %this.target.getExtent(), false);
+ %recorder.recordField(%this.target, "HorizSizing", %horizBefore,
+ %this.target.getFieldValue("HorizSizing"), false);
+ %recorder.recordField(%this.target, "VertSizing", %vertBefore,
+ %this.target.getFieldValue("VertSizing"), false);
+ %recorder.end();
+
+ %this.refreshGeometry();
+ %this.afterCommit();
+}
+
+// The two modes that overwrite what the user set. Center takes the position;
+// fill takes the position and the extent.
+function GuiEditorInspectorPane::sizingOwnsGeometry(%this, %mode)
+{
+ return %mode $= "center" || %mode $= "fill";
+}
+
+// Entering one of those: keep what it is about to overwrite, unless something
+// is already kept -- the first value is the one worth returning to.
+function GuiEditorInspectorPane::stashIfNeeded(%this, %axis, %mode)
+{
+ if(!%this.sizingOwnsGeometry(%mode) || %this.stashed[%axis] !$= "")
+ {
+ return;
+ }
+
+ %this.stashed[%axis] = true;
+ %this.stashPos[%axis] = %this.target.getPosition();
+ %this.stashExtent[%axis] = %this.target.getExtent();
+}
+
+// Leaving one: give back what it took, on this axis only -- the other may still
+// be filled, and restoring both would undo it.
+function GuiEditorInspectorPane::restoreIfNeeded(%this, %axis, %mode)
+{
+ if(%this.sizingOwnsGeometry(%mode) || %this.stashed[%axis] $= "")
+ {
+ return;
+ }
+
+ %pos = %this.target.getPosition();
+ %extent = %this.target.getExtent();
+ if(%axis $= "h")
+ {
+ %pos = getWord(%this.stashPos[%axis], 0) SPC getWord(%pos, 1);
+ %extent = getWord(%this.stashExtent[%axis], 0) SPC getWord(%extent, 1);
+ }
+ else
+ {
+ %pos = getWord(%pos, 0) SPC getWord(%this.stashPos[%axis], 1);
+ %extent = getWord(%extent, 0) SPC getWord(%this.stashExtent[%axis], 1);
+ }
+
+ // Through the fields, not setPosition/setExtent. "scale" does not recompute
+ // its proportion every layout -- relPosBatteryH caches it in
+ // mStoredRelativePosH and keeps using it until resetStoredRelPos runs, and
+ // the only things that call that are the Position and Extent field setters
+ // (guiControl.h setPositionFn / setExtentFn). Restoring with the console
+ // methods left the proportion captured while the control was filled, so the
+ // next layout stretched it straight back out again.
+ %this.writeField("Position", %pos);
+ %this.writeField("Extent", %extent);
+ %this.clearStash(%axis);
+}
+
+function GuiEditorInspectorPane::clearStash(%this, %axis)
+{
+ %this.stashed[%axis] = "";
+ %this.stashPos[%axis] = "";
+ %this.stashExtent[%axis] = "";
+}
+
+// Position and extent move without the pane being rebuilt -- dragging a control
+// on the canvas ends in an Edit event -- so this is the cheap path that reloads
+// values and touches nothing else.
+function GuiEditorInspectorPane::refreshGeometry(%this)
+{
+ if(!isObject(%this.target))
+ {
+ return;
+ }
+
+ %this.populating = true;
+ %this.header.positionRow.setValue(%this.target.getPosition());
+ %this.header.extentRow.setValue(%this.target.getExtent());
+ %this.populating = false;
+}
+
+function GuiEditorInspectorPane::readField(%this, %field)
+{
+ // getFieldValue answers "" for a name in editor mode, where assignName
+ // stashes the name rather than registering it, so ask the object instead.
+ if(%field $= "name")
+ {
+ return %this.target.getName();
+ }
+ return %this.target.getFieldValue(%field);
+}
+
+function GuiEditorInspectorPane::writeField(%this, %field, %value)
+{
+ // Through the recorder, which does the setEditFieldValue itself. Every write
+ // the pane makes is an edit the user can take back, and routing them all
+ // through one place is what makes that true without each caller remembering
+ // to say so.
+ //
+ // (setEditFieldValue rather than a plain assignment brackets the write with
+ // inspectPreApply / inspectPostApply, which is what lets a control react to
+ // being re-profiled or resized from here.)
+ GuiEditor.undoRecorder.writeField(%this.target, %field, %value);
+}
+
+function GuiEditorInspectorPane::refreshToggles(%this)
+{
+ %header = %this.header;
+
+ // hidden and locked are not here. They are editor working state, never
+ // written to the document, and they live in the Explorer tree's two columns.
+ %header.visibleButton.setValue(%this.target.Visible);
+ %header.activeButton.setValue(%this.target.Active);
+ %header.inputButton.setValue(%this.target.useInput);
+ %header.containerButton.setValue(%this.target.isContainer);
+
+ %header.refreshWindowToggles(%this.target);
+}
+
+// A segmented row in the header picked a value. Same contract as the toggles:
+// the widget holds the choice, the pane does the writing.
+function GuiEditorInspectorPane::onHeaderChoiceChanged(%this, %field, %value)
+{
+ if(%this.populating || !isObject(%this.target))
+ {
+ return;
+ }
+
+ %this.writeField(%field, %value);
+ %this.afterCommit();
+}
+
+// A toggle reports the click and the value it flipped itself to, and the pane
+// does the writing -- so that every write to the control still goes through one
+// place, and a new toggle needs no case of its own here.
+function GuiEditorInspectorPane::onToggleChanged(%this, %field, %value)
+{
+ if(%this.populating || !isObject(%this.target))
+ {
+ return;
+ }
+
+ %this.writeField(%field, %value);
+ %this.refreshToggles();
+ %this.afterCommit();
+}
+
+//-----------------------------------------------------------------------------
+// Profile slots. A slot is a choice among the theme's members for its category,
+// never a list of every profile in the sim -- which is what the native
+// inspector offered, applied by name, silently failing for anything made this
+// session (editor mode does not register names).
+//-----------------------------------------------------------------------------
+
+// The narrow set, which decides whether a slot is worth showing at all: this
+// theme's members of the slot's category, any standalone profile stamped for
+// that category, and whatever the slot currently holds. An uncategorised
+// standalone is deliberately absent -- one of those would otherwise make a
+// Variants row appear on every slot of every control.
+//
+// The current value counts so that a slot wearing something the theme does not
+// offer is visible and changeable rather than silently stuck. In the ordinary
+// case it is already one of the theme's members and dedupes away.
+function GuiEditorInspectorPane::profileAnchors(%this, %ctrl, %field)
+{
+ %category = %this.categoryForSlot(%ctrl, %field);
+ if(%category $= "")
+ {
+ return "";
+ }
+
+ return %this.profileAnchorsFor(%category,
+ GuiEditor.themeApplier.fieldProfile(%ctrl, %field));
+}
+
+// The same set for a category named outright. Changing a control's category has
+// to ask what the category it is moving TO can offer, which it cannot get by
+// asking the control -- the control still wears the old one.
+function GuiEditorInspectorPane::profileAnchorsFor(%this, %category, %current)
+{
+ %library = GuiEditor.themeLibrary;
+ %theme = GuiEditor.themeByName(GuiEditor.themeName);
+
+ %list = isObject(%theme) ? %theme.getProfiles(%category) : "";
+ %list = %this.addUnique(%list, %library.getStandaloneProfiles(%category));
+
+ if(isObject(%current))
+ {
+ %list = %this.addUnique(%list, %current);
+ }
+
+ return %list;
+}
+
+// The wider set, which is what the drop-down actually offers once it exists.
+// "Any" means usable as any control's main profile, not usable in any slot.
+function GuiEditorInspectorPane::profileOptions(%this, %ctrl, %field)
+{
+ %list = %this.profileAnchors(%ctrl, %field);
+ return %this.addUnique(%list, GuiEditor.themeLibrary.getStandaloneProfiles(""));
+}
+
+function GuiEditorInspectorPane::categoryForSlot(%this, %ctrl, %field)
+{
+ return GuiEditor.themeApplier.categoryForField(%field, %this.currentCategory(%ctrl));
+}
+
+//-----------------------------------------------------------------------------
+// The control's own category. For every class but one this is the class's
+// answer and there is nothing to ask: a check box wants a CheckBox profile.
+//
+// A bare GuiControl is the exception. The applier guesses at drop time from
+// what the control holds -- root, or text, or neither -- and typing a caption
+// afterwards re-runs nothing, so the guess needs to be correctable. What makes
+// that work with no new state is that a profile carries the category it was
+// stamped for: the control wearing a Label profile IS the record that it was
+// told to be a Label, and it survives being saved, reloaded, and re-themed
+// (GuiEditorThemeApplier::applyToControl carries it across a theme switch).
+//
+// Only a category the class actually offers counts. Anything else -- a
+// hand-written Gui wearing a ListBox profile on a plain GuiControl -- falls
+// back to the guess, and the profile it holds still shows up as a candidate.
+//-----------------------------------------------------------------------------
+
+function GuiEditorInspectorPane::currentCategory(%this, %ctrl)
+{
+ %applier = GuiEditor.themeApplier;
+ %isRoot = (%ctrl.getParent() == %applier.rootContainer);
+ %guess = %applier.categoryForControl(%ctrl, %isRoot);
+
+ %choices = %this.spec.categoryChoices(%ctrl.getClassName());
+ if(%choices $= "")
+ {
+ return %guess;
+ }
+
+ %current = %applier.fieldProfile(%ctrl, "Profile");
+ if(isObject(%current))
+ {
+ %match = %this.spec.matchCategory(%choices, %current.category);
+ if(%match !$= "")
+ {
+ return %match;
+ }
+ }
+
+ return %guess;
+}
+
+// Move the control onto a category: its main profile becomes that category's,
+// so the picker and the profile can never disagree. The theme's own member
+// first, then any standalone stamped for the category, and if the category is
+// empty the list is refilled and the profile left alone rather than cleared.
+function GuiEditorInspectorPane::setCategory(%this, %category)
+{
+ if(!isObject(%this.target) || %category $= "")
+ {
+ return;
+ }
+
+ %theme = GuiEditor.themeByName(GuiEditor.themeName);
+ %profile = isObject(%theme) ? %theme.getProfile(%category) : 0;
+ if(!isObject(%profile))
+ {
+ %profile = getWord(%this.profileAnchorsFor(%category, 0), 0);
+ }
+
+ if(isObject(%profile))
+ {
+ // By id, not by name: a profile made this session carries a name the Sim
+ // never registered, because the editor runs with assignName stashing
+ // names rather than adding them.
+ GuiEditor.undoRecorder.begin("Change Category", "");
+ %this.writeField("Profile", %profile.getId());
+ GuiEditor.undoRecorder.end();
+ }
+
+ // Everything downstream of the profile moves with it -- which profiles the
+ // slot now offers, and the font color the swatch falls back to.
+ %this.refresh();
+ %this.afterCommit();
+}
+
+function GuiEditorInspectorPane::addUnique(%this, %list, %additions)
+{
+ %count = getWordCount(%additions);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %item = getWord(%additions, %i);
+ if(%item $= "" || %this.spec.listHas(%list, %item))
+ {
+ continue;
+ }
+ %list = (%list $= "") ? %item : (%list SPC %item);
+ }
+ return %list;
+}
+
+// Fill every profile drop-down -- the header's Profile and any Variants row --
+// with its candidates and select what the control wears.
+function GuiEditorInspectorPane::refreshProfileChoices(%this)
+{
+ if(!isObject(%this.target))
+ {
+ return;
+ }
+
+ if(%this.header.categoryRow.isVisible())
+ {
+ %this.fillCategoryRow();
+ }
+
+ if(%this.header.profileRow.isVisible())
+ {
+ %this.fillProfileRow(%this.header.profileRow, "Profile");
+ }
+
+ %slots = %this.panelFields["Variants"];
+ %count = getWordCount(%slots);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %field = getWord(%slots, %i);
+ %row = %this.row[%field];
+ if(!isObject(%row))
+ {
+ continue;
+ }
+
+ if(%this.isCursorSlot(%field))
+ {
+ %this.fillCursorRow(%row, %field);
+ }
+ else
+ {
+ %this.fillProfileRow(%row, %field);
+ }
+ }
+}
+
+// A cursor slot's candidates, by name, exactly as a profile slot's: the name is
+// the label and the commit looks it back up, because a cursor created this
+// session has a name the Sim may not resolve.
+function GuiEditorInspectorPane::fillCursorRow(%this, %row, %field)
+{
+ %ids = %this.cursorAnchors(%this.target, %field);
+ %items = "";
+ %count = getWordCount(%ids);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %cursor = getWord(%ids, %i);
+ %name = %cursor.getName();
+ if(%name $= "")
+ {
+ continue;
+ }
+ %items = (%items $= "") ? %name : (%items TAB %name);
+ %this.cursorByName[%name] = %cursor;
+ }
+
+ %row.currentItem = "";
+ %row.fillItems(%items);
+
+ %current = GuiEditor.themeApplier.fieldCursor(%this.target, %field);
+ %row.setValue(isObject(%current) ? %current.getName() : "");
+}
+
+// The categories this class may take, and which one it is on. Unlike a profile
+// row these are plain strings, so the name in the list is the value.
+function GuiEditorInspectorPane::fillCategoryRow(%this)
+{
+ %row = %this.header.categoryRow;
+ %choices = %this.spec.categoryChoices(%this.boundClass);
+
+ %items = "";
+ %count = getWordCount(%choices);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %item = getWord(%choices, %i);
+ %items = (%items $= "") ? %item : (%items TAB %item);
+ }
+
+ // Same reason as a profile row: the list is recomputed from scratch on every
+ // bind, so a selection the new list does not hold is a ghost of the last one
+ // rather than something worth preserving.
+ %row.currentItem = "";
+ %row.fillItems(%items);
+ %row.setValue(%this.currentCategory(%this.target));
+}
+
+// One slot's candidates, listed by name. The name is only a label: a commit
+// looks the choice back up and writes the id, because a profile made during
+// this editor session has a name the Sim cannot resolve.
+function GuiEditorInspectorPane::fillProfileRow(%this, %row, %field)
+{
+ %ids = %this.profileOptions(%this.target, %field);
+ %items = "";
+ %count = getWordCount(%ids);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %profile = getWord(%ids, %i);
+ %name = %profile.getName();
+ if(%name $= "")
+ {
+ continue;
+ }
+ %items = (%items $= "") ? %name : (%items TAB %name);
+ %this.profileByName[%name] = %profile;
+ }
+
+ // Forget what the row was showing before refilling it. fillItems deliberately
+ // preserves the current selection and re-inserts it when the new list does
+ // not contain it -- right for the Profile Editor's font list, where a face
+ // outside the directory must not be silently dropped, but wrong here: the
+ // candidates are recomputed from scratch every bind, so a name that is no
+ // longer one of them is a ghost of the last fill. It is how a profile from
+ // the previous theme survived a Set Theme.
+ %row.currentItem = "";
+ %row.fillItems(%items);
+
+ %current = GuiEditor.themeApplier.fieldProfile(%this.target, %field);
+ %row.setValue(isObject(%current) ? %current.getName() : "");
+}
+
+//-----------------------------------------------------------------------------
+// Commits. Every write to the control goes through here.
+//-----------------------------------------------------------------------------
+
+function GuiEditorInspectorPane::onFieldRowCommit(%this, %row)
+{
+ if(%this.populating || !isObject(%this.target))
+ {
+ return;
+ }
+
+ %field = %row.fieldName;
+
+ // The font color swatch is two fields, and the second of them is why the
+ // changed test cannot come first: picking the color the profile already
+ // draws in is a real edit when the override was off.
+ if(%field $= "fontColor")
+ {
+ %this.commitFontColor(%row);
+ return;
+ }
+
+ // The text box has been writing to the control on every keystroke, so the
+ // changed test cannot come first here either -- by now the control already
+ // says what the row says.
+ if(%field $= "text")
+ {
+ %this.commitText(%row);
+ return;
+ }
+
+ // A text box commits on blur, so most commits arrive from a field the user
+ // only tabbed through. Writing one anyway would put an edit in the undo
+ // record -- and mark the Gui dirty -- for something that never happened.
+ if(!%row.hasChanged())
+ {
+ return;
+ }
+
+ // Category names no field on the control. It picks the control's Profile,
+ // and has to be intercepted here or writeField would put a dynamic field
+ // called "category" on it.
+ if(%field $= "category")
+ {
+ %row.markClean();
+ %this.setCategory(%row.getValue());
+ return;
+ }
+
+ // A profile slot is chosen by name in the list but written by id: a profile
+ // created this session carries a name the Sim cannot resolve, because the
+ // editor runs with assignName stashing names rather than registering them.
+ // GuiEditorThemeApplier::applyToControl writes ids for exactly this reason.
+ if(%this.isProfileSlot(%field))
+ {
+ %profile = %this.profileByName[%row.getValue()];
+ if(isObject(%profile))
+ {
+ %this.writeField(%field, %profile.getId());
+ }
+ }
+ else if(%this.isCursorSlot(%field))
+ {
+ %cursor = %this.cursorByName[%row.getValue()];
+ if(isObject(%cursor))
+ {
+ %this.writeField(%field, %cursor.getId());
+ }
+ }
+ else
+ {
+ %this.writeField(%field, %row.getValue());
+ }
+
+ %row.markClean();
+
+ // Changing a sprite's source swaps which fields the header shows, which
+ // means deleting the row this commit arrived from. Deferred to the next
+ // tick so the engine is not mid-dispatch on a control being freed.
+ if(%this.boundClass $= "GuiSpriteCtrl" && %this.isSpriteSourceField(%field))
+ {
+ %this.schedule(0, "rebindDeferred");
+ }
+
+ %this.afterCommit();
+}
+
+function GuiEditorInspectorPane::isProfileSlot(%this, %field)
+{
+ return isObject(%this.target) &&
+ %this.target.getFieldType(%field) $= "GuiProfile";
+}
+
+function GuiEditorInspectorPane::isCursorSlot(%this, %field)
+{
+ return isObject(%this.target) &&
+ %this.target.getFieldType(%field) $= "GuiCursor";
+}
+
+function GuiEditorInspectorPane::isSpriteSourceField(%this, %field)
+{
+ return %this.spec.listHas("Image Animation Bitmap", %field);
+}
+
+function GuiEditorInspectorPane::rebindDeferred(%this)
+{
+ if(isObject(%this.target))
+ {
+ %this.bind(%this.target);
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Text, which reaches the control twice: once per keystroke so the canvas keeps
+// up, and once here so the change is recorded as the single edit it was.
+//
+// The pair matters because a field write is the editor's unit of change --
+// inspectPreApply / inspectPostApply, and whatever an undo record is eventually
+// hung on. Eleven keystrokes are one edit, not eleven, so the control is put
+// back to what it held when the first key landed and the new value written over
+// it once.
+//-----------------------------------------------------------------------------
+
+function GuiEditorInspectorPane::commitText(%this, %row)
+{
+ %block = %this.activeTextBlock();
+
+ // Nobody typed: an ordinary commit, from a box the user tabbed through or a
+ // value that arrived from script. Or the selection moved while an edit was
+ // open, in which case the stash belongs to a control this row is no longer
+ // showing -- the typed text is already on it, so there is nothing to save.
+ if(!isObject(%block) || !%block.typing || %block.typingTarget != %this.target)
+ {
+ if(%row.hasChanged())
+ {
+ %this.writeField("text", %row.getValue());
+ %row.markClean();
+ %this.afterCommit();
+ }
+ if(isObject(%block))
+ {
+ %block.endTyping();
+ }
+ return;
+ }
+
+ %value = %row.getValue();
+ %before = %block.textBeforeEdit;
+ %block.endTyping();
+ %row.markClean();
+
+ // strcmp, not $=: $= runs dStricmp, so retyping a caption in a different
+ // case would read as no change at all.
+ if(strcmp(%value, %before) == 0)
+ {
+ return;
+ }
+
+ %this.target.text = %before;
+ %this.writeField("text", %value);
+ %this.afterCommit();
+}
+
+//-----------------------------------------------------------------------------
+// Font color, which is two fields wearing one widget. overrideFontColor is
+// what decides whether fontColor is used at all (guiControl.cc renderText), and
+// on its own it is a checkbox that does nothing visible -- so the swatch is
+// both: picking a color turns the override on, and the row's reset button
+// turns it off again. With it off the swatch shows the profile's own color,
+// so the row always says what the control will actually draw in.
+//-----------------------------------------------------------------------------
+
+function GuiEditorInspectorPane::commitFontColor(%this, %row)
+{
+ // Nothing to do only when the color is unchanged AND the override was
+ // already on. Picking the profile's exact color while it was off is the
+ // user asking to pin that color down, which is a change to the control
+ // even though the swatch looks the same.
+ if(!%row.hasChanged() && %this.target.overrideFontColor)
+ {
+ return;
+ }
+
+ // Two fields, one edit.
+ GuiEditor.undoRecorder.begin("Change Font Color", "");
+ %this.writeField("fontColor", %row.getValue());
+ %this.writeField("overrideFontColor", true);
+ GuiEditor.undoRecorder.end();
+
+ %row.markClean();
+ %row.setOverridden(true);
+ %this.afterCommit();
+}
+
+// The row widget's reset means "back to the theme's stamped value" everywhere
+// else, which has no analogue for a control -- so every other row here hides
+// the button. The font color row keeps it, meaning "stop overriding the
+// profile's".
+function GuiEditorInspectorPane::onFieldRowReset(%this, %row)
+{
+ if(%row.fieldName !$= "fontColor" || !isObject(%this.target))
+ {
+ return;
+ }
+
+ %this.writeField("overrideFontColor", false);
+
+ %block = %this.activeTextBlock();
+ if(isObject(%block))
+ {
+ %this.populating = true;
+ %block.loadFontColor(%this.target);
+ %this.populating = false;
+ }
+
+ %this.afterCommit();
+}
+
+// The two flags in the text block's caption row. They change the control's size
+// rather than only its look -- textExtend grows the width when wrap is off and
+// the height when it is on -- so the geometry rows are stale the moment either
+// is written.
+function GuiEditorInspectorPane::onTextFlagChanged(%this, %field, %value)
+{
+ if(%this.populating || !isObject(%this.target))
+ {
+ return;
+ }
+
+ %this.writeField(%field, %value);
+ %this.refreshGeometry();
+ %this.afterCommit();
+}
+
+// Everything a write has to tell the rest of the editor. The canvas has to
+// redraw and the explorer tree may be showing the name that just changed.
+function GuiEditorInspectorPane::afterCommit(%this)
+{
+ if(isObject(%this.window))
+ {
+ %this.window.onPaneCommit(%this.target);
+ }
+}
diff --git a/editor/GuiEditor/scripts/GuiEditorInspectorWindow.cs b/editor/GuiEditor/scripts/GuiEditorInspectorWindow.cs
index 4b32bb2f2..56a9c4d96 100644
--- a/editor/GuiEditor/scripts/GuiEditorInspectorWindow.cs
+++ b/editor/GuiEditor/scripts/GuiEditorInspectorWindow.cs
@@ -2,10 +2,12 @@
function GuiEditorInspectorWindow::onAdd(%this)
{
+ // Fill rather than width/height -- the window's only child wants its whole
+ // content rect. See GuiEditorControlListWindow for why.
%this.scroller = new GuiScrollCtrl()
{
- HorizSizing="width";
- VertSizing="height";
+ HorizSizing="fill";
+ VertSizing="fill";
Position="0 0";
Extent="352 354";
hScrollBar="alwaysOff";
@@ -20,44 +22,25 @@
ThemeManager.setProfile(%this.scroller, "scrollArrowProfile", "ArrowProfile");
%this.add(%this.scroller);
- %this.inspector = new GuiInspector()
+ // The custom pane that replaced the native GuiInspector. The inspector is
+ // still a live C++ class -- the Asset Admin uses one -- but it has no place
+ // here: it reflected every registered field, which for a Gui control means
+ // offering fields the class provably never reads.
+ %this.pane = new GuiChainCtrl()
{
- Class = "GuiEditorInspector";
- HorizSizing="width";
- VertSizing="height";
- Position="0 0";
- Extent="338 354";
- FieldCellSize="288 40";
- ControlOffset="10 18";
- ConstantThumbHeight=false;
- ScrollBarThickness=12;
- ShowArrowButtons=true;
+ class = "GuiEditorInspectorPane";
+ HorizSizing = "width";
+ Position = "0 0";
+ Extent = "338 354";
+ IsVertical = true;
+ ChildSpacing = 6;
+ paneWidth = 338;
+ window = %this;
};
- ThemeManager.setProfile(%this.inspector, "emptyProfile");
- ThemeManager.setProfile(%this.inspector, "panelProfile", "GroupPanelProfile");
- ThemeManager.setProfile(%this.inspector, "emptyProfile", "GroupGridProfile");
- ThemeManager.setProfile(%this.inspector, "labelProfile", "LabelProfile");
- ThemeManager.setProfile(%this.inspector, "textEditProfile", "textEditProfile");
- ThemeManager.setProfile(%this.inspector, "dropDownProfile", "dropDownProfile");
- ThemeManager.setProfile(%this.inspector, "dropDownItemProfile", "dropDownItemProfile");
- ThemeManager.setProfile(%this.inspector, "emptyProfile", "backgroundProfile");
- ThemeManager.setProfile(%this.inspector, "scrollingPanelProfile", "ScrollProfile");
- ThemeManager.setProfile(%this.inspector, "scrollingPanelThumbProfile", "ThumbProfile");
- ThemeManager.setProfile(%this.inspector, "scrollingPanelTrackProfile", "TrackProfile");
- ThemeManager.setProfile(%this.inspector, "scrollingPanelArrowProfile", "ArrowProfile");
- ThemeManager.setProfile(%this.inspector, "checkboxProfile", "checkboxProfile");
- ThemeManager.setProfile(%this.inspector, "buttonProfile", "buttonProfile");
- ThemeManager.setProfile(%this.inspector, "tipProfile", "tooltipProfile");
- ThemeManager.setProfile(%this.inspector, "colorPickerProfile", "colorPopupProfile");
- ThemeManager.setProfile(%this.inspector, "colorPopupProfile", "colorPopupPanelProfile");
- ThemeManager.setProfile(%this.inspector, "emptyProfile", "colorPopupPickerProfile");
- ThemeManager.setProfile(%this.inspector, "colorPickerSelectorProfile", "colorPopupSelectorProfile");
- %this.scroller.add(%this.inspector);
+ %this.scroller.add(%this.pane);
+ %this.pane.build();
%this.inspectList = new SimSet();
-
- //%this.inspector.addHiddenField("isContainer");
- %this.inspector.addHiddenField("BindToGuiEditor");
}
function GuiEditorInspectorWindow::onRemove(%this)
@@ -69,9 +52,60 @@
}
}
+// Edit arrives after every drag and every resize on the canvas, not only on a
+// fresh selection (guiEditCtrl.cc calls it from the mouse-up that ends both).
+// Rebinding on each of those would rebuild the class sections while the user is
+// still dragging, so an Edit for the control already bound reloads geometry and
+// nothing else.
function GuiEditorInspectorWindow::onEdit(%this, %object)
{
- %this.inspector.inspect(%object);
+ if(isObject(%object) && %object == %this.pane.target)
+ {
+ %this.pane.refreshGeometry();
+ return;
+ }
+ %this.pane.bind(%object);
+}
+
+// Something re-profiled the control under the pane: a fresh drop being themed
+// on arrival, or Set Theme sweeping the whole document. The pane caches what it
+// found -- which slots were worth a Variants row, and what each drop-down
+// offers -- so a re-profile behind its back leaves it describing the control
+// the way it used to be.
+//
+// This is what a dropped control needs, because it is announced (and inspected)
+// wearing its constructor's profiles and only themed afterwards.
+function GuiEditorInspectorWindow::onRethemed(%this, %object)
+{
+ if(isObject(%this.pane.target))
+ {
+ %this.pane.bind(%this.pane.target);
+ }
+}
+
+// An undo or a redo rewrote the control the pane is already showing. Every row
+// is stale, not just the geometry ones -- a replayed step can put back a
+// caption, a toggle or a profile -- so this re-reads them all. bind() would do
+// that too, but by rebuilding the pane from scratch, and the control has not
+// changed: only its values have.
+function GuiEditorInspectorWindow::onReplayed(%this)
+{
+ if(isObject(%this.pane.target))
+ {
+ %this.pane.refresh();
+ }
+}
+
+// Reparenting changes which geometry fields the control is allowed to edit --
+// a chain, grid, frame set or tab book writes its children's bounds itself --
+// so the pane has to re-evaluate against the new parent. The brain has always
+// posted this event; nothing listened to it until now.
+function GuiEditorInspectorWindow::onParentChange(%this, %parent)
+{
+ if(isObject(%this.pane.target))
+ {
+ %this.pane.bind(%this.pane.target);
+ }
}
function GuiEditorInspectorWindow::onClearInspect(%this, %object)
@@ -82,7 +116,7 @@
%count = %this.inspectList.getCount();
if(%count > 0)
{
- %this.inspector.inspect(%this.inspectList.getObject(%count - 1));
+ %this.pane.bind(%this.inspectList.getObject(%count - 1));
}
}
}
@@ -90,13 +124,20 @@
function GuiEditorInspectorWindow::onClearInspectAll(%this)
{
%this.inspectList.clear();
- %this.inspector.clear();
+ %this.pane.unbind();
}
function GuiEditorInspectorWindow::onAlsoInspect(%this, %object)
{
%this.inspectList.add(%object);
- %this.inspector.inspect(%object);
+ %this.pane.bind(%object);
+}
+
+// A write landed on the selected control. The canvas has to redraw, and the
+// explorer tree may be showing a name that just changed.
+function GuiEditorInspectorWindow::onPaneCommit(%this, %object)
+{
+ %this.postEvent("PostApply", %object);
}
function GuiEditorInspectorWindow::onObjectRemoved(%this)
diff --git a/editor/GuiEditor/scripts/GuiEditorItemRow.cs b/editor/GuiEditor/scripts/GuiEditorItemRow.cs
new file mode 100644
index 000000000..d9c66cd05
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiEditorItemRow.cs
@@ -0,0 +1,349 @@
+
+//-----------------------------------------------------------------------------
+// One static row of a list box or a drop down, on one line of the Items
+// section.
+//
+// [ Easy ][ 1 ][c][#][o][*][^][v][X]
+//
+// caption, ID, "show color", the color, active, starts selected, move up, move
+// down, remove.
+//
+// Show color, not "own color": hasColor does not tint anything. The list draws a
+// small colored bullet in front of the caption and indents the text past it
+// (GuiListBoxCtrl::onRenderItem, renderColorBullet) - the caption itself is
+// drawn in the profile's font color either way. So the switch is about whether
+// the dot is there at all.
+//
+// Nine controls rather than the eight the row looks like it needs, because that
+// dot is two values: hasColor and color. Nothing can be read off a swatch alone
+// -- a swatch always holds SOME color -- so the toggle says whether there is a
+// dot and the swatch says what color it is, dead until the toggle is on. The
+// alternative was to read a transparent swatch as "no dot", which would have
+// meant picking a hue did nothing until the alpha was raised as well.
+//
+// The row owns its widgets and no values. It speaks in the same TAB-separated
+// records GuiListBoxCtrl::getItemList writes, so the block above it can join
+// what its rows say and hand the lot back without translating anything.
+//
+// The creator sets owner inline and calls build() once AFTER adding the row to
+// its cell, because the cell is what decides how wide it is. Everything it does
+// reports to owner:
+//
+// onItemRowTyped a caption keystroke - live, and not yet an undo step
+// onItemRowCommit a box lost focus or took Enter
+// onItemRowToggled a switch flipped
+// onItemRowMove the up or down arrow, with -1 or 1
+// onItemRowRemove the bin
+//-----------------------------------------------------------------------------
+
+function GuiEditorItemRow::onAdd(%this)
+{
+ ThemeManager.setProfile(%this, "emptyProfile");
+}
+
+function GuiEditorItemRow::build(%this)
+{
+ // The width the row actually has, not the one it was created with: the grid
+ // sizes a cell the moment it is added, which is before build() runs, so
+ // laying out against the nominal width would place every widget for a
+ // 338-wide row inside a cell the grid had already made narrower - and the
+ // icons at the right-hand end would sit off the edge of the pane. Sizing
+ // flags take it from here.
+ %w = getWord(%this.getExtent(), 0);
+ %pad = 4;
+ %gap = 2;
+ %iconW = 24;
+ %swatchW = 26;
+ %idW = 34;
+ %boxH = 22;
+
+ // Everything but the caption is a fixed size, so the group is measured once
+ // and the caption takes what is left. They all carry "left" sizing, which
+ // keeps them against the right edge as the Properties frame is dragged wider
+ // and gives the caption the slack.
+ %groupW = %idW + %swatchW + (%iconW * 6) + (%gap * 7);
+ %groupX = %w - %pad - %groupW;
+ %captionW = %groupX - %pad - %gap;
+
+ %this.setExtent(%w, 26);
+
+ %this.captionBox = new GuiTextEditCtrl()
+ {
+ HorizSizing = "width";
+ Position = %pad SPC 2;
+ Extent = %captionW SPC %boxH;
+ Tooltip = "What this row says.";
+ };
+ ThemeManager.setProfile(%this.captionBox, "textEditProfile");
+ ThemeManager.setProfile(%this.captionBox, "tipProfile", "TooltipProfile");
+ // Command is per keystroke - GuiTextEditCtrl runs it on every edit to its
+ // buffer - which is what makes the row appear on the canvas as it is typed.
+ // AltCommand is the blur and ReturnCommand the Enter, and those are the two
+ // that make an undo step.
+ %this.captionBox.Command = %this.getID() @ ".onCaptionTyped();";
+ %this.captionBox.AltCommand = %this.getID() @ ".onCommit();";
+ %this.captionBox.ReturnCommand = %this.getID() @ ".onCommit();";
+ %this.add(%this.captionBox);
+
+ %x = %groupX;
+
+ %this.idBox = new GuiTextEditCtrl()
+ {
+ HorizSizing = "left";
+ Position = %x SPC 2;
+ Extent = %idW SPC %boxH;
+ align = "center";
+ inputMode = "Number";
+ Tooltip = "A number script can find this row by, with findItemID. Rows that nothing looks up can all be left at zero.";
+ };
+ ThemeManager.setProfile(%this.idBox, "textEditProfile");
+ ThemeManager.setProfile(%this.idBox, "tipProfile", "TooltipProfile");
+ %this.idBox.AltCommand = %this.getID() @ ".onCommit();";
+ %this.idBox.ReturnCommand = %this.getID() @ ".onCommit();";
+ %this.add(%this.idBox);
+ %x += %idW + %gap;
+
+ %this.colorToggle = %this.makeToggle(%x, "color", "Show color",
+ "", $EditorIcon::brush,
+ "Draws a colored dot in front of the caption, and moves the caption over to make room for it.",
+ "No dot. The caption starts at the edge of the row.");
+ %x += %iconW + %gap;
+
+ %this.swatch = new GuiColorPopupCtrl()
+ {
+ class = "GuiProfileEditorColorPopup";
+ HorizSizing = "left";
+ Position = %x SPC 2;
+ Extent = %swatchW SPC %boxH;
+ showColorValues = true;
+ Tooltip = "The color of the dot.";
+ };
+ ThemeManager.setProfile(%this.swatch, "colorPickerProfile");
+ ThemeManager.setProfile(%this.swatch, "emptyProfile", "backgroundProfile");
+ ThemeManager.setProfile(%this.swatch, "colorPopupProfile", "popupProfile");
+ ThemeManager.setProfile(%this.swatch, "emptyProfile", "pickerProfile");
+ ThemeManager.setProfile(%this.swatch, "colorPickerSelectorProfile", "selectorProfile");
+ ThemeManager.setProfile(%this.swatch, "textEditProfile", "valueProfile");
+ ThemeManager.setProfile(%this.swatch, "tipProfile", "TooltipProfile");
+ %this.swatch.Command = %this.getID() @ ".onCommit();";
+ %this.add(%this.swatch);
+ %x += %swatchW + %gap;
+
+ %this.activeToggle = %this.makeToggle(%x, "active", "Active",
+ $EditorIcon::on, $EditorIcon::off,
+ "Can be picked when the game runs.",
+ "Drawn greyed out and cannot be picked. Use it for a choice that is there but not available yet.");
+ %x += %iconW + %gap;
+
+ %this.selectedToggle = %this.makeToggle(%x, "selected", "Starts selected",
+ $EditorIcon::round_checkmark, $EditorIcon::round,
+ "Already picked when the Gui loads. A drop down shows this row instead of its placeholder text.",
+ "Not picked when the Gui loads.");
+ %x += %iconW + %gap;
+
+ %this.upButton = %this.makeIconButton(%x, $EditorIcon::sq_up,
+ "Move this row up.", ".onMoveUp();");
+ %x += %iconW + %gap;
+
+ %this.downButton = %this.makeIconButton(%x, $EditorIcon::sq_down,
+ "Move this row down.", ".onMoveDown();");
+ %x += %iconW + %gap;
+
+ %this.removeButton = %this.makeIconButton(%x, $EditorIcon::trash,
+ "Remove this row.", ".onRemoveClicked();");
+}
+
+// A checkbox wearing an icon, the same EditorToggleIcon the header's flags
+// use. frameOn is optional: where there is one picture for the idea, the tint
+// alone carries the state.
+function GuiEditorItemRow::makeToggle(%this, %x, %name, %label, %frameOn, %frameOff, %tipOn, %tipOff)
+{
+ %toggle = new GuiCheckBoxCtrl()
+ {
+ class = "EditorToggleIcon";
+ HorizSizing = "left";
+ Position = %x SPC 1;
+ Extent = "24 24";
+ frameOn = %frameOn;
+ frameOff = %frameOff;
+ tipOn = %tipOn;
+ tipOff = %tipOff;
+ toggleName = %name;
+ toggleLabel = %label;
+ owner = %this;
+ };
+ ThemeManager.setProfile(%toggle, "iconButtonProfile");
+ ThemeManager.setProfile(%toggle, "tipProfile", "TooltipProfile");
+ %this.add(%toggle);
+
+ return %toggle;
+}
+
+function GuiEditorItemRow::makeIconButton(%this, %x, %frame, %tip, %command)
+{
+ %button = new GuiButtonCtrl()
+ {
+ class = "EditorIconButton";
+ HorizSizing = "left";
+ Frame = %frame;
+ Position = %x SPC 1;
+ Extent = "24 24";
+ Tooltip = %tip;
+ Command = %this.getID() @ %command;
+ };
+ ThemeManager.setProfile(%button, "iconButtonProfile");
+ %this.add(%button);
+
+ return %button;
+}
+
+//-----------------------------------------------------------------------------
+// Values, in the records GuiListBoxCtrl::getItemList speaks:
+//
+// text ID active selected hasColor "r g b a"
+//-----------------------------------------------------------------------------
+
+function GuiEditorItemRow::setRecord(%this, %record)
+{
+ %this.populating = true;
+
+ %this.captionBox.setText(getField(%record, 0));
+ %this.idBox.setText(getField(%record, 1) + 0);
+ %this.activeToggle.setValue(getField(%record, 2));
+ %this.selectedToggle.setValue(getField(%record, 3));
+
+ %hasColor = getField(%record, 4);
+ %this.colorToggle.setValue(%hasColor);
+
+ // A row showing no dot still needs something in the swatch, or turning the
+ // toggle on would give it whatever was there before. The profile's font color
+ // is the useful default: it is the one color the list is already known to be
+ // legible in, so the first dot lands visible rather than black on black.
+ %color = getField(%record, 5);
+ if(!%hasColor || getWordCount(%color) < 4)
+ {
+ %color = %this.defaultBulletColor();
+ }
+ %this.swatch.setColorF(%color);
+
+ %this.populating = false;
+ %this.refreshColorState();
+
+ %this.lastRecord = %this.getRecord();
+}
+
+function GuiEditorItemRow::getRecord(%this)
+{
+ %hasColor = %this.colorToggle.getValue() ? 1 : 0;
+
+ return %this.captionBox.getText() TAB
+ (%this.idBox.getText() + 0) TAB
+ (%this.activeToggle.getValue() ? 1 : 0) TAB
+ (%this.selectedToggle.getValue() ? 1 : 0) TAB
+ %hasColor TAB
+ %this.swatch.getColorF();
+}
+
+function GuiEditorItemRow::hasChanged(%this)
+{
+ return strcmp(%this.getRecord(), %this.lastRecord) != 0;
+}
+
+function GuiEditorItemRow::markClean(%this)
+{
+ %this.lastRecord = %this.getRecord();
+}
+
+// What a dot starts as before anyone picks a color for it: the color the list
+// draws its captions in, which is the one color the profile guarantees reads
+// against its own background.
+function GuiEditorItemRow::defaultBulletColor(%this)
+{
+ %ctrl = isObject(%this.owner) ? %this.owner.target : "";
+ if(isObject(%ctrl))
+ {
+ %profile = %ctrl.getFieldValue("Profile");
+ if(isObject(%profile))
+ {
+ return %profile.fontColor;
+ }
+ }
+
+ return "1 1 1 1";
+}
+
+// The swatch means nothing while there is no dot to color, so it says so rather
+// than sitting there looking editable.
+function GuiEditorItemRow::refreshColorState(%this)
+{
+ %this.swatch.setActive(%this.colorToggle.getValue());
+}
+
+function GuiEditorItemRow::setCaretHere(%this)
+{
+ %this.captionBox.setFirstResponder();
+}
+
+//-----------------------------------------------------------------------------
+// Reporting. Nothing here writes to the control; the block owns every write, so
+// it stays the only thing that knows the list as a whole.
+//-----------------------------------------------------------------------------
+
+function GuiEditorItemRow::onCaptionTyped(%this)
+{
+ if(%this.populating || !isObject(%this.owner))
+ {
+ return;
+ }
+
+ %this.owner.onItemRowTyped(%this);
+}
+
+function GuiEditorItemRow::onCommit(%this)
+{
+ if(%this.populating || !isObject(%this.owner))
+ {
+ return;
+ }
+
+ %this.owner.onItemRowCommit(%this);
+}
+
+function GuiEditorItemRow::onToggleIconChanged(%this, %toggle)
+{
+ if(%this.populating || !isObject(%this.owner))
+ {
+ return;
+ }
+
+ if(%toggle.toggleName $= "color")
+ {
+ %this.refreshColorState();
+ }
+
+ %this.owner.onItemRowToggled(%this, %toggle.toggleName);
+}
+
+function GuiEditorItemRow::onMoveUp(%this)
+{
+ if(isObject(%this.owner))
+ {
+ %this.owner.onItemRowMove(%this, -1);
+ }
+}
+
+function GuiEditorItemRow::onMoveDown(%this)
+{
+ if(isObject(%this.owner))
+ {
+ %this.owner.onItemRowMove(%this, 1);
+ }
+}
+
+function GuiEditorItemRow::onRemoveClicked(%this)
+{
+ if(isObject(%this.owner))
+ {
+ %this.owner.onItemRowRemove(%this);
+ }
+}
diff --git a/editor/GuiEditor/scripts/GuiEditorItemsBlock.cs b/editor/GuiEditor/scripts/GuiEditorItemsBlock.cs
new file mode 100644
index 000000000..13258174e
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiEditorItemsBlock.cs
@@ -0,0 +1,509 @@
+
+//-----------------------------------------------------------------------------
+// The Items section of the Gui Editor's properties pane: the static rows a list
+// box or a drop down is authored with.
+//
+// Until this existed the rows were script's alone - addItem, from an onWake
+// somewhere away from the Gui that shows them - and a list laid out in the
+// editor was an empty rectangle. A row typed here appears on the canvas as it is
+// typed and is saved with the Gui.
+//
+// The block edits the list as a WHOLE. It reads it in one getItemList and writes
+// it back in one setItemList, and its rows are widgets over the records that
+// call returns. That is not laziness: every gesture on offer - add, remove, move
+// up, move down, retype a caption - shifts what is around the row it touched, so
+// a per-row write would have to know which of them it was, and an undo of one
+// would have to know what the others became. A list is short.
+//
+// It is built ONCE, in GuiEditorInspectorPane::build, and shown or hidden per
+// class - the arrangement GuiEditorDynamicFields uses, and for the reason the
+// pane's header comment gives: a block rebuilt on a selection change can delete
+// a control the engine is mid-dispatch on. The rows inside it are rebuilt, but
+// always from a schedule(0), because the click that removes or moves one arrives
+// from a button inside the row that is about to be freed.
+//
+// Not GuiTreeViewCtrl, though it derives from GuiListBoxCtrl: a tree generates
+// its rows from a root object. See GuiEditorControlSpec::hasItemList.
+//
+// The creator sets pane and blockWidth inline, then calls build() once after
+// adding it.
+//-----------------------------------------------------------------------------
+
+function GuiEditorItemsBlock::onAdd(%this)
+{
+ ThemeManager.setProfile(%this, "emptyProfile");
+}
+
+function GuiEditorItemsBlock::build(%this)
+{
+ %this.typing = false;
+ %this.listBeforeEdit = "";
+ %this.pendingFocus = false;
+
+ // The rows in a grid rather than a chain of their own, which is what every
+ // other section in this pane uses and the only thing that lays out correctly
+ // inside a collapsible panel: a chain sizes itself from its children, so a
+ // chain of full-width rows inside a chain inside a panel had each level
+ // widening the one above it, a bit per layout pass, until the row ran off the
+ // edge of the pane. A grid sizes its children from ITSELF.
+ //
+ // One column, because a row is a line. MaxColCount is the only thing that
+ // says so: left to fit as many columns as it can, the grid would put two
+ // short rows side by side the moment the Properties frame was dragged wide.
+ %this.grid = %this.pane.makeCellGrid(0);
+ %this.grid.MaxColCount = 1;
+ %this.grid.CellSizeY = 26;
+ %this.add(%this.grid);
+
+ %this.buildAddRow();
+}
+
+// A caption box and an Add button, the shape GuiEditorDynamicFields uses. Unlike
+// a dynamic field, a row with an empty caption is a legal row - a separator, or
+// one whose text a script fills in later - so Add appends whatever the box holds
+// and puts the caret in the new row.
+function GuiEditorItemsBlock::buildAddRow(%this)
+{
+ %w = %this.blockWidth;
+ %buttonW = 56;
+
+ %row = new GuiControl()
+ {
+ HorizSizing = "width";
+ Position = "0 0";
+ Extent = %w SPC 30;
+ };
+ ThemeManager.setProfile(%row, "emptyProfile");
+ %this.add(%row);
+ %this.addRow = %row;
+
+ %this.nameBox = new GuiTextEditCtrl()
+ {
+ HorizSizing = "width";
+ Position = "4 4";
+ Extent = (%w - %buttonW - 16) SPC 22;
+ Tooltip = "What a new row should say. It can be left empty and typed in afterwards.";
+ };
+ ThemeManager.setProfile(%this.nameBox, "textEditProfile");
+ ThemeManager.setProfile(%this.nameBox, "tipProfile", "TooltipProfile");
+ %this.nameBox.ReturnCommand = %this.getID() @ ".onAddClicked();";
+ %row.add(%this.nameBox);
+
+ %this.addButton = new GuiButtonCtrl()
+ {
+ HorizSizing = "left";
+ Position = (%w - %buttonW - 4) SPC 4;
+ Extent = %buttonW SPC 22;
+ Text = "Add";
+ Command = %this.getID() @ ".onAddClicked();";
+ };
+ ThemeManager.setProfile(%this.addButton, "buttonProfile");
+ %row.add(%this.addButton);
+}
+
+//-----------------------------------------------------------------------------
+// Binding.
+//-----------------------------------------------------------------------------
+
+function GuiEditorItemsBlock::bind(%this, %ctrl)
+{
+ // A half-typed caption belongs to the control it was typed on. Moving the
+ // selection abandons it; the control already holds every keystroke.
+ %this.typing = false;
+ %this.listBeforeEdit = "";
+
+ %this.target = %ctrl;
+ %this.rebuildRows();
+}
+
+// Re-read what the control holds without rebuilding, where the rows still line
+// up with it. This is the undo and redo path (GuiEditorInspectorWindow::
+// onReplayed): a replay can put back a caption, a switch or the whole order, and
+// only the last of those changes how many rows there are.
+//
+// Rebuilding from here would be re-entrant - refresh() is reached from inside a
+// commit - so a count that no longer matches goes through the same schedule(0)
+// everything else does.
+function GuiEditorItemsBlock::refresh(%this)
+{
+ if(!isObject(%this.target))
+ {
+ return;
+ }
+
+ %list = %this.target.getItemList();
+ %count = (%list $= "") ? 0 : getRecordCount(%list);
+
+ if(%count != %this.grid.getCount())
+ {
+ %this.rebuildDeferred();
+ return;
+ }
+
+ for(%i = 0; %i < %count; %i++)
+ {
+ %this.grid.getObject(%i).setRecord(getRecord(%list, %i));
+ }
+
+ %this.refreshArrows();
+}
+
+function GuiEditorItemsBlock::rebuildRows(%this)
+{
+ %this.grid.deleteObjects();
+
+ if(isObject(%this.target))
+ {
+ %list = %this.target.getItemList();
+ %count = (%list $= "") ? 0 : getRecordCount(%list);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %this.makeRow(getRecord(%list, %i));
+ }
+ }
+
+ %this.refreshArrows();
+}
+
+function GuiEditorItemsBlock::makeRow(%this, %record)
+{
+ %row = new GuiControl()
+ {
+ class = "GuiEditorItemRow";
+ HorizSizing = "width";
+ Position = "0 0";
+ Extent = %this.blockWidth SPC 26;
+ owner = %this;
+ };
+
+ // Added before build(), because the grid sizes a cell the moment it takes
+ // one and the row lays itself out against the width it actually has. The
+ // nominal blockWidth above is only what it starts at.
+ %this.grid.add(%row);
+ %row.build();
+ %row.setRecord(%record);
+
+ return %row;
+}
+
+// The first row cannot go up and the last cannot go down, so those two arrows
+// say so rather than being clickable and doing nothing.
+function GuiEditorItemsBlock::refreshArrows(%this)
+{
+ %count = %this.grid.getCount();
+ for(%i = 0; %i < %count; %i++)
+ {
+ %row = %this.grid.getObject(%i);
+ %row.upButton.setActive(%i > 0);
+ %row.downButton.setActive(%i < (%count - 1));
+ }
+}
+
+// Deferred, always: a remove or a move arrives from a button inside the row that
+// is about to be freed, and an add arrives from a button whose own Command is
+// still running.
+function GuiEditorItemsBlock::rebuildDeferred(%this)
+{
+ %this.schedule(0, "rebuildNow");
+}
+
+// Only the deferred path re-measures the section, never bind(): a bind ends with
+// the pane's own forceLayout, and forceLayout nudges the pane's width by a pixel
+// and back. An expanded GuiPanelCtrl takes the nudge up and does not give it
+// back, so a second one in the same bind left the section a pixel wider than the
+// pane every time a control was selected -- and the row's right-hand icons were
+// what fell off the edge.
+function GuiEditorItemsBlock::rebuildNow(%this)
+{
+ %this.rebuildRows();
+ %this.notifyResized();
+
+ // A row added by the Add button is one the user is about to type into.
+ if(%this.pendingFocus)
+ {
+ %this.pendingFocus = false;
+ %count = %this.grid.getCount();
+ if(%count > 0)
+ {
+ %this.grid.getObject(%count - 1).setCaretHere();
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// The list, as the rows currently spell it.
+//-----------------------------------------------------------------------------
+
+// What the ROWS say, for the three edits whose new value exists only in a widget:
+// a retyped caption, an ID, a flipped switch.
+//
+// Read off the chain rather than an index of its own, so the order the rows are
+// in and the order they are written in cannot disagree.
+function GuiEditorItemsBlock::collect(%this)
+{
+ %list = "";
+ %count = %this.grid.getCount();
+ for(%i = 0; %i < %count; %i++)
+ {
+ %record = %this.grid.getObject(%i).getRecord();
+ %list = (%i == 0) ? %record : (%list NL %record);
+ }
+
+ return %list;
+}
+
+// What the CONTROL says, which is what adding, removing and moving work from.
+//
+// Not collect(), because the rows lag: every one of those three ends in a
+// rebuild that has to be deferred, so between the write and the next tick the
+// chain still shows the list as it was. Two clicks on Add inside one frame read
+// the same stale chain and the first row added was lost. The control is never
+// stale - it was written before the rebuild was even scheduled - and a box the
+// user was typing in has already committed on the way out of it, because taking
+// focus is what AltCommand fires on.
+function GuiEditorItemsBlock::currentList(%this)
+{
+ return isObject(%this.target) ? %this.target.getItemList() : "";
+}
+
+// Whether the chain can still be trusted to say which row is which. False only
+// inside the window a deferred rebuild has not closed yet, where an index into
+// the chain would name the wrong record.
+function GuiEditorItemsBlock::rowsAreCurrent(%this)
+{
+ %list = %this.currentList();
+ %count = (%list $= "") ? 0 : getRecordCount(%list);
+
+ return %count == %this.grid.getCount();
+}
+
+function GuiEditorItemsBlock::indexOf(%this, %row)
+{
+ %count = %this.grid.getCount();
+ for(%i = 0; %i < %count; %i++)
+ {
+ if(%this.grid.getObject(%i) == %row)
+ {
+ return %i;
+ }
+ }
+
+ return -1;
+}
+
+// One write, one undo step. The recorder does the writing, so a write that was
+// not recorded is a write that did not happen.
+function GuiEditorItemsBlock::writeList(%this, %list, %name)
+{
+ if(!isObject(%this.target))
+ {
+ return;
+ }
+
+ GuiEditor.undoRecorder.begin(%name, "");
+ GuiEditor.undoRecorder.writeItems(%this.target, %list);
+ GuiEditor.undoRecorder.end();
+
+ %this.pane.afterCommit();
+}
+
+//-----------------------------------------------------------------------------
+// Editing.
+//-----------------------------------------------------------------------------
+
+// A caption keystroke. Straight onto the control rather than through the
+// recorder, because this runs per character and an edit is one change however
+// many keys it took; the undo step is written once, on commit, from the list
+// stashed here.
+function GuiEditorItemsBlock::onItemRowTyped(%this, %row)
+{
+ if(%this.populating || !isObject(%this.target))
+ {
+ return;
+ }
+
+ // Taken on the first keystroke, because that is the last moment the control
+ // still holds what the edit started from.
+ if(!%this.typing)
+ {
+ %this.typing = true;
+ %this.listBeforeEdit = %this.target.getItemList();
+ }
+
+ %this.target.setItemList(%this.collect());
+ %this.pane.afterCommit();
+}
+
+function GuiEditorItemsBlock::onItemRowCommit(%this, %row)
+{
+ if(%this.populating || !isObject(%this.target))
+ {
+ return;
+ }
+
+ %wasTyping = %this.typing;
+ %before = %this.listBeforeEdit;
+ %this.typing = false;
+ %this.listBeforeEdit = "";
+
+ if(!%row.hasChanged())
+ {
+ return;
+ }
+ %row.markClean();
+
+ // Put back what the edit started from, so the one write below is the whole
+ // of it. Without this the recorder would compare the control against itself
+ // and find nothing to record.
+ if(%wasTyping)
+ {
+ %this.target.setItemList(%before);
+ }
+
+ %this.writeList(%this.collect(), "Edit Item");
+}
+
+function GuiEditorItemsBlock::onItemRowToggled(%this, %row, %name)
+{
+ if(%this.populating || !isObject(%this.target) || !%this.rowsAreCurrent())
+ {
+ return;
+ }
+
+ %row.markClean();
+
+ %list = %this.collect();
+
+ // Only one row can start selected on a list that only allows one selection.
+ // The engine's setItemList restores exactly what it is given - it has to, or
+ // an undo could not put a multi-selection back - so the rule belongs here.
+ if(%name $= "selected" && %row.selectedToggle.getValue() &&
+ !%this.target.getFieldValue("AllowMultipleSelections"))
+ {
+ %list = %this.clearOtherSelections(%list, %this.indexOf(%row));
+ }
+
+ %this.writeList(%list, "Change Item");
+
+ // The rows have to catch up with a selection that was taken off them.
+ if(%name $= "selected")
+ {
+ %this.rebuildDeferred();
+ }
+}
+
+function GuiEditorItemsBlock::clearOtherSelections(%this, %list, %keepIndex)
+{
+ %out = "";
+ %count = getRecordCount(%list);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %record = getRecord(%list, %i);
+ if(%i != %keepIndex)
+ {
+ %record = setField(%record, 3, 0);
+ }
+ %out = (%i == 0) ? %record : (%out NL %record);
+ }
+
+ return %out;
+}
+
+function GuiEditorItemsBlock::onItemRowMove(%this, %row, %delta)
+{
+ if(!isObject(%this.target) || !%this.rowsAreCurrent())
+ {
+ return;
+ }
+
+ %index = %this.indexOf(%row);
+ %swapWith = %index + %delta;
+ if(%index == -1 || %swapWith < 0 || %swapWith >= %this.grid.getCount())
+ {
+ return;
+ }
+
+ %list = %this.currentList();
+ %out = "";
+ %count = getRecordCount(%list);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %pick = %i;
+ if(%i == %index)
+ {
+ %pick = %swapWith;
+ }
+ else if(%i == %swapWith)
+ {
+ %pick = %index;
+ }
+
+ %record = getRecord(%list, %pick);
+ %out = (%i == 0) ? %record : (%out NL %record);
+ }
+
+ %this.writeList(%out, "Move Item");
+ %this.rebuildDeferred();
+}
+
+function GuiEditorItemsBlock::onItemRowRemove(%this, %row)
+{
+ if(!isObject(%this.target) || !%this.rowsAreCurrent())
+ {
+ return;
+ }
+
+ %index = %this.indexOf(%row);
+ if(%index == -1)
+ {
+ return;
+ }
+
+ %list = %this.currentList();
+ %out = "";
+ %count = getRecordCount(%list);
+ for(%i = 0; %i < %count; %i++)
+ {
+ if(%i == %index)
+ {
+ continue;
+ }
+
+ %record = getRecord(%list, %i);
+ %out = (%out $= "") ? %record : (%out NL %record);
+ }
+
+ %this.writeList(%out, "Remove Item");
+ %this.rebuildDeferred();
+}
+
+function GuiEditorItemsBlock::onAddClicked(%this)
+{
+ if(!isObject(%this.target))
+ {
+ return;
+ }
+
+ // The defaults an LBItem is born with: no ID, active, unselected, and no
+ // color dot.
+ %record = trim(%this.nameBox.getText()) TAB "0" TAB "1" TAB "0" TAB "0" TAB "1 1 1 1";
+
+ %list = %this.currentList();
+ %list = (%list $= "") ? %record : (%list NL %record);
+
+ %this.nameBox.setText("");
+ %this.writeList(%list, "Add Item");
+
+ %this.pendingFocus = true;
+ %this.rebuildDeferred();
+}
+
+// A row was added or taken away, so the section's height changed under the
+// chain. Nothing above it moved, but the panel has to be told to re-measure.
+function GuiEditorItemsBlock::notifyResized(%this)
+{
+ if(isObject(%this.pane))
+ {
+ %this.pane.onItemsChanged();
+ }
+}
diff --git a/editor/GuiEditor/scripts/GuiEditorMenuItemBlock.cs b/editor/GuiEditor/scripts/GuiEditorMenuItemBlock.cs
new file mode 100644
index 000000000..45a8e317d
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiEditorMenuItemBlock.cs
@@ -0,0 +1,315 @@
+
+//-----------------------------------------------------------------------------
+// Everything about a menu item, in the header.
+//
+// A GuiMenuItemCtrl is unlike anything else in the palette: it calls
+// SimObject::initPersistFields rather than GuiControl's, so it has no profile,
+// no geometry, no tooltip and no sizing - and then registers a handful of
+// fields of its own. What is left is a header with a caption in it and nothing
+// underneath, which is why those fields live up here rather than in a section
+// of their own. There is nothing to scroll past to reach them.
+//
+// It owns its own caption box for the same reason. The shared GuiEditorTextBlock
+// is a multi-line box with wrap, extend, alignment and font rows attached - all
+// of which a menu item hides - and a menu caption is one short line that decides
+// how wide the menu is. A box three lines tall for a word like "File" says the
+// wrong thing about what belongs there.
+//
+// The block owns its widgets and no values: every row reports here and this
+// forwards to the pane, so the pane stays the only thing that writes to the
+// control. The creator sets pane, spec and blockWidth inline, then calls build()
+// once after adding it.
+//-----------------------------------------------------------------------------
+
+function GuiEditorMenuItemBlock::onAdd(%this)
+{
+ ThemeManager.setProfile(%this, "emptyProfile");
+}
+
+function GuiEditorMenuItemBlock::build(%this)
+{
+ %this.typing = false;
+
+ %this.grid = %this.pane.makeCellGrid(0, %this.pane.rowWidth);
+ %this.add(%this.grid);
+
+ %this.buildCaption();
+ %this.buildKind();
+ %this.buildCommands();
+
+ // Visible and Active are not here. They are GuiControl's names, and a menu
+ // item registers them again for itself, so the header's own toggle row can
+ // show them - see GuiEditorControlSpec::stateToggles. A second pair of
+ // switches a few pixels below the first would be worse than no switches.
+}
+
+//-----------------------------------------------------------------------------
+// The caption. One line, because that is what a menu label is - and because for
+// a top-level menu it is also the width of the menu, which the bar recomputes
+// from the text on every keystroke.
+//-----------------------------------------------------------------------------
+
+function GuiEditorMenuItemBlock::buildCaption(%this)
+{
+ %row = %this.pane.makeFieldRow(%this.grid, "text", "Caption", "text", "");
+ %this.textRow = %row;
+
+ // Command is free on a field row -- it commits on AltCommand (blur) and
+ // ReturnCommand -- and GuiTextEditCtrl runs Command on every edit to its
+ // buffer. So this is the per-keystroke hook, and it is what makes the menu on
+ // the canvas grow and shrink under the cursor as the caption is typed.
+ %row.editor.Command = %this.getID() @ ".onCaptionTyped();";
+}
+
+// Straight onto the control rather than through the pane's writeField: this runs
+// per character, and an edit is one change however many keys it took. The undo
+// step is written once, on commit, from the value stashed here.
+function GuiEditorMenuItemBlock::onCaptionTyped(%this)
+{
+ if(%this.populating || !isObject(%this.pane.target))
+ {
+ return;
+ }
+
+ %ctrl = %this.pane.target;
+
+ // Taken on the first keystroke, because that is the last moment the control
+ // still holds what the edit started from.
+ if(!%this.typing)
+ {
+ %this.typing = true;
+ %this.typingTarget = %ctrl;
+ %this.textBeforeEdit = %ctrl.getFieldValue("text");
+ }
+
+ %ctrl.text = %this.textRow.getValue();
+
+ // Cheap -- the explorer tree refreshes the one row's label -- and it keeps
+ // the tree reading the same as the canvas while you type.
+ %this.pane.afterCommit();
+}
+
+function GuiEditorMenuItemBlock::endTyping(%this)
+{
+ %this.typing = false;
+ %this.typingTarget = "";
+ %this.textBeforeEdit = "";
+}
+
+//-----------------------------------------------------------------------------
+// What picking the item does. Toggle and Radio are two bool fields but one
+// decision -- a menu item is a plain command, a checkable one, or one of a set
+// -- so they are one control here, and each writes both fields.
+//-----------------------------------------------------------------------------
+
+function GuiEditorMenuItemBlock::buildKind(%this)
+{
+ %row = new GuiControl()
+ {
+ class = "EditorChoiceRow";
+ labelText = "Kind";
+ labelWidth = 76;
+ fieldName = "kind";
+ owner = %this;
+ };
+
+ %row.addChoice("command", $EditorIcon::list_bullets,
+ "Runs its command and closes the menu. The ordinary kind.");
+ %row.addChoice("toggle", $EditorIcon::checkbox_checked,
+ "Carries a tick that turns on and off, like a Show Grid switch.");
+ %row.addChoice("radio", $EditorIcon::round,
+ "One of a run of items where only one is on at a time. The run is the items either side of it, so keep them together.");
+ %row.addChoice("spacer", $EditorIcon::round_minus,
+ "A rule across the menu, to group what is above it apart from what is below. It is not pickable and runs no command. Written as a single dash in the caption, which is what makes one in a .gui file too.");
+
+ %this.grid.add(%row);
+ %row.build();
+ %this.kindRow = %row;
+
+ %this.onRow = %this.pane.makeFieldRow(%this.grid, "IsOn", "Starts On", "bool", "");
+}
+
+// The chooser changed. Two fields, so one transaction: the C++ setters each
+// rewrite mDisplayType, and the one being turned OFF has to go first or it
+// undoes the one being turned on.
+function GuiEditorMenuItemBlock::onChoiceRowChanged(%this, %row)
+{
+ if(%this.populating || !isObject(%this.pane.target))
+ {
+ return;
+ }
+
+ %kind = %row.getValue();
+ %wasSpacer = (%this.pane.target.getFieldValue("text") $= "-");
+
+ GuiEditor.undoRecorder.begin("Menu Item Kind", "");
+ if(%kind $= "toggle")
+ {
+ %this.pane.writeField("Radio", false);
+ %this.pane.writeField("Toggle", true);
+ }
+ else if(%kind $= "radio")
+ {
+ %this.pane.writeField("Toggle", false);
+ %this.pane.writeField("Radio", true);
+ }
+ else
+ {
+ %this.pane.writeField("Toggle", false);
+ %this.pane.writeField("Radio", false);
+ }
+
+ // A separator has no field of its own: a single dash in the caption is the
+ // whole of it, in a .gui file and here (GuiMenuItemCtrl::setText). So the
+ // kind is written by writing the caption, and leaving it means clearing the
+ // dash - otherwise picking Command off a separator would leave a separator.
+ if(%kind $= "spacer")
+ {
+ %this.pane.writeField("text", "-");
+ }
+ else if(%wasSpacer)
+ {
+ %this.pane.writeField("text", "");
+ }
+ GuiEditor.undoRecorder.end();
+
+ %this.refreshKind();
+ %this.pane.afterCommit();
+}
+
+// What the rest of the block has to say depends on the kind. "Starts On" only
+// means something where there is a mark to start on, and a separator is not
+// pickable at all - so it runs nothing, answers to nothing, and is not named.
+function GuiEditorMenuItemBlock::refreshKind(%this)
+{
+ %kind = %this.kindRow.getValue();
+ %spacer = (%kind $= "spacer");
+
+ %this.onRow.setVisible(!%spacer && %kind !$= "command");
+
+ %this.textRow.setVisible(!%spacer);
+ %this.commandRow.setVisible(!%spacer);
+ %this.altCommandRow.setVisible(!%spacer);
+ %this.acceleratorRow.setVisible(!%spacer);
+ %this.variableRow.setVisible(!%spacer);
+
+ %this.resizeToFit();
+}
+
+//-----------------------------------------------------------------------------
+// What picking it runs.
+//-----------------------------------------------------------------------------
+
+function GuiEditorMenuItemBlock::buildCommands(%this)
+{
+ %this.commandRow = %this.pane.makeFieldRow(%this.grid, "Command", "Command", "text", "");
+ %this.altCommandRow = %this.pane.makeFieldRow(%this.grid, "AltCommand", "Alt Command", "text", "");
+ %this.acceleratorRow = %this.pane.makeFieldRow(%this.grid, "Accelerator", "Shortcut", "text", "");
+ %this.variableRow = %this.pane.makeFieldRow(%this.grid, "Variable", "Variable", "text", "");
+}
+
+//-----------------------------------------------------------------------------
+// Loading and writing.
+//-----------------------------------------------------------------------------
+
+function GuiEditorMenuItemBlock::bind(%this, %ctrl)
+{
+ %this.populating = true;
+
+ %this.textRow.setValue(%ctrl.getFieldValue("text"));
+ %this.commandRow.setValue(%ctrl.getFieldValue("Command"));
+ %this.altCommandRow.setValue(%ctrl.getFieldValue("AltCommand"));
+ %this.acceleratorRow.setValue(%ctrl.getFieldValue("Accelerator"));
+ %this.variableRow.setValue(%ctrl.getFieldValue("Variable"));
+ %this.onRow.setValue(%ctrl.getFieldValue("IsOn"));
+
+ // Only inside a menu. A rule across the menu BAR would have nothing either
+ // side of it to separate, so the engine does not make one there
+ // (GuiMenuItemCtrl::setText) and this must not offer it either.
+ %parent = %ctrl.getParent();
+ %inMenu = isObject(%parent) && %parent.isMemberOfClass("GuiMenuItemCtrl");
+ %this.kindRow.setChoiceVisible("spacer", %inMenu);
+
+ // The caption is asked first, because a dash outranks the other two: an item
+ // carrying one is a separator whatever Toggle and Radio happen to hold.
+ %kind = "command";
+ if(%inMenu && %ctrl.getFieldValue("text") $= "-")
+ {
+ %kind = "spacer";
+ }
+ else if(%ctrl.getFieldValue("Toggle"))
+ {
+ %kind = "toggle";
+ }
+ else if(%ctrl.getFieldValue("Radio"))
+ {
+ %kind = "radio";
+ }
+ %this.kindRow.setValue(%kind);
+
+ %this.populating = false;
+
+ %this.refreshKind();
+}
+
+// Every row in this block reports here. The caption is the one that has already
+// written itself, per keystroke, so it puts the old value back and writes it
+// once - which is what makes an edit one step on the undo stack however many
+// keys it took.
+function GuiEditorMenuItemBlock::onFieldRowCommit(%this, %row)
+{
+ if(%this.populating || !isObject(%this.pane.target))
+ {
+ return;
+ }
+
+ %field = %row.fieldName;
+
+ if(%field $= "text")
+ {
+ %value = %row.getValue();
+ %before = %this.textBeforeEdit;
+ %wasTyping = %this.typing;
+ %this.endTyping();
+ %row.markClean();
+
+ if(!%wasTyping)
+ {
+ return;
+ }
+
+ %this.pane.target.text = %before;
+ %this.pane.writeField("text", %value);
+
+ // Typing a dash is the documented way to make a separator, so the chooser
+ // has to notice one arriving that way as well as being picked.
+ %this.bind(%this.pane.target);
+ %this.pane.afterCommit();
+ return;
+ }
+
+ if(!%row.hasChanged())
+ {
+ return;
+ }
+
+ %row.markClean();
+ %this.pane.writeField(%field, %row.getValue());
+ %this.pane.afterCommit();
+}
+
+// Nothing here has a reset button, so this is only ever the row asking politely.
+function GuiEditorMenuItemBlock::onFieldRowReset(%this, %row)
+{
+}
+
+// Nudge the width and put it back, which is one parentResized through every
+// child -- the same trick the header block and the pane use to re-measure a
+// chain after something in it was shown or hidden.
+function GuiEditorMenuItemBlock::resizeToFit(%this)
+{
+ %w = getWord(%this.getExtent(), 0);
+ %h = getWord(%this.getExtent(), 1);
+ %this.resize(0, 0, %w + 1, %h);
+ %this.resize(0, 0, %w, %h);
+}
diff --git a/editor/GuiEditor/scripts/GuiEditorMenus.cs b/editor/GuiEditor/scripts/GuiEditorMenus.cs
new file mode 100644
index 000000000..5f4f316de
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiEditorMenus.cs
@@ -0,0 +1,133 @@
+//-----------------------------------------------------------------------------
+// The Gui Editor's four menus: File, Edit, Layout and Select.
+//
+// They used to be written into the shared bar in EditorCore, greyed in when this
+// editor opened and greyed out when it closed. They live here now because they
+// were never shared - every command in them names GuiEditor - and because the
+// Asset Manager wanted a File and an Edit of its own that mean something else.
+// EditorCore swaps whole sets in and out; see EditorMenuSet.
+//
+// The greying divides in two. Revert, Undo, Redo and Paste each answer their own
+// question and are held by name. Everything in Layout and Select answers the
+// same one - how much is selected - so those thirteen, five, two and two items
+// are groups, and refreshSelection flips them with four calls instead of
+// twenty-two.
+//-----------------------------------------------------------------------------
+
+function GuiEditorMenus::onAdd(%this)
+{
+ %this.init();
+}
+
+function GuiEditorMenus::build(%this)
+{
+ %file = %this.addMenu("File");
+ %file.addItem("New Gui", "GuiEditor.NewGui();", "Ctrl N");
+ %file.addItem("Open Gui...", "GuiEditor.OpenGui();", "Ctrl O");
+ %file.addSeparator();
+ %file.addItem("Save Gui...", "GuiEditor.SaveGui();", "Ctrl S");
+ %file.addItem("Save Gui As...", "GuiEditor.SaveGuiAs();", "Ctrl-Shift S");
+ %file.addSeparator();
+
+ // Offered only once the Gui has a file to go back to; refreshFile keeps that
+ // up to date. No accelerator - it throws away everything since the last save,
+ // and that is not a thing to have a shortcut for.
+ %this.revert = %file.addItem("Revert", "GuiEditor.Revert();");
+
+ %edit = %this.addMenu("Edit");
+ %this.undo = %edit.addItem("Undo", "GuiEditor.Undo();", "Ctrl Z");
+ %this.redo = %edit.addItem("Redo", "GuiEditor.Redo();", "Ctrl-Shift Z");
+ %edit.addSeparator();
+ %edit.addItem("Cut", "GuiEditor.Cut();", "Ctrl X", "selection");
+ %edit.addItem("Copy", "GuiEditor.Copy();", "Ctrl C", "selection");
+ %this.paste = %edit.addItem("Paste", "GuiEditor.Paste();", "Ctrl V");
+ %edit.addItem("Duplicate", "GuiEditor.Duplicate();", "Ctrl D", "selection");
+ %edit.addSeparator();
+
+ // DeleteSelection, not Delete: delete is a console method on every SimObject,
+ // so GuiEditor.Delete() would quietly destroy the editor rather than the
+ // selection.
+ //
+ // The accelerator cannot double-fire with the Delete key the canvas and the
+ // Explorer tree already handle themselves. The canvas consults accelerators
+ // only once the first responder has passed on the key -- the same thing that
+ // lets a text box in the properties pane keep Ctrl+C. What it adds is Delete
+ // working while focus is in a tool window.
+ %edit.addItem("Delete", "GuiEditor.DeleteSelection();", "Delete", "selection");
+
+ %layout = %this.addMenu("Layout");
+ %layout.addItem("Nudge Up", "GuiEditor.brain.moveSelection(0,-1);", "Up", "selection");
+ %layout.addItem("Nudge Down", "GuiEditor.brain.moveSelection(0,1);", "Down", "selection");
+ %layout.addItem("Nudge Left", "GuiEditor.brain.moveSelection(-1,0);", "Left", "selection");
+ %layout.addItem("Nudge Right", "GuiEditor.brain.moveSelection(1,0);", "Right", "selection");
+ %layout.addSeparator();
+ %layout.addItem("Shrink Height", "GuiEditor.changeExtent(0,-1);", "Ctrl Up", "selection");
+ %layout.addItem("Expand Height", "GuiEditor.changeExtent(0, 1);", "Ctrl Down", "selection");
+ %layout.addItem("Shrink Width", "GuiEditor.changeExtent(-1,0);", "Ctrl Left", "selection");
+ %layout.addItem("Expand Width", "GuiEditor.changeExtent(1,0);", "Ctrl Right", "selection");
+ %layout.addSeparator();
+ %layout.addItem("Align Top", "GuiEditor.Justify(3);", "Ctrl T", "align");
+ %layout.addItem("Align Bottom", "GuiEditor.Justify(4);", "Ctrl B", "align");
+ %layout.addItem("Align Left", "GuiEditor.Justify(0);", "Ctrl L", "align");
+ %layout.addItem("Align Right", "GuiEditor.Justify(2);", "Ctrl R", "align");
+ %layout.addSeparator();
+ %layout.addItem("Center Horizontally", "GuiEditor.Justify(1);", "", "align");
+ %layout.addItem("Space Vertically", "GuiEditor.Justify(5);", "", "space");
+ %layout.addItem("Space Horizontally", "GuiEditor.Justify(6);", "", "space");
+ %layout.addSeparator();
+ %layout.addItem("Bring to Front", "GuiEditor.BringToFront();", "Ctrl-Shift Up", "restack");
+ %layout.addItem("Push to Back", "GuiEditor.PushToBack();", "Ctrl-Shift Down", "restack");
+ %layout.addSeparator();
+ %layout.addItem("Set Grid Size...", "GuiEditor.SetGridSize();", "Ctrl-Shift G");
+ %layout.addToggle("Snap to Grid", "GuiEditor.SnapToGrid(true);", "GuiEditor.SnapToGrid(false);", "Ctrl G", true);
+
+ %select = %this.addMenu("Select");
+ %select.addItem("Select All", "GuiEditor.brain.SelectAll();", "Ctrl A");
+
+ // Ctrl-Shift A rather than Ctrl D, which Duplicate has: this is what deselect
+ // is bound to nearly everywhere else, and it pairs with Ctrl A above.
+ %select.addItem("Deselect", "GuiEditor.brain.clearSelection();", "Ctrl-Shift A", "selection");
+}
+
+//-----------------------------------------------------------------------------
+// Greying. Each of these is called by whoever owns the answer; refresh is what
+// EditorCore calls when the set goes back on the bar, so the menus look new
+// every time the editor is opened rather than carrying the state they had when
+// it was last closed.
+//-----------------------------------------------------------------------------
+
+function GuiEditorMenus::refresh(%this)
+{
+ %this.refreshFile();
+ %this.tool.undoRecorder.refreshMenu();
+ %this.tool.clipboard.refreshMenu();
+ %this.tool.brain.toggleMenuItems();
+}
+
+// Revert is the only File item whose offer changes, and what it turns on is
+// whether the document has a file to go back to.
+function GuiEditorMenus::refreshFile(%this)
+{
+ %this.revert.setActive(%this.tool.filePath !$= "");
+}
+
+function GuiEditorMenus::refreshUndo(%this, %undoCount, %redoCount)
+{
+ %this.undo.setActive(%undoCount > 0);
+ %this.redo.setActive(%redoCount > 0);
+}
+
+function GuiEditorMenus::refreshPaste(%this, %hasCopy)
+{
+ %this.paste.setActive(%hasCopy);
+}
+
+// Everything in Layout and Select, plus the half of Edit that acts on controls.
+// The thresholds are here rather than at the call site because the groups are.
+function GuiEditorMenus::refreshSelection(%this, %count)
+{
+ %this.setGroupActive("selection", %count != 0);
+ %this.setGroupActive("align", %count > 1);
+ %this.setGroupActive("space", %count > 2);
+ %this.setGroupActive("restack", %count == 1);
+}
diff --git a/editor/GuiEditor/scripts/GuiEditorSaveGuiDialog.cs b/editor/GuiEditor/scripts/GuiEditorSaveGuiDialog.cs
index 456ea95e7..bc19ba38a 100644
--- a/editor/GuiEditor/scripts/GuiEditorSaveGuiDialog.cs
+++ b/editor/GuiEditor/scripts/GuiEditorSaveGuiDialog.cs
@@ -100,9 +100,11 @@ class = "EditorForm";
%this.validate();
}
+// Grey the Save button until the form can be saved, and say why in the feedback
+// line either way. Every return below is a reason not to.
function GuiEditorSaveGuiDialog::Validate(%this)
{
- %this.createButton.active = false;
+ %this.saveButton.setActive(false);
%folderPath = %this.getFolderPath();
%guiName = %this.guiNameBox.getText();
@@ -156,16 +158,36 @@ class = "EditorForm";
}
if(isFile(%filePath))
{
- %this.createButton.active = true;
- %this.feedback.setText("A file by this name already exists. It will be overwritten.");
+ %this.saveButton.setActive(true);
+ %this.feedback.setText(%this.withFormatWarning(
+ "A file by this name already exists. It will be overwritten."));
return true;
}
- %this.createButton.active = true;
- %this.feedback.setText("A new Gui file will be created!");
+ %this.saveButton.setActive(true);
+ %this.feedback.setText(%this.withFormatWarning("A new Gui file will be created!"));
return true;
}
+// The .gui script format writes fields and child objects and nothing else, so
+// anything a control keeps as TAML custom nodes goes missing. A warning rather
+// than a refusal: the format is still the right answer for a Gui that holds none
+// of it, and which of the two to save in is the user's call. Saying which
+// controls are affected is what makes it actionable.
+function GuiEditorSaveGuiDialog::withFormatWarning(%this, %text)
+{
+ if(%this.guiFormatDropDown.getSelectedItem() != 0)
+ {
+ return %text;
+ }
+
+ %warning = GuiEditor.tamlOnlyStateSummary();
+
+ // "\n\n" rather than NL NL: NL is a binary operator, so two of them in a row
+ // have nothing between them and will not parse.
+ return (%warning $= "") ? %text : (%text @ "\n\n" @ %warning);
+}
+
function GuiEditorSaveGuiDialog::onSave(%this)
{
if(%this.validate())
@@ -178,6 +200,23 @@ class = "EditorForm";
}
}
+// The Cancel button and the window X both land here, and so does onSave once the
+// file is written. Only the first two mean anything was called off, and by the
+// time the third arrives SaveCore has already released what it was holding - so
+// dropping it here is a no-op on that path and the answer on the other two.
+//
+// This is the far end of the unsaved-changes prompt's Save button: the user said
+// save-then-carry-on, changed their mind about the file name, and must not find
+// the editor carrying on anyway with nothing written.
+function GuiEditorSaveGuiDialog::onClose(%this)
+{
+ GuiEditor.dropPendingCommand();
+
+ Canvas.popDialog(%this);
+ EditorCore.dialog = %this;
+ EditorCore.schedule(100, "deleteDialog");
+}
+
function GuiEditorSaveGuiDialog::onFolderOpened(%this, %textBox)
{
%this.Validate();
diff --git a/editor/GuiEditor/scripts/GuiEditorTextBlock.cs b/editor/GuiEditor/scripts/GuiEditorTextBlock.cs
new file mode 100644
index 000000000..da087a536
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiEditorTextBlock.cs
@@ -0,0 +1,456 @@
+
+//-----------------------------------------------------------------------------
+// Everything GuiControl::renderText reads, in one component.
+//
+// These seven fields used to be nine rows in a shared "Text" section that the
+// pane hid wholesale whenever the header carried the text box -- and the header
+// carried three of them. The other five had nowhere to go, which is how a
+// GuiControl ended up with a caption it could not resize. Putting them in one
+// component makes that impossible to repeat: the block is either shown or it is
+// not, and it is the only place any of these fields live.
+//
+// caption row the block's heading, and the two flags that change what the
+// text does to the control it sits in. Wrap first, extend
+// second; extend stays live either way, because
+// guiControl.cc grows the width when wrap is off and the
+// height when it is on.
+// text box a GuiTextEditCtrl with textWrap on, which is what makes it
+// multi-line: it wraps, scrolls and hit-tests in line space.
+// Three lines tall, because one line of a heading is a poor
+// view of a paragraph.
+// align grid the two alignments, as segmented rows labelled "H:" and
+// "V:" -- one decision each, and the icons say the rest.
+// font grid size and color, the pair anyone reaching for "this text is
+// too small" wants.
+//
+// Both grids are cell grids, so they sit side by side in a wide Properties
+// frame and stack in a narrow one.
+//
+// Two of these exist. The header holds one, for the classes whose text is a
+// principal property; the Text section holds the other, for the classes that
+// can draw text but are not asked to (a grid) and the one that draws none but
+// still sizes it (a slider). Two cheap instances rather than reparenting a live
+// control out from under a selection event -- see
+// GuiEditorControlSpec::textBlockHome.
+//
+// Who owns what: the block owns its layout and decides which of its parts a
+// class can use. It owns no values. Its three field rows report straight to the
+// pane, and its choice rows and toggles are forwarded there, so the pane stays
+// the only thing that writes to the control.
+//
+// The creator sets pane, spec and blockWidth inline, then calls build() once
+// after adding it.
+//-----------------------------------------------------------------------------
+
+function GuiEditorTextBlock::onAdd(%this)
+{
+ ThemeManager.setProfile(%this, "emptyProfile");
+}
+
+function GuiEditorTextBlock::build(%this)
+{
+ %this.buildCaption();
+ %this.buildText();
+ %this.buildAlign();
+ %this.buildFont();
+}
+
+//-----------------------------------------------------------------------------
+// The caption line: what this block is about, and the two flags that belong
+// with it rather than with the alignments.
+//-----------------------------------------------------------------------------
+
+function GuiEditorTextBlock::buildCaption(%this)
+{
+ %w = %this.blockWidth;
+ %size = 24;
+
+ %row = new GuiControl()
+ {
+ HorizSizing = "width";
+ Position = "0 0";
+ Extent = %w SPC (%size + 2);
+ };
+ ThemeManager.setProfile(%row, "emptyProfile");
+ %this.add(%row);
+ %this.captionRow = %row;
+
+ // The label stops short of the two icons and keeps that gap as the pane
+ // widens; the icons keep their distance from the right edge.
+ %this.label = new GuiControl()
+ {
+ HorizSizing = "width";
+ Position = "4 4";
+ Extent = (%w - 8 - ((%size + 2) * 2)) SPC 16;
+ Text = "Text";
+ align = "left";
+ vAlign = "middle";
+ };
+ ThemeManager.setProfile(%this.label, "labelProfile");
+ %row.add(%this.label);
+
+ %this.wrapButton = %this.makeIcon(%row, %w - ((%size + 2) * 2), %size, "textWrap",
+ "Wrap Text", $EditorIcon::align_just,
+ "Wraps onto as many lines as it needs, breaking between words. Return puts a line break in while you are typing here.",
+ "Draws on one line whatever the control's width, and anything past the edge is clipped. Return finishes the edit.");
+
+ %this.extendButton = %this.makeIcon(%row, %w - (%size + 2), %size, "textExtend",
+ "Extend To Fit Text", $EditorIcon::expand, "", "");
+ %this.applyExtendTip(false);
+}
+
+// One icon in the caption row, pinned to the right edge as the pane widens.
+function GuiEditorTextBlock::makeIcon(%this, %row, %x, %size, %field, %label, %frame, %tipOn, %tipOff)
+{
+ %button = new GuiCheckBoxCtrl()
+ {
+ class = "EditorToggleIcon";
+ HorizSizing = "left";
+ Position = %x SPC 1;
+ Extent = %size SPC %size;
+ frameOn = %frame;
+ frameOff = %frame;
+ tipOn = %tipOn;
+ tipOff = %tipOff;
+ toggleName = %field;
+ toggleLabel = %label;
+ owner = %this;
+ };
+ ThemeManager.setProfile(%button, "iconButtonProfile");
+ ThemeManager.setProfile(%button, "tipProfile", "TooltipProfile");
+ %row.add(%button);
+ return %button;
+}
+
+// Extend does something different depending on wrap, so its tooltip has to say
+// which -- it is the only way to tell from the pane that the same flag grows
+// two different axes (guiControl.cc renderText: extent.y when wrapping, extent.x
+// when not).
+function GuiEditorTextBlock::applyExtendTip(%this, %wrapping)
+{
+ %tip = %wrapping
+ ? "Grows taller to fit however many lines the text wraps onto, so nothing is clipped."
+ : "Grows wider to fit the text on its one line, so nothing is clipped.";
+
+ %this.extendButton.tipOn = %tip;
+ %this.extendButton.tipOff = %tip;
+ %this.extendButton.refresh();
+}
+
+//-----------------------------------------------------------------------------
+// The text box itself, as a field row of the pane's own kind so that loading,
+// committing and the changed check are the ones every other field gets.
+//-----------------------------------------------------------------------------
+
+function GuiEditorTextBlock::buildText(%this)
+{
+ // No caption of its own: the caption row above is its label, which is what
+ // leaves room for the two icons beside it. An empty labelText is how a field
+ // row is told to keep no space for one.
+ %row = %this.pane.makeFieldRow(%this, "text", "", "multiline", "");
+ %this.row["text"] = %row;
+ %this.textRow = %row;
+
+ // Command is free on a field row -- it commits on AltCommand (blur) and
+ // ReturnCommand -- and GuiTextEditCtrl runs Command on every edit to its
+ // buffer: a character, a backspace, a delete, a paste, an undo. So this is
+ // the per-keystroke hook, and it is what lets the control on the canvas fill
+ // in as you type.
+ %row.editor.Command = %this.getID() @ ".onTextTyped();";
+}
+
+//-----------------------------------------------------------------------------
+// Typing. The control is updated on every keystroke, but the edit is still one
+// edit: the text the control held when the first key landed is kept here, and
+// the pane writes the change once, properly, when the box loses focus.
+//-----------------------------------------------------------------------------
+
+function GuiEditorTextBlock::onTextTyped(%this)
+{
+ if(%this.isPopulating() || !isObject(%this.pane.target))
+ {
+ return;
+ }
+
+ %ctrl = %this.pane.target;
+
+ // Taken on the first keystroke, because that is the last moment the control
+ // still holds what the edit started from.
+ if(!%this.typing)
+ {
+ %this.typing = true;
+ %this.typingTarget = %ctrl;
+ %this.textBeforeEdit = %ctrl.getFieldValue("text");
+ }
+
+ // Straight onto the control rather than through the pane's writeField: this
+ // runs per character, and an edit is one change however many keys it took.
+ %ctrl.text = %this.textRow.getValue();
+
+ // Cheap -- the explorer tree refreshes the one row's label -- and it keeps
+ // the tree reading the same as the canvas while you type.
+ %this.pane.afterCommit();
+}
+
+function GuiEditorTextBlock::endTyping(%this)
+{
+ %this.typing = false;
+ %this.typingTarget = "";
+ %this.textBeforeEdit = "";
+}
+
+//-----------------------------------------------------------------------------
+// Alignment. Two segmented rows with the narrowest labels the row supports --
+// eight buttons and two captions have to fit a Properties frame someone has
+// dragged narrow.
+//-----------------------------------------------------------------------------
+
+function GuiEditorTextBlock::buildAlign(%this)
+{
+ %grid = %this.makeGrid(128, 30);
+ %this.add(%grid);
+ %this.alignGrid = %grid;
+
+ %this.alignRow = %this.makeChoiceRow(%grid, "align", "H:",
+ "left" TAB $EditorIcon::align_left TAB "Align text to the left" NL
+ "center" TAB $EditorIcon::align_center TAB "Centre text" NL
+ "right" TAB $EditorIcon::align_right TAB "Align text to the right");
+
+ %this.vAlignRow = %this.makeChoiceRow(%grid, "vAlign", "V:",
+ "top" TAB $EditorIcon::align_top TAB "Align text to the top" NL
+ "middle" TAB $EditorIcon::align_middle TAB "Centre text vertically" NL
+ "bottom" TAB $EditorIcon::align_bottom TAB "Align text to the bottom");
+}
+
+// %choices is one record per value: value TAB icon TAB tooltip. "default" leads
+// every row and wears no icon -- it is not an absence but the value a control
+// starts on, which getAlignmentType resolves to the profile's own alignment.
+function GuiEditorTextBlock::makeChoiceRow(%this, %grid, %field, %label, %choices)
+{
+ %row = new GuiControl()
+ {
+ class = "EditorChoiceRow";
+ Position = "0 0";
+ labelText = %label;
+ labelWidth = 24;
+ fieldName = %field;
+ owner = %this;
+ };
+ %grid.add(%row);
+
+ %row.addChoice("default", "", "Use the profile's alignment");
+ %count = getRecordCount(%choices);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %rec = getRecord(%choices, %i);
+ %row.addChoice(getField(%rec, 0), getField(%rec, 1), getField(%rec, 2));
+ }
+
+ %row.build();
+ return %row;
+}
+
+//-----------------------------------------------------------------------------
+// Size and color.
+//-----------------------------------------------------------------------------
+
+function GuiEditorTextBlock::buildFont(%this)
+{
+ %grid = %this.makeGrid(128, 48);
+ %this.add(%grid);
+ %this.fontGrid = %grid;
+
+ // The kind comes from the pane's table rather than a literal here, so this
+ // row cannot end up spelling a field's type differently from everywhere
+ // else -- fontSizeAdjust is a multiplier, and a whole-number row would round
+ // every useful value of it away.
+ %this.row["fontSizeAdjust"] = %this.pane.makeFieldRow(%grid, "fontSizeAdjust",
+ "Font Size", %this.pane.sharedKindFor("fontSizeAdjust"), "");
+ %this.fontSizeRow = %this.row["fontSizeAdjust"];
+
+ // One widget for two fields. overrideFontColor has no row: picking a color
+ // is what turns it on, and the reset button -- which every field row already
+ // carries -- is what turns it off again. With it off the swatch shows the
+ // profile's own color, so the row always says what the control will draw in.
+ %this.row["fontColor"] = %this.pane.makeFieldRow(%grid, "fontColor",
+ "Font Color", %this.pane.sharedKindFor("fontColor"), "");
+ %this.fontColorRow = %this.row["fontColor"];
+ %this.fontColorRow.resetButton.Tooltip = "Go back to the profile's font color";
+}
+
+// The block's own cell grid. Narrower cells than the pane's rows use: two of
+// these fit where one 220-wide field row does, which is what lets the alignment
+// pair and the font pair each sit on one line.
+function GuiEditorTextBlock::makeGrid(%this, %cellW, %cellH)
+{
+ %grid = new GuiGridCtrl()
+ {
+ HorizSizing = "width";
+ Position = "0 0";
+ Extent = %this.blockWidth SPC 4;
+ CellModeX = "variable";
+ CellModeY = "variable";
+ CellSizeX = %cellW;
+ CellSizeY = %cellH;
+ CellSpacingX = 4;
+ CellSpacingY = 4;
+ MaxColCount = 0;
+ MaxRowCount = 0;
+ OrderMode = "lrtb";
+ IsExtentDynamic = true;
+ };
+ ThemeManager.setProfile(%grid, "emptyProfile");
+ return %grid;
+}
+
+//-----------------------------------------------------------------------------
+// Binding. Which parts apply is the spec's answer, asked per field rather than
+// per role so that the per-class exceptions (a text edit has no vAlign, a tab
+// book no wrap) are honoured here too.
+//
+// The three field rows are the pane's to show or hide -- they are in its shared
+// registry and its filter walks them. What is left is this block's own.
+//-----------------------------------------------------------------------------
+
+function GuiEditorTextBlock::bindClass(%this, %ctrl, %class)
+{
+ %spec = %this.spec;
+
+ %this.label.setText(%spec.textLabelFor(%class));
+
+ %wrap = %spec.isFieldVisible(%class, "textWrap");
+ %extend = %spec.isFieldVisible(%class, "textExtend");
+ %this.wrapButton.setVisible(%wrap);
+ %this.extendButton.setVisible(%extend);
+
+ // The caption row is the heading for a text box and the home of the two
+ // flags. With none of the three on show it is a title over nothing -- which
+ // is what a slider would get, since it reads fontSizeAdjust and nothing else
+ // in this block.
+ %this.captionRow.setVisible(%spec.isFieldVisible(%class, "text") || %wrap || %extend);
+
+ %this.alignRow.setVisible(%spec.isFieldVisible(%class, "align"));
+ %this.vAlignRow.setVisible(%spec.isFieldVisible(%class, "vAlign"));
+
+ // A container that lays out only the children it can see needs telling when
+ // one of them comes or goes; nothing re-runs the layout on setVisible.
+ %this.alignGrid.setVisible(%this.alignRow.isVisible() || %this.vAlignRow.isVisible());
+ %this.fontGrid.setVisible(%spec.isFieldVisible(%class, "fontSizeAdjust") ||
+ %spec.isFieldVisible(%class, "fontColor"));
+
+ %this.resizeGrid(%this.alignGrid);
+ %this.resizeGrid(%this.fontGrid);
+ %this.resizeToFit();
+}
+
+// The block's own width rather than the one it was built at: blockWidth is what
+// the pane authored, and a grid re-measured against it would snap back to that
+// after the Properties frame had been dragged wider.
+function GuiEditorTextBlock::resizeGrid(%this, %grid)
+{
+ %grid.resize(0, 0, getWord(%this.getExtent(), 0), getWord(%grid.getExtent(), 1));
+}
+
+// A GuiChainCtrl positions its children without resizing them, and a grid only
+// learns its height once something lays it out, so nudge the width by a pixel
+// and back to force exactly one parentResized through every child.
+//
+// Through its own position, not through zero: resize() sets the position as
+// well as the extent. The header's copy lives in a chain, which puts its
+// children back wherever it likes, but the Text section's copy sits at y=24
+// under the panel's title bar -- and moving it to zero drew its caption and its
+// two icons on top of that title, which read as a second "Text" printed over
+// the first.
+function GuiEditorTextBlock::resizeToFit(%this)
+{
+ %x = getWord(%this.getPosition(), 0);
+ %y = getWord(%this.getPosition(), 1);
+ %w = getWord(%this.getExtent(), 0);
+ %h = getWord(%this.getExtent(), 1);
+
+ %this.resize(%x, %y, %w + 1, %h);
+ %this.resize(%x, %y, %w, %h);
+}
+
+//-----------------------------------------------------------------------------
+// Loading. The pane loads the field rows it registered; what is left is the two
+// choice rows, the two toggles, and the one row whose value is not simply the
+// field it names.
+//-----------------------------------------------------------------------------
+
+function GuiEditorTextBlock::load(%this, %ctrl)
+{
+ %this.populating = true;
+
+ // A pending edit belongs to whatever was selected when it started. Loading
+ // a control abandons it -- the typed text is already on that control, so
+ // there is nothing to lose, and keeping the stash would let a blur that
+ // arrives after the selection moved write one control's caption onto
+ // another.
+ %this.endTyping();
+
+ %this.alignRow.setValue(%ctrl.getFieldValue("align"));
+ %this.vAlignRow.setValue(%ctrl.getFieldValue("vAlign"));
+
+ %this.wrapButton.setValue(%ctrl.textWrap);
+ %this.extendButton.setValue(%ctrl.textExtend);
+ %this.applyExtendTip(%ctrl.textWrap);
+
+ %this.loadFontColor(%ctrl);
+
+ %this.populating = false;
+}
+
+// The swatch shows what the control will actually draw in: its own color while
+// it is overriding, and the profile's while it is not. Anything else would have
+// the row claim a color the control does not use.
+function GuiEditorTextBlock::loadFontColor(%this, %ctrl)
+{
+ %override = %ctrl.overrideFontColor;
+ %color = %ctrl.getFieldValue("fontColor");
+
+ if(!%override)
+ {
+ %profile = GuiEditor.themeApplier.fieldProfile(%ctrl, "Profile");
+ if(isObject(%profile) && %profile.fontColor !$= "")
+ {
+ %color = %profile.fontColor;
+ }
+ }
+
+ %this.fontColorRow.setValue(%color);
+ %this.fontColorRow.setOverridden(%override);
+}
+
+//-----------------------------------------------------------------------------
+// Forwarding. Every write still goes through the pane.
+//-----------------------------------------------------------------------------
+
+function GuiEditorTextBlock::isPopulating(%this)
+{
+ return %this.populating || %this.pane.populating;
+}
+
+function GuiEditorTextBlock::onChoiceRowChanged(%this, %row)
+{
+ if(%this.isPopulating())
+ {
+ return;
+ }
+ %this.pane.onHeaderChoiceChanged(%row.fieldName, %row.getValue());
+}
+
+function GuiEditorTextBlock::onToggleIconChanged(%this, %toggle)
+{
+ if(%this.isPopulating())
+ {
+ return;
+ }
+
+ if(%toggle.toggleName $= "textWrap")
+ {
+ %this.applyExtendTip(%toggle.getValue());
+ }
+
+ %this.pane.onTextFlagChanged(%toggle.toggleName, %toggle.getValue());
+}
diff --git a/editor/GuiEditor/scripts/GuiEditorThemeApplier.cs b/editor/GuiEditor/scripts/GuiEditorThemeApplier.cs
new file mode 100644
index 000000000..29f9eabf4
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiEditorThemeApplier.cs
@@ -0,0 +1,677 @@
+
+//-----------------------------------------------------------------------------
+// Puts a theme on a Gui: walks a control tree and fills every profile slot from
+// the theme, choosing each slot's category from the control's class and the
+// field's name. Owned by GuiEditor, which hands it the theme library.
+//
+// Every profile slot in the engine is a persist field of type GuiProfile -
+// "Profile" itself, plus "contentProfile", "closeButtonProfile", "thumbProfile",
+// "menuItemProfile" and the rest - so the walk asks each control for its field
+// list and fills all of them rather than working from a hand-written slot map
+// per class. Only the category each field wants is knowledge this file has to
+// carry.
+//-----------------------------------------------------------------------------
+
+function GuiEditorThemeApplier::onAdd(%this)
+{
+ %this.buildFieldTable();
+ %this.buildClassTable();
+ %this.buildCursorFieldTable();
+}
+
+// The engine has exactly six cursor slots across three classes, and unlike a
+// profile slot each one means the same thing wherever it appears: a window's
+// upDownCursor and a frame set's are both "the pointer for a horizontal edge".
+// So this needs no per-class exceptions, only the field name.
+function GuiEditorThemeApplier::buildCursorFieldTable(%this)
+{
+ %this.setCursorFieldCategory("editCursor", "Edit");
+ %this.setCursorFieldCategory("leftRightCursor", "LeftRight");
+ %this.setCursorFieldCategory("upDownCursor", "UpDown");
+ %this.setCursorFieldCategory("nWSECursor", "NWSE");
+}
+
+function GuiEditorThemeApplier::setCursorFieldCategory(%this, %field, %category)
+{
+ %this.cursorFieldCategory[strlwr(%field)] = %category;
+}
+
+function GuiEditorThemeApplier::cursorCategoryForField(%this, %field)
+{
+ return %this.cursorFieldCategory[strlwr(%field)];
+}
+
+// What each named slot is for, independent of the control wearing it. "Profile"
+// is the exception: it means "whatever this control is", so it defers to the
+// class table.
+function GuiEditorThemeApplier::buildFieldTable(%this)
+{
+ %this.setFieldCategory("tooltipProfile", "Tooltip");
+
+ %this.setFieldCategory("contentProfile", "WindowContent");
+ %this.setFieldCategory("closeButtonProfile", "WindowCloseButton");
+ %this.setFieldCategory("minButtonProfile", "WindowButton");
+ %this.setFieldCategory("maxButtonProfile", "WindowButton");
+
+ %this.setFieldCategory("scrollProfile", "Scroll");
+ %this.setFieldCategory("thumbProfile", "ScrollThumb");
+ %this.setFieldCategory("trackProfile", "ScrollTrack");
+ %this.setFieldCategory("arrowProfile", "ScrollArrow");
+
+ // The list a drop-down opens, so its items rather than a plain list box - the
+ // drop-down is the only control with this field.
+ %this.setFieldCategory("listBoxProfile", "DropDownItem");
+ %this.setFieldCategory("tabBookProfile", "TabBook");
+ %this.setFieldCategory("tabProfile", "Tab");
+ %this.setFieldCategory("tabPageProfile", "TabPage");
+ %this.setFieldCategory("dropButtonProfile", "FrameSetDropButton");
+
+ %this.setFieldCategory("menuProfile", "Menu");
+ %this.setFieldCategory("menuItemProfile", "MenuItem");
+ %this.setFieldCategory("menuContentProfile", "MenuContent");
+
+ %this.setFieldCategory("popupProfile", "ColorPopup");
+ %this.setFieldCategory("pickerProfile", "ColorPicker");
+ %this.setFieldCategory("selectorProfile", "ColorSelector");
+
+ // The full-screen catcher a drop-down, menu or color popup puts behind its
+ // open list. It has to be invisible - Overlay is the deliberate dimming scrim
+ // for modal dialogs, and dimming the game behind an open drop-down is not what
+ // anyone means by this field.
+ %this.setFieldCategory("backgroundProfile", "Empty");
+
+ // Where a slot means something different depending on the control wearing it.
+ // A slider's thumb is not a scrollbar's.
+ %this.setCategoryFieldCategory("Slider", "thumbProfile", "SliderThumb");
+}
+
+// Which category a control's own Profile wants. Ordered most-derived first and
+// matched with isMemberOfClass, so a subclass - script-side or a future C++ one -
+// inherits its parent's answer without an entry of its own.
+function GuiEditorThemeApplier::buildClassTable(%this)
+{
+ %this.classCount = 0;
+
+ // Buttons: Radio derives from CheckBox derives from Button, and both the
+ // drop-down and the color popup are buttons too. Order is load-bearing.
+ %this.addClass("GuiRadioCtrl", "Radio");
+ %this.addClass("GuiCheckBoxCtrl", "CheckBox");
+ %this.addClass("GuiDropDownCtrl", "DropDown");
+ %this.addClass("GuiColorPopupCtrl", "ColorPopup");
+ %this.addClass("GuiButtonCtrl", "Button");
+
+ // Lists: TreeView derives from ListBox.
+ %this.addClass("GuiTreeViewCtrl", "TreeView");
+ %this.addClass("GuiListBoxCtrl", "ListBox");
+
+ %this.addClass("GuiTextEditCtrl", "TextEdit");
+ %this.addClass("GuiScrollCtrl", "Scroll");
+ %this.addClass("GuiTabBookCtrl", "TabBook");
+ %this.addClass("GuiTabPageCtrl", "TabPage");
+ %this.addClass("GuiWindowCtrl", "Window");
+ %this.addClass("GuiMenuBarCtrl", "MenuBar");
+ %this.addClass("GuiMenuItemCtrl", "MenuItem");
+ %this.addClass("GuiProgressCtrl", "Progress");
+ %this.addClass("GuiFrameSetCtrl", "FrameSet");
+ %this.addClass("GuiColorPickerCtrl", "ColorPicker");
+ %this.addClass("GuiDragAndDropCtrl", "DragAndDrop");
+ %this.addClass("GuiSliderCtrl", "Slider");
+ %this.addClass("GuiPanelCtrl", "Panel");
+}
+
+function GuiEditorThemeApplier::setFieldCategory(%this, %field, %category)
+{
+ %this.fieldCategory[strlwr(%field)] = %category;
+}
+
+// An answer that only applies to controls of one kind, keyed by the category the
+// control itself takes - which is one per kind of control, so it doubles as the
+// class key without a second class lookup.
+function GuiEditorThemeApplier::setCategoryFieldCategory(%this, %mainCategory, %field, %category)
+{
+ %this.categoryField[%mainCategory, strlwr(%field)] = %category;
+}
+
+function GuiEditorThemeApplier::addClass(%this, %className, %category)
+{
+ %this.className[%this.classCount] = %className;
+ %this.classCategory[%this.classCount] = %category;
+ %this.classCount++;
+}
+
+//-----------------------------------------------------------------------------
+// Applying.
+//-----------------------------------------------------------------------------
+
+// Theme every child of %parent (and everything below them), leaving %parent
+// itself alone. The Gui Editor calls this with its simulated canvas, which is
+// stage furniture rather than part of the Gui being authored.
+function GuiEditorThemeApplier::applyToChildren(%this, %parent, %theme, %overrideStandalone)
+{
+ if(!isObject(%parent) || !isObject(%theme))
+ {
+ return 0;
+ }
+
+ %this.beginApply();
+ %changed = 0;
+ for(%i = 0; %i < %parent.getCount(); %i++)
+ {
+ %changed += %this.walk(%parent.getObject(%i), %theme, %overrideStandalone);
+ }
+ %this.endApply();
+
+ return %changed;
+}
+
+// Theme %ctrl and everything below it. Used when a single control is dropped
+// into an already-themed Gui.
+function GuiEditorThemeApplier::applyToBranch(%this, %ctrl, %theme, %overrideStandalone)
+{
+ if(!isObject(%ctrl) || !isObject(%theme))
+ {
+ return 0;
+ }
+
+ %this.beginApply();
+ %changed = %this.walk(%ctrl, %theme, %overrideStandalone);
+ %this.endApply();
+
+ // Consume the palette's request. It exists to answer one question -- which
+ // of the four faces a bare GuiControl was dropped as -- and that question is
+ // asked once, on arrival. What the control ends up WEARING is the lasting
+ // record of what it was told to be, so leaving the field set would let a drop
+ // silently overrule a later change made in the properties pane.
+ %ctrl.paletteCategory = "";
+
+ return %changed;
+}
+
+// The loaded themes change only when the library loads or creates one, so the
+// list is taken once per apply rather than per profile the walk has to place.
+function GuiEditorThemeApplier::beginApply(%this)
+{
+ %this.themeList = %this.library.getThemes();
+}
+
+function GuiEditorThemeApplier::endApply(%this)
+{
+ %this.themeList = "";
+}
+
+function GuiEditorThemeApplier::walk(%this, %ctrl, %theme, %overrideStandalone)
+{
+ %changed = %this.applyToControl(%ctrl, %theme, %overrideStandalone);
+
+ for(%i = 0; %i < %ctrl.getCount(); %i++)
+ {
+ %changed += %this.walk(%ctrl.getObject(%i), %theme, %overrideStandalone);
+ }
+
+ return %changed;
+}
+
+function GuiEditorThemeApplier::applyToControl(%this, %ctrl, %theme, %overrideStandalone)
+{
+ %isRoot = (%ctrl.getParent() == %this.rootContainer);
+ %mainCategory = %this.categoryForControl(%ctrl, %isRoot);
+ %changed = 0;
+
+ %count = %ctrl.getFieldCount();
+ for(%i = 0; %i < %count; %i++)
+ {
+ %field = %ctrl.getField(%i);
+ if(%ctrl.getFieldType(%field) !$= "GuiProfile")
+ {
+ continue;
+ }
+
+ %current = %this.fieldProfile(%ctrl, %field);
+ %owner = isObject(%current) ? %this.themeOf(%current) : 0;
+
+ // Already wearing this theme. Whatever it is - the category's main
+ // profile, a second button profile someone picked - it was chosen
+ // deliberately and this is not the place to second-guess it.
+ if(%owner == %theme && isObject(%owner))
+ {
+ continue;
+ }
+
+ if(isObject(%owner))
+ {
+ // From another theme: carry the category straight across, so a
+ // control on that theme's WindowContent lands on this one's. What
+ // the developer set outranks what this editor would have guessed.
+ %category = %current.category;
+ }
+ else
+ {
+ // A script profile, one of AppCore's, or nothing at all. Stand-alone
+ // profiles are the supported alternative to theming and are left
+ // alone unless this apply was asked to override them.
+ if(!%overrideStandalone && isObject(%current) && %this.library.isStandaloneProfile(%current))
+ {
+ continue;
+ }
+
+ %category = %this.categoryForField(%field, %mainCategory);
+ }
+
+ if(%category $= "")
+ {
+ // A slot on some control this editor has never heard of. Better to
+ // leave whatever it holds than to guess.
+ continue;
+ }
+
+ %target = %theme.getProfile(%category);
+ if(!isObject(%target) || %current == %target)
+ {
+ continue;
+ }
+
+ // Through the Gui Editor's undo recorder, which does the write. Set Theme
+ // fills a slot on every control in the document and that is one Ctrl+Z, so
+ // GuiEditor::setTheme opens a transaction around the whole sweep; the
+ // recorder is what collects the writes into it.
+ //
+ // setEditFieldValue, not a plain assignment: a profile field is a raw
+ // field offset, so writing it does not touch reference counts. The
+ // inspect-apply pair sleeps the control first and wakes it after, which
+ // releases the old profile and takes a count on the new one - and, in the
+ // editor, records the change for undo.
+ //
+ // By id, not by name: the Gui Editor runs the engine in editor mode, where
+ // SimObject::assignName stashes a new object's name rather than
+ // registering it (simObject.cc), so that naming a control being edited
+ // does not create a global. A profile made during the session - an extra
+ // added to a theme, a new stand-alone - therefore cannot be found by name
+ // until it has been saved and reloaded. An id always resolves, and the
+ // field still writes its name when the Gui is saved.
+ GuiEditor.undoRecorder.writeField(%ctrl, %field, %target.getId());
+ %changed++;
+ }
+
+ %changed += %this.applyCursorsToControl(%ctrl, %theme);
+
+ return %changed;
+}
+
+// The cursor slots, filled the same way and for the same reason: a Gui should
+// wear ITS theme's cursors, not whichever set a project happened to install
+// globally. Done silently -- the properties pane only shows a cursor row when
+// the theme offers a choice, so most controls are themed here and never
+// mention it.
+//
+// A cursor already belonging to this theme is left alone, which is what lets a
+// deliberate second choice survive a re-apply.
+function GuiEditorThemeApplier::applyCursorsToControl(%this, %ctrl, %theme)
+{
+ %changed = 0;
+
+ %count = %ctrl.getFieldCount();
+ for(%i = 0; %i < %count; %i++)
+ {
+ %field = %ctrl.getField(%i);
+ if(%ctrl.getFieldType(%field) !$= "GuiCursor")
+ {
+ continue;
+ }
+
+ %category = %this.cursorCategoryForField(%field);
+ if(%category $= "")
+ {
+ continue;
+ }
+
+ %current = %this.fieldCursor(%ctrl, %field);
+ if(isObject(%current) && %this.themeOfCursor(%current) == %theme)
+ {
+ continue;
+ }
+
+ // From another theme: carry the category across rather than this one's
+ // guess, exactly as a profile slot does.
+ if(isObject(%current) && %current.category !$= "")
+ {
+ %category = %current.category;
+ }
+
+ %target = %theme.getCursor(%category);
+ if(!isObject(%target) || %current == %target)
+ {
+ continue;
+ }
+
+ GuiEditor.undoRecorder.writeField(%ctrl, %field, %target.getId());
+ %changed++;
+ }
+
+ return %changed;
+}
+
+// The object in a cursor field, or 0. Same reasoning as fieldProfile: the field
+// reads back as a name, and a name the Sim cannot resolve is not necessarily a
+// dead reference while the editor is running in editor mode.
+function GuiEditorThemeApplier::fieldCursor(%this, %ctrl, %field)
+{
+ %value = %ctrl.getFieldValue(%field);
+ if(%value $= "")
+ {
+ return 0;
+ }
+ if(isObject(%value))
+ {
+ return %value.getId();
+ }
+ return %this.library.findCursorByName(%value);
+}
+
+// The theme a cursor belongs to, or 0. A cursor carries its category, which
+// narrows the search to one list per theme.
+function GuiEditorThemeApplier::themeOfCursor(%this, %cursor)
+{
+ %category = %cursor.category;
+ if(%category $= "")
+ {
+ return 0;
+ }
+
+ %themeCount = getWordCount(%this.themeList);
+ for(%i = 0; %i < %themeCount; %i++)
+ {
+ %theme = getWord(%this.themeList, %i);
+ %members = %theme.getCursors(%category);
+ for(%m = 0; %m < getWordCount(%members); %m++)
+ {
+ if(getWord(%members, %m) == %cursor)
+ {
+ return %theme;
+ }
+ }
+ }
+
+ return 0;
+}
+
+// The object in a profile field, or 0. The field reads back as a name (or an id
+// string for an unnamed profile). A name the Sim cannot resolve is not
+// necessarily a dead reference: in editor mode a profile created this session
+// carries a name that was never registered (see the note in applyToControl), so
+// fall back to searching what the library holds before giving up. Getting this
+// wrong would read as "the slot is empty" and quietly overwrite a deliberate
+// choice.
+function GuiEditorThemeApplier::fieldProfile(%this, %ctrl, %field)
+{
+ %value = %ctrl.getFieldValue(%field);
+ if(%value $= "")
+ {
+ return 0;
+ }
+ if(isObject(%value))
+ {
+ return %value.getId();
+ }
+ return %this.library.findProfileByName(%value);
+}
+
+// The theme a profile belongs to, or 0. A profile carries the category it was
+// stamped for, which narrows the search to one list per theme; a profile with no
+// category cannot be a member of anything.
+function GuiEditorThemeApplier::themeOf(%this, %profile)
+{
+ %category = %profile.category;
+ if(%category $= "")
+ {
+ return 0;
+ }
+
+ %themeCount = getWordCount(%this.themeList);
+ for(%i = 0; %i < %themeCount; %i++)
+ {
+ %theme = getWord(%this.themeList, %i);
+ %members = %theme.getProfiles(%category);
+ for(%m = 0; %m < getWordCount(%members); %m++)
+ {
+ if(getWord(%members, %m) == %profile)
+ {
+ return %theme;
+ }
+ }
+ }
+
+ return 0;
+}
+
+//-----------------------------------------------------------------------------
+// Detaching.
+//-----------------------------------------------------------------------------
+
+// Move every slot below %parent that wears one of the doomed profiles onto the
+// engine's GuiDefaultProfile, which is the one profile that always exists.
+//
+// This is what keeps a Profile Editor session from taking the Gui down with it.
+// Reverting or deleting a theme frees its member profiles, and a control's
+// profile field is a raw pointer - nothing tells the control its profile is
+// gone, and the dangling read surfaces much later, usually as a crash at exit.
+// Pass either a theme (all of its members) or a single profile.
+function GuiEditorThemeApplier::detach(%this, %parent, %theme, %profile)
+{
+ if(!isObject(%parent))
+ {
+ return 0;
+ }
+
+ %this.beginApply();
+ %changed = 0;
+ for(%i = 0; %i < %parent.getCount(); %i++)
+ {
+ %changed += %this.detachWalk(%parent.getObject(%i), %theme, %profile);
+ }
+ %this.endApply();
+
+ return %changed;
+}
+
+function GuiEditorThemeApplier::detachWalk(%this, %ctrl, %theme, %profile)
+{
+ %changed = 0;
+
+ %count = %ctrl.getFieldCount();
+ for(%i = 0; %i < %count; %i++)
+ {
+ %field = %ctrl.getField(%i);
+ if(%ctrl.getFieldType(%field) !$= "GuiProfile")
+ {
+ continue;
+ }
+
+ %current = %this.fieldProfile(%ctrl, %field);
+ if(!isObject(%current))
+ {
+ continue;
+ }
+
+ %doomed = (isObject(%profile) && %current == %profile);
+ if(!%doomed && isObject(%theme))
+ {
+ %doomed = (%this.themeOf(%current) == %theme);
+ }
+ if(!%doomed)
+ {
+ continue;
+ }
+
+ %ctrl.setEditFieldValue(%field, "GuiDefaultProfile");
+ %changed++;
+ }
+
+ %changed += %this.detachCursors(%ctrl, %theme);
+
+ for(%i = 0; %i < %ctrl.getCount(); %i++)
+ {
+ %changed += %this.detachWalk(%ctrl.getObject(%i), %theme, %profile);
+ }
+
+ return %changed;
+}
+
+// The same rescue for cursor slots. A GuiCursor* is as raw a pointer as a
+// profile's, so a control still naming one of a doomed theme's cursors would be
+// left pointing at freed memory.
+//
+// The replacement is the canonical name for the slot ("LeftRightCursor" and the
+// rest) rather than an empty string: writing "" through TypeGuiCursor does not
+// clear the field, it falls back to DefaultCursor (guiTypes.cc), which would
+// put an arrow on a window's resize edge.
+function GuiEditorThemeApplier::detachCursors(%this, %ctrl, %theme)
+{
+ if(!isObject(%theme))
+ {
+ return 0;
+ }
+
+ %changed = 0;
+ %count = %ctrl.getFieldCount();
+ for(%i = 0; %i < %count; %i++)
+ {
+ %field = %ctrl.getField(%i);
+ if(%ctrl.getFieldType(%field) !$= "GuiCursor")
+ {
+ continue;
+ }
+
+ %current = %this.fieldCursor(%ctrl, %field);
+ if(!isObject(%current) || %this.themeOfCursor(%current) != %theme)
+ {
+ continue;
+ }
+
+ %category = %this.cursorCategoryForField(%field);
+ if(%category $= "")
+ {
+ continue;
+ }
+
+ %ctrl.setEditFieldValue(%field, %theme.getCursorCanonicalName(%category));
+ %changed++;
+ }
+
+ return %changed;
+}
+
+//-----------------------------------------------------------------------------
+// Category lookup.
+//-----------------------------------------------------------------------------
+
+function GuiEditorThemeApplier::categoryForField(%this, %field, %mainCategory)
+{
+ %key = strlwr(%field);
+ if(%key $= "profile")
+ {
+ return %mainCategory;
+ }
+
+ %override = %this.categoryField[%mainCategory, %key];
+ if(%override !$= "")
+ {
+ return %override;
+ }
+
+ return %this.fieldCategory[%key];
+}
+
+// Empty is the answer for anything that is not a recognized control: it is the
+// deliberate invisible wrapper, which is what a layout control wants and the
+// least wrong thing for a control this editor does not know.
+//
+// A bare GuiControl is the interesting case, because in 4.0 it is both the
+// wrapper and the text control. At the root of a Gui it is the backdrop the rest
+// of the screen sits on, so it takes Panel. Below that, a line of text is a
+// Label; no text, or text that wraps (a paragraph rather than a caption), is a
+// wrapper.
+function GuiEditorThemeApplier::categoryForControl(%this, %ctrl, %isRoot)
+{
+ if(%ctrl.getClassName() $= "GuiControl")
+ {
+ // The palette can say outright which of the four it dropped, and when it
+ // does there is nothing to guess. Two of the four are unreachable by the
+ // guess below at all: a Panel that is not the root, and Overlay ever.
+ // Cleared by applyToBranch once the walk is done.
+ if(%ctrl.paletteCategory !$= "")
+ {
+ return %ctrl.paletteCategory;
+ }
+
+ if(%isRoot)
+ {
+ return "Panel";
+ }
+ return (%ctrl.text !$= "" && !%ctrl.textWrap) ? "Label" : "Empty";
+ }
+
+ for(%i = 0; %i < %this.classCount; %i++)
+ {
+ if(%ctrl.isMemberOfClass(%this.className[%i]))
+ {
+ return %this.classCategory[%i];
+ }
+ }
+
+ return "Empty";
+}
+
+//-----------------------------------------------------------------------------
+// Reading a Gui's theme back.
+//-----------------------------------------------------------------------------
+
+// Which theme a control tree is already wearing, judged by the first themed
+// profile found in it. Used when a Gui arrives without a recorded theme - one
+// written before the field existed, or authored by hand.
+function GuiEditorThemeApplier::inferTheme(%this, %parent)
+{
+ if(!isObject(%parent))
+ {
+ return 0;
+ }
+
+ %this.beginApply();
+ %theme = %this.inferFrom(%parent);
+ %this.endApply();
+
+ return %theme;
+}
+
+function GuiEditorThemeApplier::inferFrom(%this, %ctrl)
+{
+ %count = %ctrl.getFieldCount();
+ for(%i = 0; %i < %count; %i++)
+ {
+ %field = %ctrl.getField(%i);
+ if(%ctrl.getFieldType(%field) !$= "GuiProfile")
+ {
+ continue;
+ }
+
+ %profile = %this.fieldProfile(%ctrl, %field);
+ if(isObject(%profile))
+ {
+ %theme = %this.themeOf(%profile);
+ if(isObject(%theme))
+ {
+ return %theme;
+ }
+ }
+ }
+
+ for(%i = 0; %i < %ctrl.getCount(); %i++)
+ {
+ %theme = %this.inferFrom(%ctrl.getObject(%i));
+ if(isObject(%theme))
+ {
+ return %theme;
+ }
+ }
+
+ return 0;
+}
diff --git a/editor/GuiEditor/scripts/GuiEditorThemeDialog.cs b/editor/GuiEditor/scripts/GuiEditorThemeDialog.cs
new file mode 100644
index 000000000..ed8744a94
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiEditorThemeDialog.cs
@@ -0,0 +1,103 @@
+
+//-----------------------------------------------------------------------------
+// Picks the theme the Gui being edited wears. Applying re-profiles every control
+// in the document from that theme, so this is the one place a developer has to
+// think about profiles at all.
+//
+// Stand-alone profiles are the exception the checkbox exists for: they are the
+// supported alternative to theming and are left alone by default. Everything
+// else - a script profile, one of AppCore's, another theme's - is replaced.
+//-----------------------------------------------------------------------------
+
+function GuiEditorThemeDialog::init(%this, %width, %height)
+{
+ %window = %this.getObject(0);
+ %content = %window.getObject(0);
+
+ // The dialog's scrolling content is the dialog size less the window frame
+ // (4px each side) and its title bar (30) - see EditorDialog::onAdd. Lay out
+ // inside that, or the form pushes a scrollbar in and clips the button.
+ %contentWidth = %width - 8;
+ %contentHeight = %height - 26;
+
+ %form = new GuiGridCtrl()
+ {
+ class = "EditorForm";
+ extent = %contentWidth SPC 120;
+ cellSizeX = %contentWidth;
+ cellSizeY = 60;
+ };
+ %content.add(%form);
+
+ %item = %form.addFormItem("Theme", %contentWidth SPC 50);
+ %this.themeDrop = %form.createDropDownItem(%item);
+
+ %item = %form.addFormItem("Override stand alone profiles", %contentWidth SPC 40);
+ %this.overrideBox = %form.createCheckboxItem(%item);
+ %this.overrideBox.setStateOn(false);
+
+ %this.applyButton = new GuiButtonCtrl()
+ {
+ HorizSizing = "right";
+ VertSizing = "bottom";
+ Position = (%contentWidth - 110) SPC (%contentHeight - 44);
+ Extent = "100 34";
+ Text = "Apply";
+ Command = %this.getID() @ ".onApply();";
+ };
+ ThemeManager.setProfile(%this.applyButton, "primaryButtonProfile");
+ %content.add(%this.applyButton);
+
+ %this.populate();
+}
+
+function GuiEditorThemeDialog::populate(%this)
+{
+ %themes = GuiEditor.getThemeLibrary().getThemes();
+ %count = getWordCount(%themes);
+
+ %this.themeDrop.clearItems();
+ %this.themeCount = 0;
+
+ %selected = -1;
+ for(%i = 0; %i < %count; %i++)
+ {
+ %theme = getWord(%themes, %i);
+ if(%theme.getName() $= "")
+ {
+ continue;
+ }
+
+ %this.themeDrop.addItem(%theme.getName());
+ %this.theme[%this.themeCount] = %theme;
+ if(%theme.getName() $= GuiEditor.themeName)
+ {
+ %selected = %this.themeCount;
+ }
+ %this.themeCount++;
+ }
+
+ if(%this.themeCount == 0)
+ {
+ // Nothing to apply. A project always has at least one theme once AppCore
+ // has run, so this means the Gui Editor is open without a project.
+ %this.themeDrop.setActive(false);
+ %this.applyButton.setActive(false);
+ return;
+ }
+
+ %this.themeDrop.setSelected(%selected >= 0 ? %selected : 0);
+}
+
+function GuiEditorThemeDialog::onApply(%this)
+{
+ %index = %this.themeDrop.getSelectedItem();
+ if(%index < 0 || %index >= %this.themeCount)
+ {
+ %this.onClose();
+ return;
+ }
+
+ GuiEditor.setTheme(%this.theme[%index], %this.overrideBox.getStateOn());
+ %this.onClose();
+}
diff --git a/editor/GuiEditor/scripts/GuiEditorToolsWindow.cs b/editor/GuiEditor/scripts/GuiEditorToolsWindow.cs
new file mode 100644
index 000000000..4f77fade0
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiEditorToolsWindow.cs
@@ -0,0 +1,47 @@
+
+function GuiEditorToolsWindow::onAdd(%this)
+{
+ %this.buttonBar = new GuiChainCtrl()
+ {
+ Class = "EditorButtonBar";
+ Position = "6 4";
+ Extent = "0 30";
+ ChildSpacing = 4;
+ IsVertical = false;
+ Tool = %this;
+ };
+ ThemeManager.setProfile(%this.buttonBar, "emptyProfile");
+ %this.add(%this.buttonBar);
+
+ %this.buttonBar.addButton("onProfileEditor", $EditorIcon::doc_edit, "Open the Gui Profile Editor", "");
+ %this.buttonBar.addButton("onSetTheme", $EditorIcon::brush, "Set this Gui's theme", "");
+}
+
+// The window's own title carries the document being edited, and a trailing star
+// when it has changes that are not on disk. This window rather than anywhere
+// else because it is the top-left panel and already holds the theme buttons, so
+// it is where the eye goes first -- and because the editor TAB cannot do it:
+// EditorCoreTabBook::onTabSelected looks an editor up by its tab text, so
+// retitling the tab loses the lookup.
+function GuiEditorToolsWindow::showDocument(%this, %name, %modified)
+{
+ %this.setText(%modified ? (%name @ " *") : %name);
+}
+
+function GuiEditorToolsWindow::onRemove(%this)
+{
+ if(isObject(%this.buttonBar))
+ {
+ %this.buttonBar.delete();
+ }
+}
+
+function GuiEditorToolsWindow::onProfileEditor(%this)
+{
+ GuiEditor.openProfileEditor();
+}
+
+function GuiEditorToolsWindow::onSetTheme(%this)
+{
+ GuiEditor.openThemeDialog();
+}
diff --git a/editor/GuiEditor/scripts/GuiEditorUndoAction.cs b/editor/GuiEditor/scripts/GuiEditorUndoAction.cs
new file mode 100644
index 000000000..dd0ccb8fe
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiEditorUndoAction.cs
@@ -0,0 +1,510 @@
+
+//-----------------------------------------------------------------------------
+// One undoable step. Built by GuiEditorUndoRecorder, which is the only thing
+// that creates one, and handed to the UndoManager the Gui Editor's GuiEditCtrl
+// already owns (guiEditCtrl.cc, getUndoManager). UndoScriptAction is the engine
+// class that exists for exactly this - its undo() and redo() call straight back
+// into script (collection/undo.h).
+//
+// An action holds an ordered list of ops rather than one change, because a
+// single gesture is several changes: dropping a control moves it into the add
+// set, then the theme applier writes a profile into every slot it has, then its
+// position is set. That is one Ctrl+Z, so it is one action.
+//
+// field a persist field write. setEditFieldValue, not assignment, so the
+// control sleeps and wakes around it and protected fields run their
+// setters - Position and Extent are protected (guiControl.h), so a
+// geometry op really does resize the control rather than poke
+// mBounds behind its back.
+// dynamic a dynamic field write, which has no edit-field path.
+// move a change of parent, of index within the parent, or both. The
+// trash is an ordinary parent here: it is a real SimGroup the edit
+// control owns and never empties, so a deleted control is still a
+// live object with a home, and undoing a delete is the same
+// operation as undoing anything else that moved.
+// items a list box or drop down's whole set of static rows, before and
+// after. Whole for the same reason an order op is: every gesture the
+// Items pane offers can shift the rows around the one it touched.
+// order one parent's whole list of children, before and after. Restoring
+// a list is order-independent and cannot be got subtly wrong, which
+// a pile of per-control indices can: every index a move op restores
+// shifts the siblings around it, so a rearrangement that touched
+// several of them at once depends on replay order. A drag in the
+// Explorer tree can move any number of controls between any number
+// of parents, so it records lists.
+//
+// undo() replays the list backwards writing the before values, redo() forwards
+// writing the after values. Every op re-checks its objects: an action can
+// outlive the controls it names.
+//-----------------------------------------------------------------------------
+
+function GuiEditorUndoAction::onAdd(%this)
+{
+ %this.opCount = 0;
+ %this.fixCount = 0;
+ %this.frameCount = 0;
+ %this.touchList = "";
+}
+
+// Where a control's layout has to end up once the ops have run.
+//
+// A container that places its own children takes their layout from them when
+// they arrive, and every one of them takes something different: a GuiChainCtrl
+// zeroes the position outright while the editor is open, a GuiGridCtrl forces
+// the sizing off center and fill, a GuiFrameSetCtrl forces it off center and
+// then resizes the control to its frame (guiChainCtrl.cc / guiGridCtrl.cc /
+// guiFrameSetCtrl.cc, onChildAdded). All of it is right for a control being
+// dropped in and wrong for one being put back, which knows what it was.
+//
+// So a structural op remembers all four of the fields a container can take -
+// position, extent, and the two sizing modes - at each end, and writes them
+// again afterwards. Afterwards is the point: recording them as ordinary field
+// ops would not do, because ops replay forwards for a redo and backwards for an
+// undo, and the layout has to be written after the move in both directions.
+//
+// %before and %after are TAB-delimited, from GuiEditorUndoRecorder::layoutOf.
+function GuiEditorUndoAction::addLayoutFix(%this, %ctrl, %before, %after)
+{
+ %i = %this.fixCount;
+ %this.fixCtrl[%i] = %ctrl;
+ %this.fixBefore[%i] = %before;
+ %this.fixAfter[%i] = %after;
+ %this.fixCount = %i + 1;
+}
+
+// The same idea one level up, for the one container that keeps a layout of its
+// own beside the child list. A GuiFrameSetCtrl destroys a frame when the
+// control in it is removed and merges the split into its twin, so putting the
+// control back is not enough - the frame it stood in has to be rebuilt too, and
+// the sibling that swallowed the space given it back.
+function GuiEditorUndoAction::addFrameFix(%this, %frameSet, %before, %after)
+{
+ %i = %this.frameCount;
+ %this.frameSet[%i] = %frameSet;
+ %this.frameBefore[%i] = %before;
+ %this.frameAfter[%i] = %after;
+ %this.frameCount = %i + 1;
+}
+
+//-----------------------------------------------------------------------------
+// Recording.
+//-----------------------------------------------------------------------------
+
+function GuiEditorUndoAction::addFieldOp(%this, %ctrl, %field, %before, %after, %isDynamic)
+{
+ %i = %this.opCount;
+ %this.opType[%i] = %isDynamic ? "dynamic" : "field";
+ %this.opCtrl[%i] = %ctrl;
+ %this.opField[%i] = %field;
+ %this.opBefore[%i] = %before;
+ %this.opAfter[%i] = %after;
+ %this.opCount = %i + 1;
+}
+
+function GuiEditorUndoAction::addMoveOp(%this, %ctrl, %oldParent, %oldIndex, %newParent, %newIndex)
+{
+ %i = %this.opCount;
+ %this.opType[%i] = "move";
+ %this.opCtrl[%i] = %ctrl;
+ %this.opOldParent[%i] = %oldParent;
+ %this.opOldIndex[%i] = %oldIndex;
+ %this.opNewParent[%i] = %newParent;
+ %this.opNewIndex[%i] = %newIndex;
+ %this.opCount = %i + 1;
+}
+
+// %before and %after are lists of child ids. The parent is the op's object.
+function GuiEditorUndoAction::addOrderOp(%this, %parent, %before, %after)
+{
+ %i = %this.opCount;
+ %this.opType[%i] = "order";
+ %this.opCtrl[%i] = %parent;
+ %this.opBefore[%i] = %before;
+ %this.opAfter[%i] = %after;
+ %this.opCount = %i + 1;
+}
+
+// %before and %after are whole item lists, from GuiListBoxCtrl::getItemList.
+// Like an order op it names no field, so it holds the same two slots a field op
+// does and findOp matches it on the object alone.
+function GuiEditorUndoAction::addItemsOp(%this, %ctrl, %before, %after)
+{
+ %i = %this.opCount;
+ %this.opType[%i] = "items";
+ %this.opCtrl[%i] = %ctrl;
+ %this.opBefore[%i] = %before;
+ %this.opAfter[%i] = %after;
+ %this.opCount = %i + 1;
+}
+
+function GuiEditorUndoAction::isEmpty(%this)
+{
+ return %this.opCount <= 0;
+}
+
+// Name a control the user should be looking at after this action replays, for
+// the cases where the ops do not say it themselves - an order op names the
+// parent whose children were rearranged, not the control that was dragged.
+function GuiEditorUndoAction::noteTouched(%this, %ctrl)
+{
+ if(!isObject(%ctrl) || %this.listHas(%this.touchList, %ctrl))
+ {
+ return;
+ }
+
+ %this.touchList = (%this.touchList $= "") ? %ctrl : (%this.touchList SPC %ctrl);
+}
+
+// Fold another action's ops into this one, for coalescing a run of nudges into
+// the single move the user thinks they made. An op that touches something this
+// action already touched updates that op's after value and keeps its before -
+// so ten nudges read as one move from where the control started to where it
+// ended up. Anything new is appended.
+function GuiEditorUndoAction::mergeFrom(%this, %other)
+{
+ for(%i = 0; %i < %other.opCount; %i++)
+ {
+ %found = %this.findOp(%other.opType[%i], %other.opCtrl[%i], %other.opField[%i]);
+ if(%found == -1)
+ {
+ if(%other.opType[%i] $= "move")
+ {
+ %this.addMoveOp(%other.opCtrl[%i], %other.opOldParent[%i], %other.opOldIndex[%i],
+ %other.opNewParent[%i], %other.opNewIndex[%i]);
+ }
+ else if(%other.opType[%i] $= "order")
+ {
+ %this.addOrderOp(%other.opCtrl[%i], %other.opBefore[%i], %other.opAfter[%i]);
+ }
+ else if(%other.opType[%i] $= "items")
+ {
+ %this.addItemsOp(%other.opCtrl[%i], %other.opBefore[%i], %other.opAfter[%i]);
+ }
+ else
+ {
+ %this.addFieldOp(%other.opCtrl[%i], %other.opField[%i], %other.opBefore[%i],
+ %other.opAfter[%i], %other.opType[%i] $= "dynamic");
+ }
+ continue;
+ }
+
+ if(%other.opType[%i] $= "move")
+ {
+ %this.opNewParent[%found] = %other.opNewParent[%i];
+ %this.opNewIndex[%found] = %other.opNewIndex[%i];
+ }
+ else if(%other.opType[%i] $= "order")
+ {
+ %this.opAfter[%found] = %other.opAfter[%i];
+ }
+ else
+ {
+ %this.opAfter[%found] = %other.opAfter[%i];
+ }
+ }
+}
+
+function GuiEditorUndoAction::findOp(%this, %type, %ctrl, %field)
+{
+ for(%i = 0; %i < %this.opCount; %i++)
+ {
+ if(%this.opType[%i] !$= %type || %this.opCtrl[%i] != %ctrl)
+ {
+ continue;
+ }
+
+ // A move, an order or an items op names no field, so matching the object
+ // - the control for one, the parent for the other - is the whole test.
+ if(%type $= "move" || %type $= "order" || %type $= "items" ||
+ %this.opField[%i] $= %field)
+ {
+ return %i;
+ }
+ }
+
+ return -1;
+}
+
+// Every control this action names, once each, in the order they were recorded.
+// The recorder re-selects these after a replay so the user can see what a
+// Ctrl+Z did.
+function GuiEditorUndoAction::touched(%this)
+{
+ if(%this.touchList !$= "")
+ {
+ return %this.touchList;
+ }
+
+ %list = "";
+ for(%i = 0; %i < %this.opCount; %i++)
+ {
+ %ctrl = %this.opCtrl[%i];
+ if(%ctrl $= "" || %this.listHas(%list, %ctrl))
+ {
+ continue;
+ }
+ %list = (%list $= "") ? %ctrl : (%list SPC %ctrl);
+ }
+
+ return %list;
+}
+
+function GuiEditorUndoAction::listHas(%this, %list, %item)
+{
+ for(%i = 0; %i < getWordCount(%list); %i++)
+ {
+ if(getWord(%list, %i) == %item)
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Replaying. Called by the UndoManager (collection/undo.h, UndoScriptAction).
+//-----------------------------------------------------------------------------
+
+function GuiEditorUndoAction::undo(%this)
+{
+ %this.replay(false);
+}
+
+function GuiEditorUndoAction::redo(%this)
+{
+ %this.replay(true);
+}
+
+// Backwards for an undo: the ops were recorded in the order they happened, and
+// unwinding them in that same order would replay a control's second write
+// before its first.
+function GuiEditorUndoAction::replay(%this, %forward)
+{
+ // The recorder that built this action, handed over when it did. It outlives
+ // the stack it filled, but check anyway: an action can still be replayed
+ // while the editor is being torn down around it.
+ // With the direction, because the recorder tracks which state the document is
+ // in and that is the only thing that says which way it just moved.
+ if(isObject(%this.recorder))
+ {
+ %this.recorder.noteReplay(%this, %forward);
+ }
+
+ if(%forward)
+ {
+ for(%i = 0; %i < %this.opCount; %i++)
+ {
+ %this.applyOp(%i, true);
+ }
+ }
+ else
+ {
+ for(%i = %this.opCount - 1; %i >= 0; %i--)
+ {
+ %this.applyOp(%i, false);
+ }
+ }
+
+ %this.applyLayoutFixes(%forward);
+ %this.applyFrameFixes(%forward);
+ %this.notifyParents(%forward);
+}
+
+// After the layout fixes, because rebuilding the tree lays the children out
+// from their frames -- which is the frame set's answer, and it outranks the
+// control's own.
+function GuiEditorUndoAction::applyFrameFixes(%this, %forward)
+{
+ for(%i = 0; %i < %this.frameCount; %i++)
+ {
+ %frameSet = %this.frameSet[%i];
+ if(!isObject(%frameSet))
+ {
+ continue;
+ }
+
+ %frameSet.setFrameLayout(%forward ? %this.frameAfter[%i] : %this.frameBefore[%i]);
+ }
+}
+
+function GuiEditorUndoAction::applyLayoutFixes(%this, %forward)
+{
+ for(%i = 0; %i < %this.fixCount; %i++)
+ {
+ %ctrl = %this.fixCtrl[%i];
+ if(!isObject(%ctrl))
+ {
+ continue;
+ }
+
+ %layout = %forward ? %this.fixAfter[%i] : %this.fixBefore[%i];
+
+ // The sizing modes first: a control still set to center or fill
+ // overrides any position or extent written to it, so writing the
+ // geometry while the old mode is still on would not stick.
+ %ctrl.setEditFieldValue("HorizSizing", getField(%layout, 2));
+ %ctrl.setEditFieldValue("VertSizing", getField(%layout, 3));
+ %ctrl.setEditFieldValue("Position", getField(%layout, 0));
+ %ctrl.setEditFieldValue("Extent", getField(%layout, 1));
+ }
+}
+
+// A container lays its children out in list order, and the SimSet ordering
+// methods change that order without telling it - which is why a control put
+// back into a chain kept the slot it briefly held at the end of the list.
+// Adding and removing a child notify on their own; being rearranged does not.
+//
+// Only the parent the control ends up in needs telling: the one it came from
+// hears about it through onChildRemoved.
+function GuiEditorUndoAction::notifyParents(%this, %forward)
+{
+ %told = "";
+
+ for(%i = 0; %i < %this.opCount; %i++)
+ {
+ %type = %this.opType[%i];
+ if(%type $= "order")
+ {
+ %parent = %this.opCtrl[%i];
+ }
+ else if(%type $= "move")
+ {
+ %parent = %forward ? %this.opNewParent[%i] : %this.opOldParent[%i];
+ }
+ else
+ {
+ continue;
+ }
+
+ // The trash is a plain SimGroup, and lays nothing out.
+ if(!isObject(%parent) || !%parent.isMemberOfClass("GuiControl") ||
+ %this.listHas(%told, %parent))
+ {
+ continue;
+ }
+
+ %told = (%told $= "") ? %parent : (%told SPC %parent);
+ %parent.childrenReordered();
+ }
+}
+
+function GuiEditorUndoAction::applyOp(%this, %i, %forward)
+{
+ %ctrl = %this.opCtrl[%i];
+ if(!isObject(%ctrl))
+ {
+ return;
+ }
+
+ %type = %this.opType[%i];
+
+ // Rebuilding a list: adding each child in turn and pushing it to the back
+ // leaves the parent holding exactly the recorded order. add() is what makes
+ // this cover reparenting too - a control listed by one parent is taken from
+ // whichever other one currently holds it - and it means the order the ops
+ // replay in cannot matter, since every control appears in exactly one list.
+ if(%type $= "order")
+ {
+ %list = %forward ? %this.opAfter[%i] : %this.opBefore[%i];
+ for(%w = 0; %w < getWordCount(%list); %w++)
+ {
+ %child = getWord(%list, %w);
+ if(!isObject(%child))
+ {
+ continue;
+ }
+ %ctrl.add(%child);
+ %ctrl.pushToBack(%child);
+ }
+ return;
+ }
+
+ // A list box or drop down's static rows, whole. setItemList replaces the lot
+ // and resizes the control, which is the same thing the pane's every gesture
+ // does, so a replay needs nothing else.
+ if(%type $= "items")
+ {
+ %ctrl.setItemList(%forward ? %this.opAfter[%i] : %this.opBefore[%i]);
+ return;
+ }
+
+ if(%type $= "move")
+ {
+ %parent = %forward ? %this.opNewParent[%i] : %this.opOldParent[%i];
+ %index = %forward ? %this.opNewIndex[%i] : %this.opOldIndex[%i];
+ if(isObject(%parent))
+ {
+ %this.placeAt(%parent, %ctrl, %index);
+ }
+ return;
+ }
+
+ %value = %forward ? %this.opAfter[%i] : %this.opBefore[%i];
+
+ // A profile field holds an id, and a Profile Editor revert frees and reloads
+ // the theme's profiles - so an id recorded before that no longer resolves.
+ // The recorder clears the stack on those paths; this is the belt to its
+ // braces, because writing a dead id would put the control on nothing.
+ if(%ctrl.getFieldType(%this.opField[%i]) $= "GuiProfile" && !isObject(%value))
+ {
+ warn("Gui Editor: skipping undo of " @ %this.opField[%i] @ " - the profile it named is gone.");
+ return;
+ }
+
+ if(%type $= "dynamic")
+ {
+ %ctrl.setFieldValue(%this.opField[%i], %value);
+ return;
+ }
+
+ %ctrl.setEditFieldValue(%this.opField[%i], %value);
+}
+
+// Put %ctrl in %parent at %index, which is the whole of what a move op does.
+// An index of -1 means "wherever" - used for the trash, where order is not a
+// thing anyone can see.
+//
+// add() is a no-op when the control is already a child (SimGroup::addObject
+// returns early on obj->mGroup == this), so the same call covers a reparent and
+// a reorder within one parent. reorderChild inserts before its target, which is
+// why moving down the list has to aim one past the index: erasing the control
+// first shifts everything below it up by one.
+function GuiEditorUndoAction::placeAt(%this, %parent, %ctrl, %index)
+{
+ %parent.add(%ctrl);
+
+ if(%index < 0)
+ {
+ return;
+ }
+
+ %count = %parent.getCount();
+ if(%index >= %count - 1)
+ {
+ %parent.pushToBack(%ctrl);
+ return;
+ }
+
+ %current = %this.indexOf(%parent, %ctrl);
+ if(%current == %index || %current == -1)
+ {
+ return;
+ }
+
+ %target = (%current > %index) ? %parent.getObject(%index) : %parent.getObject(%index + 1);
+ %parent.reorderChild(%ctrl, %target);
+}
+
+function GuiEditorUndoAction::indexOf(%this, %parent, %ctrl)
+{
+ for(%i = 0; %i < %parent.getCount(); %i++)
+ {
+ if(%parent.getObject(%i) == %ctrl)
+ {
+ return %i;
+ }
+ }
+
+ return -1;
+}
diff --git a/editor/GuiEditor/scripts/GuiEditorUndoRecorder.cs b/editor/GuiEditor/scripts/GuiEditorUndoRecorder.cs
new file mode 100644
index 000000000..2519b0534
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiEditorUndoRecorder.cs
@@ -0,0 +1,1027 @@
+
+//-----------------------------------------------------------------------------
+// The Gui Editor's undo recorder. Owned by GuiEditor; the only thing in the
+// editor that touches the UndoManager, and the only thing that builds a
+// GuiEditorUndoAction.
+//
+// Everything that changes the Gui being authored comes through here, by one of
+// three routes:
+//
+// writeField / writeDynamicField a field write. The recorder does the
+// writing, so a write that is not recorded
+// is a write that did not happen.
+// beginGesture / endGesture a gesture with the mouse on the canvas: a
+// drag-move or a handle-resize, which the
+// C++ brackets with onPreEdit/onPostEdit
+// and which can reparent as well as move.
+// snapshot + commitGeometry everything else whose result is only
+// known once it is over - a run of arrow
+// key nudges (bracketed in turn by
+// onPreSelectionNudged/...), an align, a
+// resize from the Layout menu.
+//
+// The C++ edit control has always made all of those callbacks; they are what
+// they were always for.
+//
+// A transaction groups whatever happens between begin() and end() into one
+// action, so that a gesture the user made once is one Ctrl+Z. Transactions
+// nest: the outermost owns the action, and a write outside any transaction
+// gets one of its own.
+//
+// Records name objects by id, so the stack has to be dropped whenever the
+// document or the profiles under it are replaced. See GuiEditor::NewGui,
+// DisplayGuiContent and detachTheme.
+//-----------------------------------------------------------------------------
+
+function GuiEditorUndoRecorder::onAdd(%this)
+{
+ %this.depth = 0;
+ %this.pending = 0;
+ %this.pendingKind = "";
+ %this.suspended = false;
+ %this.lastAction = 0;
+ %this.lastKind = "";
+ %this.snapCount = 0;
+ %this.gestCount = 0;
+ %this.inGesture = false;
+
+ // Both zero: a recorder that has recorded nothing is sitting on a document
+ // nobody has changed. See the serials section below.
+ %this.serialCounter = 0;
+ %this.editSerial = 0;
+ %this.savedSerial = 0;
+
+ %this.hierCount = 0;
+ %this.hierCtrlCount = 0;
+ %this.watchCount = 0;
+ %this.replayTouched = "";
+}
+
+function GuiEditorUndoRecorder::onRemove(%this)
+{
+ // A transaction still open at teardown owns an action nobody will ever
+ // push, and an action that never reached a manager is nobody else's to
+ // free.
+ if(isObject(%this.pending))
+ {
+ %this.pending.delete();
+ %this.pending = 0;
+ }
+}
+
+// The manager is a member of the C++ edit control (guiEditCtrl.cc), registered
+// with it and freed with it, so it is asked for rather than held.
+function GuiEditorUndoRecorder::manager(%this)
+{
+ if(!isObject(%this.owner) || !isObject(%this.owner.brain))
+ {
+ return 0;
+ }
+
+ return %this.owner.brain.getUndoManager();
+}
+
+//-----------------------------------------------------------------------------
+// Transactions.
+//-----------------------------------------------------------------------------
+
+// %kind is only read by the coalescing test; "" means "never merge this with
+// anything".
+function GuiEditorUndoRecorder::begin(%this, %name, %kind)
+{
+ if(%this.suspended)
+ {
+ return;
+ }
+
+ %this.depth++;
+ if(%this.depth > 1)
+ {
+ return;
+ }
+
+ %this.pending = new UndoScriptAction()
+ {
+ class = "GuiEditorUndoAction";
+ actionName = (%name $= "") ? "Edit" : %name;
+ recorder = %this;
+ };
+ %this.pendingKind = %kind;
+}
+
+function GuiEditorUndoRecorder::end(%this)
+{
+ if(%this.suspended || %this.depth <= 0)
+ {
+ return;
+ }
+
+ %this.depth--;
+ if(%this.depth > 0)
+ {
+ return;
+ }
+
+ %action = %this.pending;
+ %this.pending = 0;
+ if(!isObject(%action))
+ {
+ return;
+ }
+
+ // Nothing happened between the begin and the end: a click that selected
+ // without moving, a text box the user only tabbed through, a commit whose
+ // value was already what it is. Not an undo step.
+ if(%action.isEmpty())
+ {
+ %action.delete();
+ return;
+ }
+
+ if(%this.canCoalesce(%action))
+ {
+ %this.lastAction.mergeFrom(%action);
+ %action.delete();
+
+ // The surviving action now ends somewhere new, so it needs a serial that
+ // has never been seen. Its priorSerial is left alone: undoing it still
+ // takes the document back to before the whole run.
+ %this.lastAction.serial = %this.nextSerial();
+ %this.moveTo(%this.lastAction.serial);
+ return;
+ }
+
+ // Now that the operation is over, what any frame set involved in it ended up
+ // looking like.
+ %this.emitFrameFixes(%action);
+
+ // Both ends recorded before the document is said to have moved: priorSerial
+ // is where it was standing, serial is where this action puts it.
+ %action.priorSerial = %this.editSerial;
+ %action.serial = %this.nextSerial();
+
+ %action.addToManager(%this.manager());
+ %this.lastAction = %action;
+ %this.lastKind = %this.pendingKind;
+ %this.moveTo(%action.serial);
+ %this.refreshMenu();
+}
+
+// Holding an arrow key down is one move, not thirty. Only a like-for-like
+// repeat merges: the same kind of gesture, touching exactly the same controls,
+// with nothing in between. lastAction is dropped by any replay and any clear,
+// so a merge can never reach an action that is no longer on top of the stack.
+function GuiEditorUndoRecorder::canCoalesce(%this, %action)
+{
+ if(%this.pendingKind $= "" || %this.pendingKind !$= %this.lastKind)
+ {
+ return false;
+ }
+
+ if(!isObject(%this.lastAction))
+ {
+ return false;
+ }
+
+ return %this.lastAction.touched() $= %action.touched();
+}
+
+//-----------------------------------------------------------------------------
+// Serials: whether the document has changed since it was last saved.
+//
+// The recorder answers this because it is already the one thing every change
+// goes through, so an edit that is not recorded is an edit that did not happen.
+//
+// It cannot be answered by counting. UndoManager offers getUndoCount,
+// getRedoCount and the two name lookups and nothing else -- there is no way to
+// ask which action is on top -- and a count is not an identity anyway: save at a
+// depth of five, undo once, make a DIFFERENT edit, and the depth is five again
+// with a different document underneath it. A flag built on depth reads clean
+// there, which is the one direction it must never be wrong in.
+//
+// So every action carries two numbers instead. serial is where the document
+// stands once that action has been applied; priorSerial is where it stood
+// before. Both come from a counter that never repeats, so a value identifies a
+// state rather than a position:
+//
+// undo of A -> the document is at A.priorSerial
+// redo of A -> the document is at A.serial
+//
+// and editSerial is wherever the document is now. markClean records it,
+// isModified compares against it. Undo back to the state that was saved and the
+// two match again; branch off differently and they cannot, because the new
+// action's serial has never existed before.
+//
+// Nothing here mirrors the C++ stack, so nothing can drift out of step with it.
+// An action trimmed off the bottom of the manager's stack simply never replays,
+// so its serial never comes back -- the safe direction.
+//
+// clear() taking a fresh serial is what keeps GuiEditor::detachTheme honest: it
+// empties the stack because every record names a profile about to be freed, and
+// the controls are exactly as edited afterwards as they were before.
+//-----------------------------------------------------------------------------
+
+function GuiEditorUndoRecorder::nextSerial(%this)
+{
+ %this.serialCounter++;
+ return %this.serialCounter;
+}
+
+// Say where the document now stands, and tell whoever is showing that.
+function GuiEditorUndoRecorder::moveTo(%this, %serial)
+{
+ %this.editSerial = %serial;
+
+ if(isObject(%this.owner))
+ {
+ %this.owner.refreshDocumentTitle();
+ }
+}
+
+// This is the document as it now stands on disk. Called after a save, and after
+// a new or freshly opened document, which are clean by definition.
+function GuiEditorUndoRecorder::markClean(%this)
+{
+ %this.savedSerial = %this.editSerial;
+
+ if(isObject(%this.owner))
+ {
+ %this.owner.refreshDocumentTitle();
+ }
+}
+
+function GuiEditorUndoRecorder::isModified(%this)
+{
+ return %this.editSerial != %this.savedSerial;
+}
+
+//-----------------------------------------------------------------------------
+// Writes. The recorder does the writing so that recording cannot be forgotten.
+//-----------------------------------------------------------------------------
+
+// setEditFieldValue rather than a plain assignment: it brackets the write with
+// inspectPreApply / inspectPostApply, which sleeps and wakes the control (so a
+// re-profile releases the old profile and takes a count on the new one) and
+// runs the protected-field setters - Position and Extent are protected fields
+// whose setters call setPosition/setExtent.
+function GuiEditorUndoRecorder::writeField(%this, %ctrl, %field, %value)
+{
+ if(!isObject(%ctrl))
+ {
+ return;
+ }
+
+ %before = %this.fieldSnapshot(%ctrl, %field);
+ %ctrl.setEditFieldValue(%field, %value);
+ %this.recordField(%ctrl, %field, %before, %this.fieldSnapshot(%ctrl, %field), false);
+}
+
+function GuiEditorUndoRecorder::writeDynamicField(%this, %ctrl, %field, %value)
+{
+ if(!isObject(%ctrl))
+ {
+ return;
+ }
+
+ %before = %ctrl.getFieldValue(%field);
+ %ctrl.setFieldValue(%field, %value);
+ %this.recordField(%ctrl, %field, %before, %ctrl.getFieldValue(%field), true);
+}
+
+// A list box or drop down's static rows. Not a field write, because the rows are
+// not a field: they are TAML custom nodes, and getItemList/setItemList is the
+// only handle script has on all of them at once - the same arrangement a frame
+// set's layout has.
+//
+// The whole list at each end rather than the one row that changed. Every gesture
+// the Items pane offers - add, remove, move up, move down, retype a caption -
+// can shift what is around it, so a per-row record would have to know which of
+// them it was. A list is short, and restoring one cannot be got subtly wrong.
+function GuiEditorUndoRecorder::writeItems(%this, %ctrl, %list)
+{
+ if(!isObject(%ctrl))
+ {
+ return;
+ }
+
+ %before = %ctrl.getItemList();
+ %ctrl.setItemList(%list);
+ %after = %ctrl.getItemList();
+
+ if(%this.suspended || strcmp(%before, %after) == 0)
+ {
+ return;
+ }
+
+ %owned = %this.autoBegin("Change Items");
+ if(isObject(%this.pending))
+ {
+ %this.pending.addItemsOp(%ctrl, %before, %after);
+ }
+ %this.autoEnd(%owned);
+}
+
+// What the field holds, in the form an undo should write back.
+//
+// A profile slot is read as an id, never the name the field reads back: the
+// editor runs in editor mode, where SimObject::assignName stashes a new
+// object's name instead of registering it, so a profile made this session
+// cannot be found by name. GuiEditorThemeApplier::fieldProfile already knows
+// how to resolve one either way.
+//
+// A control's own name is asked of the object for the same reason -
+// getFieldValue answers "" for it in editor mode.
+function GuiEditorUndoRecorder::fieldSnapshot(%this, %ctrl, %field)
+{
+ if(%ctrl.getFieldType(%field) $= "GuiProfile")
+ {
+ %profile = %this.owner.themeApplier.fieldProfile(%ctrl, %field);
+ return isObject(%profile) ? %profile.getId() : "";
+ }
+
+ if(%field $= "name")
+ {
+ return %ctrl.getName();
+ }
+
+ return %ctrl.getFieldValue(%field);
+}
+
+// Reading the value back after the write, rather than trusting what was asked
+// for, is what keeps redo faithful: the engine normalises plenty of them - a
+// bool, a point, a profile written by id and read back by name.
+function GuiEditorUndoRecorder::recordField(%this, %ctrl, %field, %before, %after, %isDynamic)
+{
+ if(%this.suspended || !isObject(%ctrl))
+ {
+ return;
+ }
+
+ // strcmp, not $=: $= runs dStricmp, so recasing a caption would read as no
+ // change at all and vanish from the stack.
+ if(strcmp(%before, %after) == 0)
+ {
+ return;
+ }
+
+ %owned = %this.autoBegin("Change " @ %field);
+ if(isObject(%this.pending))
+ {
+ %this.pending.addFieldOp(%ctrl, %field, %before, %after, %isDynamic);
+ }
+ %this.autoEnd(%owned);
+}
+
+//-----------------------------------------------------------------------------
+// Structure: a control changing parent, index, or both.
+//-----------------------------------------------------------------------------
+
+// Called after the control has arrived. Undoing an add moves the control to
+// the trash rather than deleting it, which is what leaves redo something to
+// put back.
+function GuiEditorUndoRecorder::recordAdd(%this, %ctrl, %name)
+{
+ if(%this.suspended || !isObject(%ctrl))
+ {
+ return;
+ }
+
+ %parent = %ctrl.getParent();
+ if(!isObject(%parent))
+ {
+ return;
+ }
+
+ %this.watchFrameSet(%parent);
+
+ %owned = %this.autoBegin((%name $= "") ? "Add Control" : %name);
+ if(isObject(%this.pending))
+ {
+ %this.pending.addMoveOp(%ctrl, %this.trash(), -1, %parent, %this.indexOf(%parent, %ctrl));
+
+ // The layout it arrived with, which is what a redo has to put back: a
+ // container that places its own children will have taken it once
+ // already. Both ends are the same value - undo sends the control to the
+ // trash, where its layout is nobody's business.
+ %layout = %this.layoutOf(%ctrl);
+ %this.pending.addLayoutFix(%ctrl, %layout, %layout);
+ }
+ %this.autoEnd(%owned);
+}
+
+// Called before the control is trashed - the C++ fires onTrashSelection ahead
+// of the move for this reason - because where it came from is exactly what is
+// about to be lost.
+function GuiEditorUndoRecorder::recordDelete(%this, %ctrl, %name)
+{
+ if(%this.suspended || !isObject(%ctrl))
+ {
+ return;
+ }
+
+ %parent = %ctrl.getParent();
+ if(!isObject(%parent))
+ {
+ return;
+ }
+
+ // Before the trash move, which is the last moment the frame it stands in
+ // still exists.
+ %this.watchFrameSet(%parent);
+
+ %owned = %this.autoBegin((%name $= "") ? "Delete Control" : %name);
+ if(isObject(%this.pending))
+ {
+ %this.pending.addMoveOp(%ctrl, %parent, %this.indexOf(%parent, %ctrl), %this.trash(), -1);
+
+ // How it was laid out, which the parent will take from it the moment it
+ // is put back, if the parent is one that places its own children.
+ %layout = %this.layoutOf(%ctrl);
+ %this.pending.addLayoutFix(%ctrl, %layout, %layout);
+ }
+ %this.autoEnd(%owned);
+}
+
+// A whole selection on its way to the trash, recorded highest index first.
+//
+// Order is load-bearing. An undo replays the ops backwards, so recording them
+// in descending index order restores them in ascending order - and restoring
+// low to high is the only order that lands them all where they were, because
+// putting a control back at index 3 shifts everything from 3 down one place.
+function GuiEditorUndoRecorder::recordDeleteSelection(%this, %selection)
+{
+ if(%this.suspended || !isObject(%selection))
+ {
+ return;
+ }
+
+ %count = %selection.getCount();
+ for(%i = 0; %i < %count; %i++)
+ {
+ %list[%i] = %selection.getObject(%i);
+ }
+
+ %this.begin("Delete Control", "");
+ for(%out = 0; %out < %count; %out++)
+ {
+ %pick = -1;
+ for(%i = 0; %i < %count; %i++)
+ {
+ if(%list[%i] $= "" || !isObject(%list[%i]))
+ {
+ continue;
+ }
+ if(%pick == -1 || %this.parentIndexOf(%list[%i]) > %this.parentIndexOf(%list[%pick]))
+ {
+ %pick = %i;
+ }
+ }
+
+ if(%pick == -1)
+ {
+ break;
+ }
+
+ %this.recordDelete(%list[%pick], "");
+ %list[%pick] = "";
+ }
+ %this.end();
+}
+
+function GuiEditorUndoRecorder::parentIndexOf(%this, %ctrl)
+{
+ %parent = %ctrl.getParent();
+ return isObject(%parent) ? %this.indexOf(%parent, %ctrl) : -1;
+}
+
+// Called after the move, with where the control used to be.
+function GuiEditorUndoRecorder::recordMove(%this, %ctrl, %oldParent, %oldIndex, %name)
+{
+ if(%this.suspended || !isObject(%ctrl) || !isObject(%oldParent))
+ {
+ return;
+ }
+
+ %parent = %ctrl.getParent();
+ if(!isObject(%parent))
+ {
+ return;
+ }
+
+ %index = %this.indexOf(%parent, %ctrl);
+ if(%parent == %oldParent && %index == %oldIndex)
+ {
+ return;
+ }
+
+ %owned = %this.autoBegin((%name $= "") ? "Move Control" : %name);
+ if(isObject(%this.pending))
+ {
+ %this.pending.addMoveOp(%ctrl, %oldParent, %oldIndex, %parent, %index);
+ }
+ %this.autoEnd(%owned);
+}
+
+//-----------------------------------------------------------------------------
+// Frame sets, the one container that keeps a layout of its own beside the child
+// list. Removing a control destroys the frame it stood in and merges the split
+// into its twin, so the tree has to be kept before the removal and put back
+// after -- getFrameLayout / setFrameLayout, which exist for this.
+//
+// The tree afterwards is only knowable once the operation is over, so what is
+// taken here is the before, and the after is read when the transaction closes.
+//-----------------------------------------------------------------------------
+
+function GuiEditorUndoRecorder::watchFrameSet(%this, %parent)
+{
+ if(%this.suspended || !isObject(%parent) || !%parent.isMemberOfClass("GuiFrameSetCtrl"))
+ {
+ return;
+ }
+
+ for(%i = 0; %i < %this.watchCount; %i++)
+ {
+ if(%this.watchSet[%i] == %parent)
+ {
+ return;
+ }
+ }
+
+ %i = %this.watchCount;
+ %this.watchSet[%i] = %parent;
+ %this.watchBefore[%i] = %parent.getFrameLayout();
+ %this.watchCount = %i + 1;
+}
+
+function GuiEditorUndoRecorder::emitFrameFixes(%this, %action)
+{
+ for(%i = 0; %i < %this.watchCount; %i++)
+ {
+ %frameSet = %this.watchSet[%i];
+ if(isObject(%frameSet))
+ {
+ %action.addFrameFix(%frameSet, %this.watchBefore[%i], %frameSet.getFrameLayout());
+ }
+ }
+
+ %this.watchCount = 0;
+}
+
+// Everything a container can take from a child when it arrives, in the order
+// GuiEditorUndoAction::applyLayoutFixes reads it back out. TAB-delimited
+// because a position has a space in it.
+function GuiEditorUndoRecorder::layoutOf(%this, %ctrl)
+{
+ return %ctrl.getPosition() TAB %ctrl.getExtent() TAB
+ %ctrl.getFieldValue("HorizSizing") TAB %ctrl.getFieldValue("VertSizing");
+}
+
+// The edit control's trash: a SimGroup it owns and never empties, which is
+// where deleteSelection puts controls instead of deleting them.
+function GuiEditorUndoRecorder::trash(%this)
+{
+ return %this.owner.brain.getTrash();
+}
+
+function GuiEditorUndoRecorder::indexOf(%this, %parent, %ctrl)
+{
+ for(%i = 0; %i < %parent.getCount(); %i++)
+ {
+ if(%parent.getObject(%i) == %ctrl)
+ {
+ return %i;
+ }
+ }
+
+ return -1;
+}
+
+// A record made outside any transaction is its own undo step. Returns whether
+// it opened one, so the matching end only fires for the one that did.
+function GuiEditorUndoRecorder::autoBegin(%this, %name)
+{
+ if(%this.depth > 0)
+ {
+ return false;
+ }
+
+ %this.begin(%name, "");
+ return true;
+}
+
+function GuiEditorUndoRecorder::autoEnd(%this, %owned)
+{
+ if(%owned)
+ {
+ %this.end();
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Geometry, which is only known to have changed once the gesture is over.
+//-----------------------------------------------------------------------------
+
+// %selection is the edit control's selected set, which is one reused SimSet
+// (guiEditCtrl.cc, updateSelectedSet) - so its contents are copied out here
+// rather than the set being held on to.
+function GuiEditorUndoRecorder::snapshot(%this, %selection)
+{
+ %this.snapCount = 0;
+
+ // Nothing to do while a canvas gesture is running: the moves inside one are
+ // the engine's way of dragging, not edits of their own, and the gesture keeps
+ // its own snapshot of where everything started. See beginGesture.
+ if(%this.suspended || %this.inGesture || !isObject(%selection))
+ {
+ return;
+ }
+
+ for(%i = 0; %i < %selection.getCount(); %i++)
+ {
+ %ctrl = %selection.getObject(%i);
+ %this.snapCtrl[%i] = %ctrl;
+ %this.snapPos[%i] = %ctrl.getPosition();
+ %this.snapExt[%i] = %ctrl.getExtent();
+ }
+
+ %this.snapCount = %selection.getCount();
+}
+
+// Diff against the snapshot and push whatever actually moved. A mouse-down
+// that selected without dragging, or a nudge into a wall, records nothing.
+function GuiEditorUndoRecorder::commitGeometry(%this, %name, %kind)
+{
+ if(%this.suspended || %this.inGesture || %this.snapCount <= 0)
+ {
+ return;
+ }
+
+ // An empty name means "say what happened". The C++ knows whether the gesture
+ // was a drag or a resize (mMouseDownMode) but does not pass it, and the
+ // snapshot can answer it anyway.
+ if(%name $= "")
+ {
+ %name = "Move Control";
+ for(%i = 0; %i < %this.snapCount; %i++)
+ {
+ %ctrl = %this.snapCtrl[%i];
+ if(isObject(%ctrl) && strcmp(%this.snapExt[%i], %ctrl.getExtent()) != 0)
+ {
+ %name = "Resize Control";
+ break;
+ }
+ }
+ }
+
+ %this.begin(%name, %kind);
+ for(%i = 0; %i < %this.snapCount; %i++)
+ {
+ %ctrl = %this.snapCtrl[%i];
+ if(!isObject(%ctrl))
+ {
+ continue;
+ }
+
+ %this.recordField(%ctrl, "Position", %this.snapPos[%i], %ctrl.getPosition(), false);
+ %this.recordField(%ctrl, "Extent", %this.snapExt[%i], %ctrl.getExtent(), false);
+ }
+ %this.end();
+
+ %this.snapCount = 0;
+}
+
+//-----------------------------------------------------------------------------
+// A canvas gesture: a drag-move or a handle-resize, which the C++ brackets with
+// onPreEdit and onPostEdit.
+//
+// The whole gesture is one action taken from one snapshot -- where each selected
+// control stood when the mouse went down, and where it stands when the mouse
+// comes up. The nudge callbacks that arrive in between are ignored for the
+// duration: guiEditCtrl.cc drags by calling moveSelection once per mouse-move
+// event, and each of those brackets itself with the nudge pair, so left to
+// themselves they record a hundred small moves that then have to coalesce back
+// into the one move the user made.
+//
+// Ignoring them is also what makes a drag across a container boundary come out
+// right. Halfway through the gesture the C++ reparents the selection into
+// whatever container is under the cursor and rewrites each control's position to
+// keep it there (onTouchDragged -> moveSelectionToCtrl), announcing neither -- so
+// a record built frame by frame ends up holding a position that means something
+// only inside the parent the control has just left, and no record of the parent
+// at all. Undo then put the control somewhere it had never been.
+//-----------------------------------------------------------------------------
+
+function GuiEditorUndoRecorder::beginGesture(%this, %selection)
+{
+ %this.gestCount = 0;
+ %this.inGesture = true;
+
+ if(%this.suspended || !isObject(%selection))
+ {
+ return;
+ }
+
+ for(%i = 0; %i < %selection.getCount(); %i++)
+ {
+ %ctrl = %selection.getObject(%i);
+ %this.gestCtrl[%i] = %ctrl;
+ %this.gestPos[%i] = %ctrl.getPosition();
+ %this.gestExt[%i] = %ctrl.getExtent();
+ %this.gestLayout[%i] = %this.layoutOf(%ctrl);
+ %this.gestParent[%i] = %ctrl.getParent();
+ %this.gestIndex[%i] = %this.parentIndexOf(%ctrl);
+ }
+
+ %this.gestCount = %selection.getCount();
+}
+
+function GuiEditorUndoRecorder::endGesture(%this)
+{
+ %this.inGesture = false;
+
+ if(%this.suspended || %this.gestCount <= 0)
+ {
+ return;
+ }
+
+ %this.begin(%this.gestureName(), "");
+ for(%i = 0; %i < %this.gestCount; %i++)
+ {
+ %ctrl = %this.gestCtrl[%i];
+ if(!isObject(%ctrl))
+ {
+ continue;
+ }
+
+ // A control that changed parent has its geometry put back by the layout
+ // fix rather than by a field op. The fix carries the sizing modes as well
+ // as the bounds and is applied after every op has run, so it is the one
+ // that wins where the container places its own children -- and one writer
+ // per control is one less pair of records that can disagree.
+ if(%ctrl.getParent() != %this.gestParent[%i])
+ {
+ %this.watchFrameSet(%this.gestParent[%i]);
+ %this.watchFrameSet(%ctrl.getParent());
+
+ %this.recordMove(%ctrl, %this.gestParent[%i], %this.gestIndex[%i], "");
+ if(isObject(%this.pending))
+ {
+ %this.pending.addLayoutFix(%ctrl, %this.gestLayout[%i], %this.layoutOf(%ctrl));
+ }
+ continue;
+ }
+
+ %this.recordField(%ctrl, "Position", %this.gestPos[%i], %ctrl.getPosition(), false);
+ %this.recordField(%ctrl, "Extent", %this.gestExt[%i], %ctrl.getExtent(), false);
+ }
+ %this.end();
+
+ %this.gestCount = 0;
+}
+
+// What the gesture turned out to be, which is only knowable now it is over. The
+// C++ knows whether the mouse took a handle or the body (mMouseDownMode) but
+// does not pass it, and the snapshot can answer anyway.
+function GuiEditorUndoRecorder::gestureName(%this)
+{
+ %name = "Move Control";
+
+ for(%i = 0; %i < %this.gestCount; %i++)
+ {
+ %ctrl = %this.gestCtrl[%i];
+ if(!isObject(%ctrl))
+ {
+ continue;
+ }
+
+ if(%ctrl.getParent() != %this.gestParent[%i])
+ {
+ return "Reparent Control";
+ }
+
+ if(strcmp(%this.gestExt[%i], %ctrl.getExtent()) != 0)
+ {
+ %name = "Resize Control";
+ }
+ }
+
+ return %name;
+}
+
+//-----------------------------------------------------------------------------
+// Hierarchy, for a rearrangement whose shape is not known until it is over.
+//
+// A drag in the Explorer tree can move any number of selected controls into any
+// number of parents at once (GuiTreeViewCtrl::reorderFromDrag), so rather than
+// try to follow it, the whole document's shape is remembered before and read
+// again after. What changed is then whichever parents' child lists differ.
+//-----------------------------------------------------------------------------
+
+function GuiEditorUndoRecorder::snapshotHierarchy(%this, %root)
+{
+ %this.hierCount = 0;
+ %this.hierCtrlCount = 0;
+
+ // Taken here rather than in commitHierarchy: a drag that pulls a control out
+ // of a frame set destroys the frame on the way, so by the time the move is
+ // over the tree it came from is already gone.
+ %this.watchCount = 0;
+
+ if(%this.suspended || !isObject(%root))
+ {
+ return;
+ }
+
+ %this.hierWalk(%root);
+}
+
+function GuiEditorUndoRecorder::hierWalk(%this, %parent)
+{
+ %n = %this.hierCount;
+ %this.hierParent[%n] = %parent;
+ %this.hierList[%n] = %this.childList(%parent);
+ %this.hierCount = %n + 1;
+
+ for(%i = 0; %i < %parent.getCount(); %i++)
+ {
+ %ctrl = %parent.getObject(%i);
+
+ // Per control as well as per parent: the lists are what gets restored,
+ // but they cannot say which control the user actually dragged, and that
+ // is what the selection should land on afterwards.
+ %c = %this.hierCtrlCount;
+ %this.hierCtrl[%c] = %ctrl;
+ %this.hierCtrlParent[%c] = %parent;
+ %this.hierCtrlIndex[%c] = %i;
+ %this.hierCtrlLayout[%c] = %this.layoutOf(%ctrl);
+ %this.hierCtrlCount = %c + 1;
+
+ // Every frame set in the document, whether the drag turns out to touch
+ // it or not: which ones it touches is not known until it is over, and by
+ // then their trees have already changed.
+ %this.watchFrameSet(%ctrl);
+
+ %this.hierWalk(%ctrl);
+ }
+}
+
+function GuiEditorUndoRecorder::childList(%this, %parent)
+{
+ %list = "";
+ for(%i = 0; %i < %parent.getCount(); %i++)
+ {
+ %child = %parent.getObject(%i);
+ %list = (%list $= "") ? %child : (%list SPC %child);
+ }
+
+ return %list;
+}
+
+function GuiEditorUndoRecorder::commitHierarchy(%this, %name)
+{
+ if(%this.suspended || %this.hierCount <= 0)
+ {
+ return;
+ }
+
+ %this.begin(%name, "");
+
+ for(%i = 0; %i < %this.hierCount; %i++)
+ {
+ %parent = %this.hierParent[%i];
+ if(!isObject(%parent))
+ {
+ continue;
+ }
+
+ %now = %this.childList(%parent);
+ if(%this.hierList[%i] $= %now)
+ {
+ continue;
+ }
+
+ %this.pending.addOrderOp(%parent, %this.hierList[%i], %now);
+ }
+
+ // Which controls moved, for the selection after a replay.
+ if(isObject(%this.pending) && !%this.pending.isEmpty())
+ {
+ for(%i = 0; %i < %this.hierCtrlCount; %i++)
+ {
+ %ctrl = %this.hierCtrl[%i];
+ if(!isObject(%ctrl))
+ {
+ continue;
+ }
+
+ %parent = %ctrl.getParent();
+ if(%parent != %this.hierCtrlParent[%i] ||
+ %this.indexOf(%parent, %ctrl) != %this.hierCtrlIndex[%i])
+ {
+ %this.pending.noteTouched(%ctrl);
+
+ // Dragged into a container that places its own children, its
+ // layout is no longer its own - so both ends are kept and put
+ // back after the lists are.
+ %this.pending.addLayoutFix(%ctrl, %this.hierCtrlLayout[%i],
+ %this.layoutOf(%ctrl));
+ }
+ }
+ }
+
+ %this.end();
+
+ %this.hierCount = 0;
+ %this.hierCtrlCount = 0;
+}
+
+//-----------------------------------------------------------------------------
+// Replaying, and the state that has to be dropped when one happens.
+//-----------------------------------------------------------------------------
+
+function GuiEditorUndoRecorder::suspend(%this)
+{
+ %this.suspended = true;
+}
+
+function GuiEditorUndoRecorder::resume(%this)
+{
+ %this.suspended = false;
+}
+
+// Called by the action as it replays, both ways. Three jobs: hand GuiEditor the
+// controls to re-select afterwards, drop the coalescing state - the action on
+// top of the undo stack is no longer the one a merge would be aiming at - and
+// move the document to whichever end of this action the replay is heading for.
+function GuiEditorUndoRecorder::noteReplay(%this, %action, %forward)
+{
+ %this.replayTouched = %action.touched();
+ %this.lastAction = 0;
+ %this.lastKind = "";
+
+ %this.moveTo(%forward ? %action.serial : %action.priorSerial);
+}
+
+function GuiEditorUndoRecorder::clear(%this)
+{
+ if(isObject(%this.pending))
+ {
+ %this.pending.delete();
+ %this.pending = 0;
+ }
+ %this.depth = 0;
+
+ %manager = %this.manager();
+ if(isObject(%manager))
+ {
+ %manager.clearAll();
+ }
+
+ %this.lastAction = 0;
+ %this.lastKind = "";
+ %this.snapCount = 0;
+
+ // Including a gesture left open. Nothing clears the stack in the middle of a
+ // drag, but a gesture whose mouse-up never arrived would otherwise go on
+ // swallowing every move made after it.
+ %this.gestCount = 0;
+ %this.inGesture = false;
+
+ // A state nothing can replay its way back to. Losing the records does not
+ // un-edit the controls, and the actions that could have carried the document
+ // home have just been freed - so from here the only route back to clean is a
+ // save. The callers that DO leave a clean document (a new one, a freshly
+ // opened one) say so themselves by calling markClean afterwards.
+ %this.moveTo(%this.nextSerial());
+
+ %this.replayTouched = "";
+ %this.refreshMenu();
+}
+
+//-----------------------------------------------------------------------------
+// The Edit menu.
+//-----------------------------------------------------------------------------
+
+function GuiEditorUndoRecorder::undoCount(%this)
+{
+ %manager = %this.manager();
+ return isObject(%manager) ? %manager.getUndoCount() : 0;
+}
+
+function GuiEditorUndoRecorder::redoCount(%this)
+{
+ %manager = %this.manager();
+ return isObject(%manager) ? %manager.getRedoCount() : 0;
+}
+
+// Called on every recorded change, which is often - a run of nudges is one per
+// key press. That used to be worth caching against, because greying an item meant
+// walking the whole menu tree by item text and re-applying every item's profile.
+// The set holds the two items by handle now, and this is two flag writes.
+function GuiEditorUndoRecorder::refreshMenu(%this)
+{
+ if(isObject(%this.owner) && isObject(%this.owner.menus))
+ {
+ %this.owner.menus.refreshUndo(%this.undoCount(), %this.redoCount());
+ }
+}
diff --git a/editor/GuiEditor/scripts/GuiProfileEditorBorderForm.cs b/editor/GuiEditor/scripts/GuiProfileEditorBorderForm.cs
new file mode 100644
index 000000000..d98bdfedd
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiProfileEditorBorderForm.cs
@@ -0,0 +1,88 @@
+
+//-----------------------------------------------------------------------------
+// The custom border-editing pane shown in place of the generic inspector when a
+// border node is selected in the Gui Profile Editor. It shows the border's name
+// (display only -- renaming a border that profiles reference by name is
+// deliberately not offered here) above a shared GuiProfileEditorBorderGrid,
+// which does all the real editing. Every grid commit is forwarded to the dialog,
+// which marks the theme dirty and refreshes the preview -- the same path the
+// inspector's onProfileChanged took.
+//
+// The creator sets the dialog back-pointer inline and an initial Extent (the
+// grid lays out within that width). Call build() once after adding it to its
+// scroller, then bind()/unbind() to attach a border.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorBorderForm::onAdd(%this)
+{
+ ThemeManager.setProfile(%this, "emptyProfile");
+}
+
+function GuiProfileEditorBorderForm::build(%this)
+{
+ %w = getWord(%this.extent, 0);
+ %pad = 8;
+ %inner = %w - %pad * 2;
+
+ // Name header: display only.
+ %this.nameLabel = new GuiControl()
+ {
+ Position = %pad SPC %pad;
+ Extent = %inner SPC 24;
+ Text = "Border:";
+ align = "left";
+ vAlign = "middle";
+ };
+ ThemeManager.setProfile(%this.nameLabel, "labelProfile");
+ %this.add(%this.nameLabel);
+
+ %gridY = %pad + 32;
+ %this.grid = new GuiControl()
+ {
+ class = "GuiProfileEditorBorderGrid";
+ Position = %pad SPC %gridY;
+ gridWidth = %inner;
+ owner = %this;
+ };
+ %this.add(%this.grid);
+ %this.grid.build();
+
+ %this.setExtent(%w, %gridY + %this.grid.gridHeight + %pad);
+}
+
+//-----------------------------------------------------------------------------
+// Binding.
+//-----------------------------------------------------------------------------
+
+// %label is the friendly border name shown in the tree (its category), used for
+// the display-only header; the grid edits the %border object itself.
+function GuiProfileEditorBorderForm::bind(%this, %border, %label)
+{
+ if(!isObject(%border))
+ {
+ %this.unbind();
+ return;
+ }
+ %this.border = %border;
+ %this.nameLabel.setText("Border: " @ %label);
+ %this.grid.bind(%border);
+}
+
+function GuiProfileEditorBorderForm::unbind(%this)
+{
+ %this.border = "";
+ if(isObject(%this.grid))
+ {
+ %this.grid.unbind();
+ }
+}
+
+// The shared grid forwards every edit here; route it to the dialog's border
+// commit sink (mark the theme dirty + refresh the preview).
+function GuiProfileEditorBorderForm::onBorderGridCommit(%this)
+{
+ if(isObject(%this.dialog))
+ {
+ %this.dialog.onBorderChanged();
+ }
+}
diff --git a/editor/GuiEditor/scripts/GuiProfileEditorBorderGrid.cs b/editor/GuiEditor/scripts/GuiProfileEditorBorderGrid.cs
new file mode 100644
index 000000000..7496be560
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiProfileEditorBorderGrid.cs
@@ -0,0 +1,270 @@
+
+//-----------------------------------------------------------------------------
+// A reusable editor for the sixteen values that matter for a GuiBorderProfile:
+// margin / border / border-color / padding, each across the four control states
+// normal / HL / SL / NA -- plus the border's underfill flag. Laid out as four
+// labelled rows of four inputs (numeric boxes, or color-popup swatches for the
+// color row) with a state caption under each column, and an Underfill checkbox
+// below the grid.
+//
+// The grid never decides what it edits or where edits go: the host sets .target
+// (the GuiBorderProfile to read/write) and .owner (notified after every commit
+// via owner.onBorderGridCommit()). The same grid therefore serves both the
+// Borders pane's "Custom..." slot (editing a hidden single-use border) and the
+// Border pane (editing a theme's named border in place).
+//
+// The creator sets these fields inline: gridWidth (the content width to lay out
+// within) and owner. Call build() once after adding the grid to a container,
+// then bind()/unbind() to attach a border. build() records the laid-out height
+// in .gridHeight so the host can size its container to fit.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorBorderGrid::onAdd(%this)
+{
+ ThemeManager.setProfile(%this, "emptyProfile");
+}
+
+// The four border-profile field state suffixes (box 0 is the normal state,
+// drawn with no caption).
+function GuiProfileEditorBorderGrid::stateSuffix(%this, %i)
+{
+ return getWord("_ HL SL NA", %i);
+}
+
+function GuiProfileEditorBorderGrid::stateField(%this, %field, %i)
+{
+ return (%i == 0) ? %field : (%field @ %this.stateSuffix(%i));
+}
+
+// Build the four property blocks (label, four inputs, four captions) and the
+// underfill checkbox beneath them.
+function GuiProfileEditorBorderGrid::build(%this)
+{
+ %w = %this.gridWidth;
+ %blockH = 58;
+ %boxW = 42;
+ %boxGap = 4;
+ %x0 = 8;
+
+ %rows = "Margin" TAB "margin" TAB "num" NL "Border" TAB "border" TAB "num" NL "Border Color" TAB "borderColor" TAB "color" NL "Padding" TAB "padding" TAB "num";
+
+ %count = getRecordCount(%rows);
+ for(%r = 0; %r < %count; %r++)
+ {
+ %rec = getRecord(%rows, %r);
+ %title = getField(%rec, 0);
+ %field = getField(%rec, 1);
+ %isColor = getField(%rec, 2) $= "color";
+ %y = %r * %blockH + 2;
+
+ %rowLabel = new GuiControl()
+ {
+ Position = %x0 SPC %y;
+ Extent = (%w - %x0 - 4) SPC 16;
+ Text = %title;
+ align = "left";
+ };
+ ThemeManager.setProfile(%rowLabel, "labelProfile");
+ %this.add(%rowLabel);
+
+ for(%i = 0; %i < 4; %i++)
+ {
+ %bx = %x0 + %i * (%boxW + %boxGap);
+ %by = %y + 18;
+
+ if(%isColor)
+ {
+ %box = new GuiColorPopupCtrl()
+ {
+ class = "GuiProfileEditorColorPopup";
+ Position = %bx SPC %by;
+ Extent = %boxW SPC 22;
+ showColorValues = true;
+ };
+ ThemeManager.setProfile(%box, "colorPickerProfile");
+ ThemeManager.setProfile(%box, "emptyProfile", "backgroundProfile");
+ ThemeManager.setProfile(%box, "colorPopupProfile", "popupProfile");
+ ThemeManager.setProfile(%box, "emptyProfile", "pickerProfile");
+ ThemeManager.setProfile(%box, "colorPickerSelectorProfile", "selectorProfile");
+ ThemeManager.setProfile(%box, "textEditProfile", "valueProfile");
+ ThemeManager.setProfile(%box, "tipProfile", "TooltipProfile");
+ %box.isColor = true;
+ %box.Command = %this.getID() @ ".commitBox(" @ %box.getID() @ ");";
+ }
+ else
+ {
+ %box = new GuiTextEditCtrl()
+ {
+ class = "GuiProfileEditorBorderInput";
+ Position = %bx SPC %by;
+ Extent = %boxW SPC 22;
+ inputMode = "Number";
+ align = "center";
+ };
+ ThemeManager.setProfile(%box, "textEditProfile");
+ %box.isColor = false;
+ %box.setter = %this;
+ %box.AltCommand = %this.getID() @ ".commitBox(" @ %box.getID() @ ");";
+ }
+ %box.borderField = %field;
+ %box.stateIndex = %i;
+ %this.add(%box);
+ %this.box[%field, %i] = %box;
+
+ %cap = new GuiControl()
+ {
+ Position = %bx SPC (%by + 24);
+ Extent = %boxW SPC 14;
+ Text = (%i == 0) ? "" : %this.stateSuffix(%i);
+ align = "center";
+ };
+ ThemeManager.setProfile(%cap, "labelProfile");
+ %this.add(%cap);
+ }
+ }
+
+ // Underfill: whether the border's fill is drawn beneath the control's
+ // content. A single checkbox sits under the grid. The inner box is sized
+ // square explicitly and the row is tall enough that onRender's fit-to-content
+ // clamp never shrinks it (the default box, at offset y=7 + 16 high, gets
+ // clamped vertically in a short row and renders as a rectangle).
+ %uy = %count * %blockH + 2;
+ %cbW = %w - %x0 - 4;
+ %this.underfillBox = new GuiCheckBoxCtrl()
+ {
+ Position = %x0 SPC %uy;
+ Extent = %cbW SPC 26;
+ Text = "Underfill";
+ boxOffset = "0 4";
+ boxExtent = "18 18";
+ textOffset = "26 4";
+ textExtent = (%cbW - 26) SPC 18;
+ Command = %this.getID() @ ".commitUnderfill();";
+ };
+ ThemeManager.setProfile(%this.underfillBox, "checkboxProfile");
+ %this.add(%this.underfillBox);
+
+ %this.gridHeight = %uy + 32;
+ %this.setExtent(%w, %this.gridHeight);
+}
+
+//-----------------------------------------------------------------------------
+// Binding.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorBorderGrid::bind(%this, %border)
+{
+ %this.target = %border;
+ %this.refresh();
+}
+
+function GuiProfileEditorBorderGrid::unbind(%this)
+{
+ %this.target = "";
+}
+
+// Load the bound border's values into the boxes. The populating guard keeps the
+// setText/setColorI/setStateOn calls from echoing back through the commits.
+function GuiProfileEditorBorderGrid::refresh(%this)
+{
+ if(!isObject(%this.target))
+ {
+ return;
+ }
+ %this.populating = true;
+ %numFields = "margin" TAB "border" TAB "padding";
+ for(%f = 0; %f < 3; %f++)
+ {
+ %field = getField(%numFields, %f);
+ for(%i = 0; %i < 4; %i++)
+ {
+ %this.box[%field, %i].setText(%this.target.getFieldValue(%this.stateField(%field, %i)));
+ }
+ }
+ for(%i = 0; %i < 4; %i++)
+ {
+ %this.box["borderColor", %i].setColorI(%this.target.getFieldValue(%this.stateField("borderColor", %i)));
+ }
+ %this.underfillBox.setStateOn(%this.target.underfill);
+ %this.populating = false;
+}
+
+// Copy all sixteen values plus underfill from one border to another.
+function GuiProfileEditorBorderGrid::copyValues(%this, %from, %to)
+{
+ %fields = "margin" TAB "border" TAB "borderColor" TAB "padding";
+ for(%f = 0; %f < 4; %f++)
+ {
+ %field = getField(%fields, %f);
+ for(%i = 0; %i < 4; %i++)
+ {
+ %name = %this.stateField(%field, %i);
+ %to.setFieldValue(%name, %from.getFieldValue(%name));
+ }
+ }
+ %to.underfill = %from.underfill;
+}
+
+//-----------------------------------------------------------------------------
+// Commit.
+//-----------------------------------------------------------------------------
+
+// A single input box committed a value into the bound border.
+function GuiProfileEditorBorderGrid::commitBox(%this, %box)
+{
+ if(%this.populating || !isObject(%this.target))
+ {
+ return;
+ }
+ %name = %this.stateField(%box.borderField, %box.stateIndex);
+ %value = %box.isColor ? %box.getColorI() : %box.getText();
+ %this.target.setFieldValue(%name, %value);
+ %this.notifyCommit();
+}
+
+function GuiProfileEditorBorderGrid::commitUnderfill(%this)
+{
+ if(%this.populating || !isObject(%this.target))
+ {
+ return;
+ }
+ %this.target.underfill = %this.underfillBox.getStateOn();
+ %this.notifyCommit();
+}
+
+// Every commit routes through here so the host can react (mark dirty, refresh
+// the preview) without the grid knowing whether it edits a hidden copy or a
+// named border in place.
+function GuiProfileEditorBorderGrid::notifyCommit(%this)
+{
+ if(isObject(%this.owner))
+ {
+ %this.owner.onBorderGridCommit();
+ }
+}
+
+//-----------------------------------------------------------------------------
+// The numeric input boxes: up/down arrows nudge by 1. Clicking places the caret,
+// as it does in every other box in the editor - see the note in
+// EditorFieldRow for why nothing re-selects here.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorBorderInput::onUpArrow(%this)
+{
+ %this.nudge(1);
+}
+
+function GuiProfileEditorBorderInput::onDownArrow(%this)
+{
+ %this.nudge(-1);
+}
+
+function GuiProfileEditorBorderInput::nudge(%this, %delta)
+{
+ %this.setText(%this.getText() + %delta);
+ %this.selectAllText();
+ if(isObject(%this.setter))
+ {
+ %this.setter.commitBox(%this);
+ }
+}
diff --git a/editor/GuiEditor/scripts/GuiProfileEditorBorderSetter.cs b/editor/GuiEditor/scripts/GuiProfileEditorBorderSetter.cs
new file mode 100644
index 000000000..7c4ca0839
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiProfileEditorBorderSetter.cs
@@ -0,0 +1,494 @@
+
+//-----------------------------------------------------------------------------
+// One row of the Profile Editor's Borders pane: edits a single border slot of
+// a GuiControlProfile (its default border, or one of the top/bottom/left/right
+// sides). A dropdown picks one of the theme's six named borders or "Custom...";
+// choosing Custom expands a GuiProfileEditorBorderGrid -- the shared editor of
+// the sixteen border values (and underfill) -- backed by a hidden single-use
+// border profile.
+//
+// The base control is a vertical GuiChainCtrl (a GuiControl subclass) wearing a
+// panel profile, so it auto-sizes as the editor expands/collapses and ripples
+// that height change up through the pane's outer chain. Its two chain items are
+// the header (checkbox + label + dropdown) and the collapsible editor.
+//
+// The creator sets these fields inline on the new object: slot
+// ("default"/"top"/"bottom"/"left"/"right"), slotTitle, hasCheckbox,
+// setterWidth, dialog. The dialog drives it through bind()/unbind() and reads
+// currentCategory() to grey the default's border out of the side lists.
+//-----------------------------------------------------------------------------
+
+$BorderSetter::CustomRow = "Custom...";
+$BorderSetter::HeaderHeight = 30;
+// The editor panelProfile insets its content by 10px of padding on the left and
+// right; children must fit inside that or they clip. (All editor themes match.)
+$BorderSetter::PanelHPad = 20;
+
+function GuiProfileEditorBorderSetter::onAdd(%this)
+{
+ %full = %this.setterWidth;
+ %w = %full - $BorderSetter::PanelHPad;
+
+ %this.IsVertical = true;
+ %this.ChildSpacing = 0;
+ %this.setExtent(%full, $BorderSetter::HeaderHeight);
+ ThemeManager.setProfile(%this, "panelProfile");
+
+ //--- Header: checkbox (optional) + label + dropdown.
+ %this.header = new GuiControl()
+ {
+ Position = "0 0";
+ Extent = %w SPC $BorderSetter::HeaderHeight;
+ };
+ ThemeManager.setProfile(%this.header, "emptyProfile");
+ %this.add(%this.header);
+
+ %labelX = 6;
+ if(%this.hasCheckbox)
+ {
+ %this.checkbox = new GuiCheckBoxCtrl()
+ {
+ Position = "6 5";
+ Extent = "20 20";
+ Text = "";
+ boxOffset = "0 0";
+ boxExtent = "18 18";
+ textExtent = "0 0";
+ Command = %this.getID() @ ".onCheck();";
+ };
+ ThemeManager.setProfile(%this.checkbox, "checkboxProfile");
+ %this.header.add(%this.checkbox);
+ %labelX = 28;
+ }
+
+ %this.label = new GuiControl()
+ {
+ Position = %labelX SPC 6;
+ Extent = "64 18";
+ Text = %this.slotTitle;
+ align = "left";
+ vAlign = "middle";
+ };
+ ThemeManager.setProfile(%this.label, "labelProfile");
+ %this.header.add(%this.label);
+
+ %dropX = %labelX + 68;
+ %this.dropdown = new GuiDropDownCtrl()
+ {
+ class = "GuiProfileEditorBorderDropDown";
+ Position = %dropX SPC 3;
+ Extent = (%w - %dropX - 6) SPC 24;
+ ConstantThumbHeight = false;
+ ScrollBarThickness = 12;
+ ShowArrowButtons = true;
+ setter = %this;
+ };
+ ThemeManager.setProfile(%this.dropdown, "dropDownProfile");
+ ThemeManager.setProfile(%this.dropdown, "dropDownItemProfile", "listBoxProfile");
+ ThemeManager.setProfile(%this.dropdown, "emptyProfile", "backgroundProfile");
+ ThemeManager.setProfile(%this.dropdown, "scrollingPanelProfile", "ScrollProfile");
+ ThemeManager.setProfile(%this.dropdown, "scrollingPanelThumbProfile", "ThumbProfile");
+ ThemeManager.setProfile(%this.dropdown, "scrollingPanelTrackProfile", "TrackProfile");
+ ThemeManager.setProfile(%this.dropdown, "scrollingPanelArrowProfile", "ArrowProfile");
+ %this.header.add(%this.dropdown);
+
+ //--- Editor: the shared sixteen-value grid, collapsed until "Custom..." is
+ // chosen. The grid owns the fields and commits; this row just hosts it.
+ %this.editor = new GuiControl()
+ {
+ Position = "0 0";
+ Extent = %w SPC 0;
+ Visible = false;
+ };
+ ThemeManager.setProfile(%this.editor, "emptyProfile");
+ %this.add(%this.editor);
+
+ %this.grid = new GuiControl()
+ {
+ class = "GuiProfileEditorBorderGrid";
+ Position = "0 0";
+ gridWidth = %w;
+ owner = %this;
+ };
+ %this.editor.add(%this.grid);
+ %this.grid.build();
+ %this.editor.setExtent(%w, 0); // start collapsed
+}
+
+//-----------------------------------------------------------------------------
+// Binding.
+//-----------------------------------------------------------------------------
+
+// %container owns any custom borders (the theme, or a standalone bundle).
+// %disableName is the border the Default slot resolves to, greyed out of the
+// side lists (empty for the Default setter itself).
+function GuiProfileEditorBorderSetter::bind(%this, %profile, %container, %disableName)
+{
+ %this.editProfile = %profile;
+ %this.container = %container;
+ %this.disableName = %disableName;
+ %this.customBorder = "";
+ %this.populating = true;
+
+ %this.populateDropdown();
+
+ %border = %this.currentBorder();
+ %checked = %this.hasCheckbox && %this.slotHasOwnBorder();
+
+ if(%this.hasCheckbox)
+ {
+ %this.checkbox.setStateOn(%checked);
+ %this.dropdown.setActive(%checked);
+ }
+
+ if(!%this.hasCheckbox || %checked)
+ {
+ %this.selectForBorder(%border);
+ }
+ else
+ {
+ %this.dropdown.setSelected(-1);
+ %this.setEditorExpanded(false);
+ }
+
+ %this.populating = false;
+}
+
+function GuiProfileEditorBorderSetter::unbind(%this)
+{
+ %this.editProfile = "";
+ %this.container = "";
+ %this.customBorder = "";
+ %this.setEditorExpanded(false);
+}
+
+// The border object (its name) this slot currently uses, or "" if none.
+function GuiProfileEditorBorderSetter::currentBorder(%this)
+{
+ if(!isObject(%this.editProfile))
+ {
+ return "";
+ }
+ if(%this.slot $= "default")
+ {
+ return %this.editProfile.borderDefault;
+ }
+ return %this.sideName();
+}
+
+function GuiProfileEditorBorderSetter::sideName(%this)
+{
+ switch$(%this.slot)
+ {
+ case "top": return %this.editProfile.borderTop;
+ case "bottom": return %this.editProfile.borderBottom;
+ case "left": return %this.editProfile.borderLeft;
+ case "right": return %this.editProfile.borderRight;
+ }
+ return "";
+}
+
+function GuiProfileEditorBorderSetter::slotHasOwnBorder(%this)
+{
+ return %this.slot !$= "default" && %this.sideName() !$= "";
+}
+
+// The category the current border belongs to ("" if none, blank, or custom).
+function GuiProfileEditorBorderSetter::currentCategory(%this)
+{
+ %border = %this.currentBorder();
+ if(isObject(%border) && !%border.isCustom)
+ {
+ return %border.category;
+ }
+ return "";
+}
+
+// If this side now duplicates the default border, clear it back to "use
+// default" -- a side should never carry the same border as the default.
+function GuiProfileEditorBorderSetter::clearIfMatches(%this, %category)
+{
+ if(%category $= "" || %this.slot $= "default")
+ {
+ return;
+ }
+ if(%this.currentCategory() $= %category)
+ {
+ %this.assignSide("");
+ if(%this.hasCheckbox)
+ {
+ %this.checkbox.setStateOn(false);
+ %this.dropdown.setActive(false);
+ }
+ %this.dropdown.setSelected(-1);
+ %this.setEditorExpanded(false);
+ %this.commit();
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Dropdown.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorBorderSetter::populateDropdown(%this)
+{
+ %dd = %this.dropdown;
+ %dd.clearItems();
+
+ if(%this.hasCheckbox)
+ {
+ %dd.addItem("", 0); // the initially-selected blank row
+ }
+
+ %names = %this.dialog.borderNamesFor(%this.container);
+ for(%i = 0; %i < getWordCount(%names); %i++)
+ {
+ %dd.addItem(getWord(%names, %i));
+ }
+
+ %dd.addItem($BorderSetter::CustomRow);
+
+ // Grey out the border the Default slot already uses (redundant for a side).
+ if(%this.disableName !$= "")
+ {
+ %idx = %dd.findItemText(%this.disableName, false);
+ if(%idx >= 0)
+ {
+ %dd.setItemInactive(%idx);
+ }
+ }
+}
+
+// Re-grey the item the Default slot resolves to, without re-binding (so the side
+// lists stay correct when the default border is changed live).
+function GuiProfileEditorBorderSetter::applyDisable(%this, %name)
+{
+ %this.disableName = %name;
+ %dd = %this.dropdown;
+ for(%i = 0; %i < %dd.getItemCount(); %i++)
+ {
+ %dd.setItemActive(%i);
+ }
+ if(%name !$= "")
+ {
+ %idx = %dd.findItemText(%name, false);
+ if(%idx >= 0)
+ {
+ %dd.setItemInactive(%idx);
+ }
+ }
+}
+
+function GuiProfileEditorBorderSetter::selectForBorder(%this, %border)
+{
+ if(isObject(%border) && %border.isCustom)
+ {
+ %this.dropdown.setSelected(%this.dropdown.findItemText($BorderSetter::CustomRow, false));
+ %this.showCustom(%border);
+ return;
+ }
+
+ if(isObject(%border) && %border.category !$= "")
+ {
+ %idx = %this.dropdown.findItemText(%border.category, false);
+ if(%idx >= 0)
+ {
+ %this.dropdown.setSelected(%idx);
+ %this.setEditorExpanded(false);
+ return;
+ }
+ }
+
+ %this.dropdown.setSelected(%this.hasCheckbox ? 0 : -1);
+ %this.setEditorExpanded(false);
+}
+
+// Forwarded from GuiProfileEditorBorderDropDown::onSelect.
+function GuiProfileEditorBorderSetter::onSelect(%this, %text)
+{
+ if(%this.populating)
+ {
+ return;
+ }
+
+ if(%text $= $BorderSetter::CustomRow)
+ {
+ %this.chooseCustom();
+ }
+ else if(%text $= "")
+ {
+ %this.assignSide("");
+ %this.setEditorExpanded(false);
+ }
+ else
+ {
+ %this.assignNamed(%text);
+ %this.setEditorExpanded(false);
+ }
+
+ %this.commit();
+
+ // Changing the default border re-greys it out of the side lists.
+ if(%this.slot $= "default" && isObject(%this.dialog))
+ {
+ %this.dialog.refreshSideDisables();
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Checkbox (sides only).
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorBorderSetter::onCheck(%this)
+{
+ if(%this.populating)
+ {
+ return;
+ }
+ %on = %this.checkbox.getStateOn();
+ %this.dropdown.setActive(%on);
+
+ if(%on)
+ {
+ %this.dropdown.setSelected(0); // blank; nothing changes until a pick
+ }
+ else
+ {
+ %this.assignSide("");
+ %this.dropdown.setSelected(-1);
+ %this.setEditorExpanded(false);
+ %this.commit();
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Assigning a border to this slot.
+//-----------------------------------------------------------------------------
+
+// Assign one of the theme's six named borders (by category name).
+function GuiProfileEditorBorderSetter::assignNamed(%this, %category)
+{
+ if(%this.slot $= "default")
+ {
+ %this.editProfile.borderDefault = %this.dialog.borderObjectFor(%this.container, %category);
+ }
+ else
+ {
+ %border = %this.dialog.borderObjectFor(%this.container, %category);
+ %this.assignSide(isObject(%border) ? %border.getName() : "");
+ }
+}
+
+function GuiProfileEditorBorderSetter::assignSide(%this, %name)
+{
+ switch$(%this.slot)
+ {
+ case "top": %this.editProfile.borderTop = %name;
+ case "bottom": %this.editProfile.borderBottom = %name;
+ case "left": %this.editProfile.borderLeft = %name;
+ case "right": %this.editProfile.borderRight = %name;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Custom borders: a single-use border named CustomBorder, seeded
+// from whatever the slot currently shows.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorBorderSetter::customBorderName(%this)
+{
+ return %this.editProfile.getName() @ %this.slot @ "CustomBorder";
+}
+
+function GuiProfileEditorBorderSetter::chooseCustom(%this)
+{
+ %source = %this.currentBorder();
+ if(!isObject(%source))
+ {
+ %source = %this.editProfile.borderDefault; // blank side copies the default
+ }
+
+ %name = %this.customBorderName();
+ if(isObject(%name))
+ {
+ %border = %name;
+ }
+ else
+ {
+ %border = %this.dialog.createCustomBorder(%this.container, %name);
+ }
+
+ if(!isObject(%border))
+ {
+ return;
+ }
+
+ // Seed the custom border from whatever the slot currently shows.
+ if(isObject(%source) && %source != %border)
+ {
+ %this.grid.copyValues(%source, %border);
+ }
+
+ if(%this.slot $= "default")
+ {
+ %this.editProfile.borderDefault = %border;
+ }
+ else
+ {
+ %this.assignSide(%border.getName());
+ }
+
+ %this.showCustom(%border);
+}
+
+function GuiProfileEditorBorderSetter::showCustom(%this, %border)
+{
+ %this.customBorder = %border;
+ %this.grid.bind(%border);
+ %this.setEditorExpanded(true);
+}
+
+//-----------------------------------------------------------------------------
+// Expand / collapse + commit.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorBorderSetter::setEditorExpanded(%this, %open)
+{
+ %w = %this.setterWidth - $BorderSetter::PanelHPad;
+ if(%open)
+ {
+ %this.editor.setVisible(true);
+ %this.editor.setExtent(%w, %this.grid.gridHeight);
+ }
+ else
+ {
+ %this.editor.setVisible(false);
+ %this.editor.setExtent(%w, 0);
+ }
+}
+
+function GuiProfileEditorBorderSetter::commit(%this)
+{
+ if(isObject(%this.dialog))
+ {
+ %this.dialog.onBorderChanged();
+ }
+}
+
+// The shared grid notifies its owner after every edit of the custom border.
+function GuiProfileEditorBorderSetter::onBorderGridCommit(%this)
+{
+ %this.commit();
+}
+
+//-----------------------------------------------------------------------------
+// The dropdown forwards its selection to the owning setter.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorBorderDropDown::onSelect(%this, %index, %text, %id)
+{
+ if(isObject(%this.setter))
+ {
+ %this.setter.onSelect(%text);
+ }
+}
diff --git a/editor/GuiEditor/scripts/GuiProfileEditorColorPopup.cs b/editor/GuiEditor/scripts/GuiProfileEditorColorPopup.cs
new file mode 100644
index 000000000..db821160b
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiProfileEditorColorPopup.cs
@@ -0,0 +1,62 @@
+
+//-----------------------------------------------------------------------------
+// The color popup the Profile Editor puts on every color field: an ordinary
+// GuiColorPopupCtrl whose swatch row holds the six colors of whichever theme is
+// selected in the tree. Setting a fill color to the theme's accent is then a
+// click, rather than a hunt around the color wheel for something close.
+//
+// The swatches are filled when the popup opens, not when it is built, because
+// the theme in play changes as the user moves around the tree while the same
+// widgets stay on screen. A stand-alone profile belongs to no theme, so it gets
+// no swatches and the row does not appear at all.
+//-----------------------------------------------------------------------------
+
+// The six theme colors, in the order the theme form lists them.
+function GuiProfileEditorColorPopup::themeColorFields(%this)
+{
+ return "colorBackground colorSurface colorForeground colorAccent colorHighlight colorWarning";
+}
+
+function GuiProfileEditorColorPopup::onOpen(%this)
+{
+ %this.fillThemeSwatches();
+}
+
+// Replace the swatch row with the selected theme's colors, or empty it when
+// there is no theme to take them from.
+function GuiProfileEditorColorPopup::fillThemeSwatches(%this)
+{
+ %this.clearSwatches();
+
+ %theme = %this.currentTheme();
+ if(!isObject(%theme))
+ {
+ return;
+ }
+
+ %fields = %this.themeColorFields();
+ %count = getWordCount(%fields);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %this.addSwatchI(%theme.getFieldValue(getWord(%fields, %i)));
+ }
+}
+
+// The theme whose colors belong in the swatch row. The dialog's currentRoot is a
+// GuiProfileTheme for every node kind except a stand-alone profile, where it is
+// the profile itself -- hence the class check rather than a bare isObject.
+function GuiProfileEditorColorPopup::currentTheme(%this)
+{
+ if(!isObject(GuiEditor) || !isObject(GuiEditor.profileEditorDialog))
+ {
+ return "";
+ }
+
+ %root = GuiEditor.profileEditorDialog.currentRoot;
+ if(!isObject(%root) || %root.getClassName() !$= "GuiProfileTheme")
+ {
+ return "";
+ }
+
+ return %root;
+}
diff --git a/editor/GuiEditor/scripts/GuiProfileEditorConfirmDialog.cs b/editor/GuiEditor/scripts/GuiProfileEditorConfirmDialog.cs
new file mode 100644
index 000000000..84c7f7b5f
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiProfileEditorConfirmDialog.cs
@@ -0,0 +1,69 @@
+
+//-----------------------------------------------------------------------------
+// A small confirmation dialog used by the Gui Profile Editor. The spawner
+// sets dialogText, message, confirmText, callbackTarget, and callbackMethod;
+// the confirm button calls callbackTarget.callbackMethod().
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorConfirmDialog::init(%this, %width, %height)
+{
+ %window = %this.getObject(0);
+ %content = %window.getObject(0);
+
+ // Everything above the buttons, rather than a fixed height: the messages
+ // differ in length by a factor of three, and the spawner sizes the dialog to
+ // the one it is about to show (see openConfirmDialog). A fixed box silently
+ // clipped the last line of the longer ones.
+ %this.feedback = new GuiControl()
+ {
+ HorizSizing = "right";
+ VertSizing = "bottom";
+ Position = "12 12";
+ Extent = (%width - 24) SPC (%height - 86);
+ text = %this.message;
+ textWrap = true;
+ };
+ ThemeManager.setProfile(%this.feedback, "infoProfile");
+ %content.add(%this.feedback);
+
+ %this.cancelButton = new GuiButtonCtrl()
+ {
+ HorizSizing = "right";
+ VertSizing = "bottom";
+ Position = (%width - 226) SPC (%height - 62);
+ Extent = "100 30";
+ Text = "Cancel";
+ Command = %this.getID() @ ".onClose();";
+ };
+ ThemeManager.setProfile(%this.cancelButton, "buttonProfile");
+ %content.add(%this.cancelButton);
+
+ %this.confirmButton = new GuiButtonCtrl()
+ {
+ HorizSizing = "right";
+ VertSizing = "bottom";
+ Position = (%width - 116) SPC (%height - 64);
+ Extent = "100 34";
+ Text = %this.confirmText;
+ Command = %this.getID() @ ".onConfirm();";
+ };
+ ThemeManager.setProfile(%this.confirmButton, "primaryButtonProfile");
+ %content.add(%this.confirmButton);
+}
+
+function GuiProfileEditorConfirmDialog::onConfirm(%this)
+{
+ %this.callbackTarget.call(%this.callbackMethod);
+ %this.onClose();
+}
+
+// This dialog sits on top of the profile editor dialog, so it must not use
+// the shared EditorCore.dialog delete slot - closing both within the
+// scheduled delay would leak one of them. The parent object deletes it after
+// a pause; scheduling "delete" on the dialog itself would fire inside its
+// own script-callback guard and assert.
+function GuiProfileEditorConfirmDialog::onClose(%this)
+{
+ Canvas.popDialog(%this);
+ EditorCore.schedule(100, "deleteDialogObject", %this);
+}
diff --git a/editor/GuiEditor/scripts/GuiProfileEditorCursorForm.cs b/editor/GuiEditor/scripts/GuiProfileEditorCursorForm.cs
new file mode 100644
index 000000000..11476a921
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiProfileEditorCursorForm.cs
@@ -0,0 +1,529 @@
+
+//-----------------------------------------------------------------------------
+// The cursor pane: the fourth form sharing the Gui Profile Editor's Properties
+// window, shown when a cursor node is selected.
+//
+// Most of it is the ordinary field-row machinery the profile pane uses. What is
+// not is the hot spot, which cannot be set by typing numbers: it is one pixel
+// in a 13x17 image, and whether it is the right pixel is a question about what
+// the art looks like. So the pane is built around a GuiEditorCursorCtrl showing
+// the art magnified with the hot spot marked and draggable, and the fields
+// underneath report what the dragging did.
+//
+// Two fields decide where the pointer lands and both are shown, because they do
+// different jobs:
+//
+// Anchor (renderOffset) a fraction of the art's own size, so "0.5 0.5" is
+// the middle whatever the art measures. This is what
+// stops a small pointer and a large sizer from
+// appearing to leap when one replaces the other, and
+// it is why the anchor is set from a 3x3 of presets
+// rather than typed.
+// Nudge (hotSpot) pixels on top of that. This is what dragging writes.
+//
+// The creator sets the dialog back-pointer and an initial Extent inline, then
+// calls build() once after adding it to its scroller; bind()/unbind() attach a
+// cursor.
+//-----------------------------------------------------------------------------
+
+// The Marker and Tint buttons are the same size as each other on purpose: they
+// do the same kind of job, one on the cursor and one on the editor's own
+// marker, and matching them is what says so.
+$CursorForm::SwatchWidth = 66;
+$CursorForm::SwatchExtent = "66 22";
+
+function GuiProfileEditorCursorForm::onAdd(%this)
+{
+ ThemeManager.setProfile(%this, "emptyProfile");
+ %this.rowWidth = 200;
+
+ // Every color row this form builds gets the popup that offers the selected
+ // theme's six colors. EditorFieldRow lives in EditorCore and cannot name a
+ // Gui Editor class, so the pane that wants one says so -- see its header.
+ %this.swatchClass = "GuiProfileEditorColorPopup";
+}
+
+function GuiProfileEditorCursorForm::build(%this)
+{
+ %w = %this.formWidth;
+
+ %this.nameLabel = new GuiControl()
+ {
+ HorizSizing = "width";
+ Position = "0 0";
+ Extent = %w SPC 24;
+ Text = "Cursor:";
+ align = "left";
+ vAlign = "middle";
+ };
+ ThemeManager.setProfile(%this.nameLabel, "labelProfile");
+ %this.add(%this.nameLabel);
+
+ %this.buildEditor();
+ %this.buildAnchor();
+
+ %grid = %this.makeCellGrid();
+ %this.add(%grid);
+ %this.addFieldRow(%grid, "bitmapName", "Art", "file");
+ %this.addFieldRow(%grid, "color", "Tint", "color", $CursorForm::SwatchWidth);
+ %this.addFieldRow(%grid, "hotSpot", "Nudge (pixels)", "point");
+ %this.addFieldRow(%grid, "renderOffset", "Anchor (fraction)", "pointf");
+
+ // Same forced layout pass the profile form needs: a GuiChainCtrl positions
+ // its children without resizing them, so nothing would otherwise tell the
+ // grid how wide it is.
+ %h = getWord(%this.getExtent(), 1);
+ %this.resize(0, 0, %w + 1, %h);
+ %this.resize(0, 0, %w, %h);
+}
+
+// The magnifier, and the zoom controls that belong to it.
+function GuiProfileEditorCursorForm::buildEditor(%this)
+{
+ %w = %this.formWidth;
+
+ // Tall enough that the small cursors reach the full 16x: the stock pointer is
+ // 17 pixels high, so it needs 272 for the art plus what the profile's border
+ // and padding take off the content rect. The big ones (a 32x32 move cursor
+ // would want 512) still cap lower, which is what the greyed "+" is for.
+ %viewHeight = 304;
+
+ %this.editorBox = new GuiControl()
+ {
+ HorizSizing = "width";
+ Position = "0 0";
+ Extent = %w SPC (%viewHeight + 60);
+ };
+ ThemeManager.setProfile(%this.editorBox, "emptyProfile");
+ %this.add(%this.editorBox);
+
+ %this.editor = new GuiEditorCursorCtrl()
+ {
+ class = "GuiProfileEditorCursorView";
+ HorizSizing = "width";
+ VertSizing = "height";
+ Position = "0 0";
+ Extent = %w SPC %viewHeight;
+ zoom = 8;
+ owner = %this;
+ };
+ ThemeManager.setProfile(%this.editor, "displayBoxProfile");
+ %this.editorBox.add(%this.editor);
+
+ // The readout names the pixel the pointer really lands on, which is neither
+ // field on its own -- so without it the two numbers below would look like
+ // they disagreed with the dot.
+ %rowY = %viewHeight + 6;
+
+ %this.readout = new GuiControl()
+ {
+ HorizSizing = "width";
+ Position = "0" SPC %rowY;
+ Extent = (%w - 96) SPC 22;
+ Text = "";
+ align = "left";
+ vAlign = "middle";
+ };
+ ThemeManager.setProfile(%this.readout, "labelProfile");
+ %this.editorBox.add(%this.readout);
+
+ %this.zoomOut = %this.makeZoomButton(%w - 92, %rowY, "-", "Zoom out");
+ %this.zoomLabel = new GuiControl()
+ {
+ HorizSizing = "left";
+ Position = (%w - 62) SPC %rowY;
+ Extent = "34 22";
+ Text = "8x";
+ align = "center";
+ vAlign = "middle";
+ };
+ ThemeManager.setProfile(%this.zoomLabel, "labelProfile");
+ %this.editorBox.add(%this.zoomLabel);
+ %this.zoomIn = %this.makeZoomButton(%w - 26, %rowY, "+", "Zoom in");
+
+ %this.dotRow = new GuiControl()
+ {
+ HorizSizing = "width";
+ Position = "0" SPC (%rowY + 26);
+ Extent = %w SPC 24;
+ };
+ ThemeManager.setProfile(%this.dotRow, "emptyProfile");
+ %this.editorBox.add(%this.dotRow);
+
+ %dotLabel = new GuiControl()
+ {
+ Position = "0 0";
+ Extent = "80 22";
+ Text = "Marker";
+ align = "left";
+ vAlign = "middle";
+ };
+ ThemeManager.setProfile(%dotLabel, "labelProfile");
+ %this.dotRow.add(%dotLabel);
+
+ // The hot-spot marker's own color, because a dark dot vanishes on dark art
+ // and a light one on light art. It belongs to the editor, not the cursor, so
+ // it is never written to the theme.
+ //
+ // A button, not a bar: left-aligned at a fixed width rather than filling the
+ // row, because a full-width band of flat color reads as a progress bar.
+ %this.dotSwatch = new GuiColorPopupCtrl()
+ {
+ class = "GuiProfileEditorColorPopup";
+ Position = "84 0";
+ Extent = $CursorForm::SwatchExtent;
+ showColorValues = false;
+ };
+ ThemeManager.setProfile(%this.dotSwatch, "colorPickerProfile");
+ ThemeManager.setProfile(%this.dotSwatch, "emptyProfile", "backgroundProfile");
+ ThemeManager.setProfile(%this.dotSwatch, "colorPopupProfile", "popupProfile");
+ ThemeManager.setProfile(%this.dotSwatch, "emptyProfile", "pickerProfile");
+ ThemeManager.setProfile(%this.dotSwatch, "colorPickerSelectorProfile", "selectorProfile");
+ ThemeManager.setProfile(%this.dotSwatch, "textEditProfile", "valueProfile");
+ %this.dotSwatch.Command = %this.getID() @ ".onDotColorChanged();";
+ %this.dotRow.add(%this.dotSwatch);
+ %this.dotSwatch.setColorI(%this.editor.dotColor);
+}
+
+function GuiProfileEditorCursorForm::makeZoomButton(%this, %x, %y, %text, %tip)
+{
+ %button = new GuiButtonCtrl()
+ {
+ HorizSizing = "left";
+ Position = %x SPC %y;
+ Extent = "26 22";
+ Text = %text;
+ tooltip = %tip;
+ Command = %this.getID() @ ".onZoom(" @ (%text $= "+" ? 1 : -1) @ ");";
+ };
+ ThemeManager.setProfile(%button, "buttonProfile");
+ ThemeManager.setProfile(%button, "tipProfile", "TooltipProfile");
+ %this.editorBox.add(%button);
+ return %button;
+}
+
+// Nine presets for the anchor, laid out as the thing they mean: where in the
+// art the pointer sits. Typing "0.5 0.5" is the same edit, but nobody reads a
+// pair of decimals as "the middle" at a glance.
+function GuiProfileEditorCursorForm::buildAnchor(%this)
+{
+ %w = %this.formWidth;
+
+ // Tall enough for four wrapped lines beside the pin cluster. The cluster
+ // itself only needs 82, but a hint that clips mid-sentence is worse than
+ // none, and the pane scrolls.
+ %this.anchorBox = new GuiControl()
+ {
+ HorizSizing = "width";
+ Position = "0 0";
+ Extent = %w SPC 104;
+ };
+ ThemeManager.setProfile(%this.anchorBox, "emptyProfile");
+ %this.add(%this.anchorBox);
+
+ %label = new GuiControl()
+ {
+ Position = "0 0";
+ Extent = "200 20";
+ Text = "Anchor";
+ align = "left";
+ vAlign = "middle";
+ };
+ ThemeManager.setProfile(%label, "labelProfile");
+ %this.anchorBox.add(%label);
+
+ %fractions = "0" TAB "0.5" TAB "1";
+ for(%row = 0; %row < 3; %row++)
+ {
+ for(%col = 0; %col < 3; %col++)
+ {
+ %x = getField(%fractions, %col);
+ %y = getField(%fractions, %row);
+
+ %pin = new GuiButtonCtrl()
+ {
+ Position = (%col * 20) SPC (22 + (%row * 20));
+ Extent = "18 18";
+ Text = "";
+ tooltip = "Anchor at" SPC %x SPC %y;
+ Command = %this.getID() @ ".onAnchorPreset(" @ %x @ "," SPC %y @ ");";
+ };
+ ThemeManager.setProfile(%pin, "buttonProfile");
+ ThemeManager.setProfile(%pin, "tipProfile", "TooltipProfile");
+ %this.anchorBox.add(%pin);
+ }
+ }
+
+ %hint = new GuiControl()
+ {
+ HorizSizing = "width";
+ Position = "68 22";
+ Extent = (%w - 68) SPC 78;
+ Text = "A fraction of the art's size, so one anchor suits art of any size - which is what stops a cursor appearing to jump when another replaces it.";
+ align = "left";
+ vAlign = "top";
+ textWrap = true;
+ };
+ ThemeManager.setProfile(%hint, "labelProfile");
+ %this.anchorBox.add(%hint);
+}
+
+function GuiProfileEditorCursorForm::makeCellGrid(%this)
+{
+ %grid = new GuiGridCtrl()
+ {
+ HorizSizing = "width";
+ Position = "0 0";
+ Extent = %this.formWidth SPC 4;
+ CellModeX = "variable";
+ CellModeY = "variable";
+ CellSizeX = %this.rowWidth;
+ CellSizeY = 48;
+ CellSpacingX = 4;
+ CellSpacingY = 4;
+ MaxColCount = 0;
+ MaxRowCount = 0;
+ OrderMode = "lrtb";
+ IsExtentDynamic = true;
+ };
+ ThemeManager.setProfile(%grid, "emptyProfile");
+ return %grid;
+}
+
+function GuiProfileEditorCursorForm::addFieldRow(%this, %container, %field, %label, %kind, %swatchWidth)
+{
+ %row = new GuiControl()
+ {
+ class = "EditorFieldRow";
+ Position = "0 0";
+ fieldName = %field;
+ labelText = %label;
+ kind = %kind;
+ swatchWidth = %swatchWidth;
+ owner = %this;
+ };
+ %container.add(%row);
+ %row.build();
+
+ %this.row[%field] = %row;
+ %this.rowFields = (%this.rowFields $= "") ? %field : (%this.rowFields SPC %field);
+ return %row;
+}
+
+//-----------------------------------------------------------------------------
+// Binding.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorCursorForm::bind(%this, %cursor, %label)
+{
+ if(!isObject(%cursor))
+ {
+ %this.unbind();
+ return;
+ }
+
+ %this.target = %cursor;
+ %this.nameLabel.setText("Cursor: " @ %label);
+ %this.editor.cursor = %cursor;
+ %this.refresh();
+}
+
+function GuiProfileEditorCursorForm::unbind(%this)
+{
+ %this.target = "";
+ if(isObject(%this.editor))
+ {
+ %this.editor.cursor = "";
+ }
+}
+
+function GuiProfileEditorCursorForm::refresh(%this)
+{
+ if(!isObject(%this.target))
+ {
+ return;
+ }
+
+ // populating stops a row that is only being filled in from reporting itself
+ // as an edit, which would record a theme override nobody asked for.
+ %this.populating = true;
+
+ %count = getWordCount(%this.rowFields);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %field = getWord(%this.rowFields, %i);
+ %this.row[%field].setValue(%this.target.getFieldValue(%field));
+ }
+
+ %this.populating = false;
+
+ %this.refreshOverrides();
+ %this.refreshReadout();
+}
+
+// Only the tint is derived from the theme, so it is the only row that can be
+// overridden and the only one with anything to reset. The art fields are the
+// user's outright -- there is no theme value behind them to go back to.
+function GuiProfileEditorCursorForm::refreshOverrides(%this)
+{
+ %theme = %this.currentTheme();
+ %hasTheme = isObject(%theme) && isObject(%this.target);
+
+ %count = getWordCount(%this.rowFields);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %field = getWord(%this.rowFields, %i);
+ %this.row[%field].setOverridden(%hasTheme && %theme.isFieldOverridden(%this.target, %field));
+ }
+}
+
+function GuiProfileEditorCursorForm::refreshReadout(%this)
+{
+ if(!isObject(%this.target) || !isObject(%this.editor))
+ {
+ return;
+ }
+
+ %extent = %this.editor.getImageExtent();
+ if(getWord(%extent, 0) <= 0)
+ {
+ %this.readout.setText("No art - choose a file.");
+ %this.zoomIn.setActive(false);
+ %this.zoomOut.setActive(false);
+ return;
+ }
+
+ %hot = %this.editor.getEffectiveHotSpot();
+ %this.readout.setText("Points at pixel" SPC getWord(%hot, 0) @ "," SPC getWord(%hot, 1) SPC
+ "of" SPC getWord(%extent, 0) @ "x" @ getWord(%extent, 1));
+
+ // The zoom the control is really drawing at, which is not always the one
+ // that was asked for: art too big for the pane is clamped to what fits. The
+ // buttons grey out at both ends rather than letting a click do nothing and
+ // leaving the number to explain why.
+ %zoom = %this.editor.getZoom();
+ %this.zoomLabel.setText(%zoom @ "x");
+ %this.zoomIn.setActive(%zoom < %this.editor.getMaxZoom());
+ %this.zoomOut.setActive(%zoom > 1);
+}
+
+function GuiProfileEditorCursorForm::currentTheme(%this)
+{
+ %root = %this.dialog.currentRoot;
+ if(isObject(%root) && %root.getClassName() $= "GuiProfileTheme")
+ {
+ return %root;
+ }
+ return "";
+}
+
+//-----------------------------------------------------------------------------
+// Edits.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorCursorForm::onFieldRowCommit(%this, %row)
+{
+ if(%this.populating || !isObject(%this.target))
+ {
+ return;
+ }
+
+ // A text box commits on blur, so most commits arrive from a field the user
+ // only tabbed through; writing one would record an override for an edit that
+ // never happened.
+ if(!%row.hasChanged())
+ {
+ return;
+ }
+
+ %this.target.setFieldValue(%row.fieldName, %row.getValue());
+ %row.markClean();
+ %this.afterCommit();
+}
+
+function GuiProfileEditorCursorForm::onFieldRowReset(%this, %row)
+{
+ %theme = %this.currentTheme();
+ if(!isObject(%theme) || !isObject(%this.target))
+ {
+ return;
+ }
+
+ %theme.resetField(%this.target, %row.fieldName);
+ %this.refresh();
+ %this.afterCommit();
+}
+
+// The magnifier reports a drag here. The value is already on the cursor -- the
+// control writes it as the drag happens, which is what makes the dot follow the
+// mouse -- so this only has to catch the rows up and mark the theme dirty.
+function GuiProfileEditorCursorForm::onHotSpotChanged(%this, %x, %y)
+{
+ if(!isObject(%this.target))
+ {
+ return;
+ }
+
+ %this.populating = true;
+ %this.row["hotSpot"].setValue(%x SPC %y);
+ %this.populating = false;
+
+ %this.refreshReadout();
+ %this.afterCommit();
+}
+
+function GuiProfileEditorCursorForm::onAnchorPreset(%this, %x, %y)
+{
+ if(!isObject(%this.target))
+ {
+ return;
+ }
+
+ %this.target.renderOffset = %x SPC %y;
+ %this.refresh();
+ %this.afterCommit();
+}
+
+function GuiProfileEditorCursorForm::onZoom(%this, %step)
+{
+ %this.editor.setZoom(%this.editor.getZoom() + %step);
+ %this.refreshReadout();
+}
+
+function GuiProfileEditorCursorForm::onDotColorChanged(%this)
+{
+ %this.editor.dotColor = %this.dotSwatch.getColorI();
+}
+
+function GuiProfileEditorCursorForm::afterCommit(%this)
+{
+ %this.refreshOverrides();
+
+ // The readout is a function of BOTH placement fields, so typing into either
+ // one moves it. Without this it goes on reporting the pixel the dot used to
+ // be on while the dot itself has already moved -- which reads as the
+ // magnifier and the numbers disagreeing.
+ %this.refreshReadout();
+
+ %this.dialog.onProfileChanged(%this.target);
+}
+
+//-----------------------------------------------------------------------------
+// The magnifier forwards its callbacks to the pane that owns it.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorCursorView::onHotSpotChanged(%this, %x, %y)
+{
+ if(isObject(%this.owner))
+ {
+ %this.owner.onHotSpotChanged(%x, %y);
+ }
+}
+
+function GuiProfileEditorCursorView::onZoomChanged(%this, %zoom)
+{
+ if(isObject(%this.owner))
+ {
+ %this.owner.refreshReadout();
+ }
+}
diff --git a/editor/GuiEditor/scripts/GuiProfileEditorDialog.cs b/editor/GuiEditor/scripts/GuiProfileEditorDialog.cs
new file mode 100644
index 000000000..192743494
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiProfileEditorDialog.cs
@@ -0,0 +1,1244 @@
+
+//-----------------------------------------------------------------------------
+// The Gui Profile Editor: a near-full-screen dialog showing a tree of every
+// theme and standalone profile in the project's themes folder, a field
+// editing pane for the selected member, and a live preview control. Edits apply
+// immediately to the live objects; Save writes the dirty themes to their
+// files, Cancel (or the window X) reverts dirty themes from their files.
+// The themes themselves are owned by GuiEditor's persistent library and
+// survive the dialog, so guis can reference their member profiles.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorDialog::init(%this, %width, %height)
+{
+ %window = %this.getObject(0);
+ %content = %window.getObject(0);
+
+ // The Gui Editor runs with editorMode on, which shadow-names new objects
+ // and would break theme member naming, border references, and TAML.
+ // Turn it off while the dialog lives; onRemove restores it.
+ editorMode(false);
+
+ // The spawner hands us GuiEditor's persistent theme library; the dialog
+ // only borrows it for this session.
+ %this.library.dialog = %this;
+
+ %this.toolbar = new GuiChainCtrl()
+ {
+ Class = "EditorButtonBar";
+ Position = "6 4";
+ Extent = "0 30";
+ ChildSpacing = 4;
+ IsVertical = false;
+ Tool = %this;
+ };
+ ThemeManager.setProfile(%this.toolbar, "emptyProfile");
+ %content.add(%this.toolbar);
+
+ %this.toolbar.addButton("onNewTheme", $EditorIcon::doc_plus, "New Theme", "");
+ %this.toolbar.addButton("onRename", $EditorIcon::doc_edit, "Rename Theme or Stand Alone Profile", "getRootSelected");
+ %this.toolbar.addButton("onDelete", $EditorIcon::round_delete, "Delete Theme or Stand Alone Profile", "getRootSelected");
+ // These two serve profiles and cursors alike, so their tips are answered per
+ // selection rather than fixed - see newInCategoryTip.
+ %this.toolbar.addButton("onNewProfile", $EditorIcon::round_plus, "New Profile in Category", "getCategorySelected", "newInCategoryTip");
+ %this.toolbar.addButton("onRemoveProfile", $EditorIcon::round_delete, "Remove Extra Profile", "getExtraSelected", "removeExtraTip");
+ %this.toolbar.addButton("onNewStandalone", $EditorIcon::round_plus, "New Stand Alone Profile", "");
+ // The circular revert arrow; a plain plus was reading as another "new"
+ // button next to the three that really are.
+ %this.toolbar.addButton("onResetMember", $EditorIcon::playback_reload, "Reset All Overrides on Member", "getMemberSelected");
+
+ %paneTop = 40;
+ %paneHeight = %height - 116;
+
+ // The three sections live in a resizable frame set (like the main Gui
+ // Editor): the user drags the dividers to resize the tree, the member
+ // editor, and the preview. Adding more sections later is just another split.
+ // A GuiFrameSetCtrl fills its parent, so it's wrapped in a plain container
+ // positioned to the pane area -- otherwise it expands over the buttons.
+ %this.framePane = new GuiControl()
+ {
+ HorizSizing = "width";
+ VertSizing = "height";
+ Position = "6" SPC %paneTop;
+ Extent = (%width - 12) SPC %paneHeight;
+ };
+ ThemeManager.setProfile(%this.framePane, "emptyProfile");
+ %content.add(%this.framePane);
+
+ %this.frames = new GuiFrameSetCtrl()
+ {
+ HorizSizing = "width";
+ VertSizing = "height";
+ Position = "0 0";
+ Extent = (%width - 12) SPC %paneHeight;
+ DividerThickness = 6;
+ };
+ ThemeManager.setProfile(%this.frames, "frameSetProfile");
+ ThemeManager.setProfile(%this.frames, "dropButtonProfile", "dropButtonProfile");
+ ThemeManager.setProfile(%this.frames, "frameSetTabBookProfile", "tabBookProfile");
+ ThemeManager.setProfile(%this.frames, "frameSetTabProfile", "tabProfile");
+ ThemeManager.setProfile(%this.frames, "frameSetTabPageProfile", "tabPageProfile");
+ %this.framePane.add(%this.frames);
+
+ // tree | member editor | preview. The first child of a split is the anchored
+ // (fixed-width) frame by default, so sizing the tree and member frames leaves
+ // the preview to take the remaining space.
+ %ids = %this.frames.createHorizontalSplit(1);
+ %treeFrame = getWord(%ids, 0);
+ %restFrame = getWord(%ids, 1);
+ %this.frames.setFrameSize(%treeFrame, 240);
+
+ // Kept on the dialog because the Properties pane's width is meaningful: the
+ // profile form flows its fields into more columns as this frame widens.
+ %ids = %this.frames.createHorizontalSplit(%restFrame);
+ %this.memberFrame = getWord(%ids, 0);
+ %previewFrame = getWord(%ids, 1);
+ %this.frames.setFrameSize(%this.memberFrame, 400);
+
+ // A fourth frame holds the Borders pane (shown only for profiles). Splitting
+ // the preview frame keeps the order tree | Properties | Borders | preview.
+ %ids = %this.frames.createHorizontalSplit(%previewFrame);
+ %this.bordersFrame = getWord(%ids, 0);
+ %previewFrame = getWord(%ids, 1);
+ %this.frames.setFrameSize(%this.bordersFrame, 360);
+
+ // The frame set docks children into empty frames in add-order (depth-first
+ // through the splits), so add them tree -> member editor -> preview.
+
+ //--- Frame 1: the theme/profile tree.
+ %this.treeScroller = new GuiScrollCtrl()
+ {
+ HorizSizing = "width";
+ VertSizing = "height";
+ Position = "0 0";
+ Extent = "240" SPC %paneHeight;
+ hScrollBar = "alwaysOff";
+ vScrollBar = "alwaysOn";
+ constantThumbHeight = "0";
+ showArrowButtons = "1";
+ scrollBarThickness = "14";
+ };
+ ThemeManager.setProfile(%this.treeScroller, "emptyProfile");
+ ThemeManager.setProfile(%this.treeScroller, "thumbProfile", "ThumbProfile");
+ ThemeManager.setProfile(%this.treeScroller, "trackProfile", "TrackProfile");
+ ThemeManager.setProfile(%this.treeScroller, "scrollArrowProfile", "ArrowProfile");
+ %this.frames.add(%this.treeScroller);
+
+ %this.tree = new GuiTreeViewCtrl()
+ {
+ class = "GuiProfileEditorTree";
+ HorizSizing = "width";
+ VertSizing = "height";
+ Position = "0 0";
+ Extent = "224" SPC %paneHeight;
+ dialog = %this;
+ };
+ ThemeManager.setProfile(%this.tree, "treeViewProfile");
+ %this.treeScroller.add(%this.tree);
+
+ //--- Frame 2: the member editor, wrapped in a window so it can be dragged
+ // out of the frame set. The three custom forms -- profile, theme and border --
+ // share this window; onTreeSelect toggles which one is visible.
+ %this.memberWindow = new GuiWindowCtrl()
+ {
+ HorizSizing = "width";
+ VertSizing = "height";
+ Position = "0 0";
+ Extent = "400" SPC %paneHeight;
+ MinExtent = "200 100";
+ text = "Properties";
+ canMove = true;
+ canClose = false;
+ canMinimize = false;
+ canMaximize = false;
+ resizeWidth = true;
+ resizeHeight = true;
+ };
+ ThemeManager.setProfile(%this.memberWindow, "windowProfile");
+ ThemeManager.setProfile(%this.memberWindow, "windowContentProfile", "ContentProfile");
+ ThemeManager.setProfile(%this.memberWindow, "windowButtonProfile", "CloseButtonProfile");
+ ThemeManager.setProfile(%this.memberWindow, "windowButtonProfile", "MinButtonProfile");
+ ThemeManager.setProfile(%this.memberWindow, "windowButtonProfile", "MaxButtonProfile");
+ %this.frames.add(%this.memberWindow);
+
+ // The custom profile pane: it replaced the generic GuiInspector for profile
+ // nodes, so the member window now holds three custom forms and no inspector.
+ // It shows only the fields the selected profile's category actually uses --
+ // see GuiProfileEditorProfileForm and GuiProfileEditorFieldSpec.
+ // Starts hidden like the other two panes: nothing is selected yet, so there is
+ // no profile to show. onTreeSelect brings whichever pane the selection calls
+ // for. (The inspector this replaced started visible and got away with it by
+ // rendering as an empty box; this pane always draws its header and
+ // essentials, so an unbound one looks like a real profile.)
+ %this.profileFormScroller = new GuiScrollCtrl()
+ {
+ HorizSizing = "fill";
+ VertSizing = "fill";
+ Position = "0 0";
+ Extent = "400" SPC %paneHeight;
+ hScrollBar = "alwaysOff";
+ vScrollBar = "alwaysOn";
+ constantThumbHeight = "0";
+ showArrowButtons = "1";
+ scrollBarThickness = "14";
+ Visible = false;
+ };
+ ThemeManager.setProfile(%this.profileFormScroller, "emptyProfile");
+ ThemeManager.setProfile(%this.profileFormScroller, "thumbProfile", "ThumbProfile");
+ ThemeManager.setProfile(%this.profileFormScroller, "trackProfile", "TrackProfile");
+ ThemeManager.setProfile(%this.profileFormScroller, "scrollArrowProfile", "ArrowProfile");
+ %this.memberWindow.add(%this.profileFormScroller);
+
+ %this.profileForm = new GuiChainCtrl()
+ {
+ class = "GuiProfileEditorProfileForm";
+ HorizSizing = "width";
+ Position = "0 0";
+ Extent = "386" SPC %paneHeight;
+ IsVertical = true;
+ ChildSpacing = 6;
+ formWidth = 386;
+ dialog = %this;
+ };
+ %this.profileFormScroller.add(%this.profileForm);
+ %this.profileForm.build();
+
+ // The custom theme form takes the member pane whenever a theme (rather than a
+ // member) is selected. onTreeSelect swaps which of the three scrollers is
+ // visible.
+ %this.formScroller = new GuiScrollCtrl()
+ {
+ HorizSizing = "fill";
+ VertSizing = "fill";
+ Position = "0 0";
+ Extent = "400" SPC %paneHeight;
+ hScrollBar = "alwaysOff";
+ vScrollBar = "dynamic";
+ constantThumbHeight = "0";
+ showArrowButtons = "1";
+ scrollBarThickness = "14";
+ Visible = false;
+ };
+ ThemeManager.setProfile(%this.formScroller, "emptyProfile");
+ ThemeManager.setProfile(%this.formScroller, "thumbProfile", "ThumbProfile");
+ ThemeManager.setProfile(%this.formScroller, "trackProfile", "TrackProfile");
+ ThemeManager.setProfile(%this.formScroller, "scrollArrowProfile", "ArrowProfile");
+ %this.memberWindow.add(%this.formScroller);
+
+ %this.themeForm = new GuiGridCtrl()
+ {
+ class = "ProfileThemeEditForm";
+ superclass = "EditorForm";
+ HorizSizing = "width";
+ VertSizing = "height";
+ Position = "0 0";
+ Extent = "386" SPC %paneHeight;
+ cellSizeX = 356;
+ cellSizeY = 50;
+ dialog = %this;
+ };
+ %this.themeForm.addListener(%this.themeForm);
+ %this.themeForm.build();
+ %this.formScroller.add(%this.themeForm);
+
+ // The custom border form is the third pane sharing the member window: it
+ // takes over whenever a border node (rather than a profile) is selected.
+ // onTreeSelect swaps which of the three scrollers is visible.
+ %this.borderFormScroller = new GuiScrollCtrl()
+ {
+ HorizSizing = "fill";
+ VertSizing = "fill";
+ Position = "0 0";
+ Extent = "400" SPC %paneHeight;
+ hScrollBar = "alwaysOff";
+ vScrollBar = "dynamic";
+ constantThumbHeight = "0";
+ showArrowButtons = "1";
+ scrollBarThickness = "14";
+ Visible = false;
+ };
+ ThemeManager.setProfile(%this.borderFormScroller, "emptyProfile");
+ ThemeManager.setProfile(%this.borderFormScroller, "thumbProfile", "ThumbProfile");
+ ThemeManager.setProfile(%this.borderFormScroller, "trackProfile", "TrackProfile");
+ ThemeManager.setProfile(%this.borderFormScroller, "scrollArrowProfile", "ArrowProfile");
+ %this.memberWindow.add(%this.borderFormScroller);
+
+ %this.borderForm = new GuiControl()
+ {
+ class = "GuiProfileEditorBorderForm";
+ HorizSizing = "fill";
+ VertSizing = "fill";
+ Position = "0 0";
+ Extent = "386" SPC %paneHeight;
+ dialog = %this;
+ };
+ %this.borderForm.build();
+ %this.borderFormScroller.add(%this.borderForm);
+
+ // The cursor pane, fourth of the forms sharing this window. It is the one
+ // that is not mostly field rows: a hot spot has to be placed by eye against
+ // magnified art, so the pane is built around the magnifier.
+ %this.cursorFormScroller = new GuiScrollCtrl()
+ {
+ HorizSizing = "fill";
+ VertSizing = "fill";
+ Position = "0 0";
+ Extent = "400" SPC %paneHeight;
+ hScrollBar = "alwaysOff";
+ vScrollBar = "dynamic";
+ constantThumbHeight = "0";
+ showArrowButtons = "1";
+ scrollBarThickness = "14";
+ Visible = false;
+ };
+ ThemeManager.setProfile(%this.cursorFormScroller, "emptyProfile");
+ ThemeManager.setProfile(%this.cursorFormScroller, "thumbProfile", "ThumbProfile");
+ ThemeManager.setProfile(%this.cursorFormScroller, "trackProfile", "TrackProfile");
+ ThemeManager.setProfile(%this.cursorFormScroller, "scrollArrowProfile", "ArrowProfile");
+ %this.memberWindow.add(%this.cursorFormScroller);
+
+ %this.cursorForm = new GuiChainCtrl()
+ {
+ class = "GuiProfileEditorCursorForm";
+ HorizSizing = "width";
+ Position = "0 0";
+ Extent = "386" SPC %paneHeight;
+ IsVertical = true;
+ ChildSpacing = 6;
+ formWidth = 386;
+ dialog = %this;
+ };
+ %this.cursorFormScroller.add(%this.cursorForm);
+ %this.cursorForm.build();
+
+ //--- Frame 3: the Borders pane -- a movable window of five border setters,
+ // shown only while a profile is selected (onTreeSelect toggles it). Added
+ // before the preview so it docks into the Borders frame.
+ %this.bordersWindow = new GuiWindowCtrl()
+ {
+ HorizSizing = "width";
+ VertSizing = "height";
+ Position = "0 0";
+ Extent = "360" SPC %paneHeight;
+ MinExtent = "220 120";
+ text = "Borders";
+ canMove = true;
+ canClose = false;
+ canMinimize = false;
+ canMaximize = false;
+ resizeWidth = true;
+ resizeHeight = true;
+ };
+ ThemeManager.setProfile(%this.bordersWindow, "windowProfile");
+ ThemeManager.setProfile(%this.bordersWindow, "windowContentProfile", "ContentProfile");
+ ThemeManager.setProfile(%this.bordersWindow, "windowButtonProfile", "CloseButtonProfile");
+ ThemeManager.setProfile(%this.bordersWindow, "windowButtonProfile", "MinButtonProfile");
+ ThemeManager.setProfile(%this.bordersWindow, "windowButtonProfile", "MaxButtonProfile");
+ %this.frames.add(%this.bordersWindow);
+
+ %this.bordersScroller = new GuiScrollCtrl()
+ {
+ HorizSizing = "fill";
+ VertSizing = "fill";
+ Position = "0 0";
+ Extent = "360" SPC %paneHeight;
+ hScrollBar = "alwaysOff";
+ vScrollBar = "dynamic";
+ constantThumbHeight = "0";
+ showArrowButtons = "1";
+ scrollBarThickness = "14";
+ };
+ ThemeManager.setProfile(%this.bordersScroller, "emptyProfile");
+ ThemeManager.setProfile(%this.bordersScroller, "thumbProfile", "ThumbProfile");
+ ThemeManager.setProfile(%this.bordersScroller, "trackProfile", "TrackProfile");
+ ThemeManager.setProfile(%this.bordersScroller, "scrollArrowProfile", "ArrowProfile");
+ %this.bordersWindow.add(%this.bordersScroller);
+
+ %this.borderChain = new GuiChainCtrl()
+ {
+ HorizSizing = "width";
+ VertSizing = "height";
+ Position = "0 0";
+ Extent = "346" SPC %paneHeight;
+ IsVertical = true;
+ ChildSpacing = 8;
+ };
+ ThemeManager.setProfile(%this.borderChain, "emptyProfile");
+ %this.bordersScroller.add(%this.borderChain);
+
+ %this.buildBorderSetters();
+
+ //--- Frame 4: the live preview.
+ %this.preview = new GuiControl()
+ {
+ class = "GuiProfileEditorPreview";
+ HorizSizing = "width";
+ VertSizing = "height";
+ Position = "0 0";
+ Extent = "400" SPC %paneHeight;
+ dialog = %this;
+ };
+ ThemeManager.setProfile(%this.preview, "emptyProfile");
+ %this.frames.add(%this.preview);
+
+ // The frame set only lays its frames out on a resize event; the dialog is
+ // created at its final size, so none fires and the frames stay collapsed
+ // until the user drags a divider. Force one layout pass now.
+ %this.frames.resize(0, 0, %width - 12, %paneHeight);
+
+ // The Borders pane starts collapsed; onTreeSelect opens it for profiles.
+ %this.hideBorders();
+
+ %this.cancelButton = new GuiButtonCtrl()
+ {
+ HorizSizing = "right";
+ VertSizing = "bottom";
+ Position = (%width - 226) SPC (%height - 64);
+ Extent = "100 30";
+ Text = "Cancel";
+ Command = %this.getID() @ ".onCancel();";
+ };
+ ThemeManager.setProfile(%this.cancelButton, "buttonProfile");
+ %content.add(%this.cancelButton);
+
+ %this.saveButton = new GuiButtonCtrl()
+ {
+ HorizSizing = "right";
+ VertSizing = "bottom";
+ Position = (%width - 116) SPC (%height - 66);
+ Extent = "100 34";
+ Text = "Save";
+ Command = %this.getID() @ ".onSave();";
+ };
+ ThemeManager.setProfile(%this.saveButton, "primaryButtonProfile");
+ %content.add(%this.saveButton);
+
+ %this.library.scanThemes();
+ %this.tree.inspect(%this.library.proxyRoot);
+ %this.toolbar.refreshEnabled();
+}
+
+function GuiProfileEditorDialog::onRemove(%this)
+{
+ // Nothing in the dying dialog may keep referencing theme members.
+ if(isObject(%this.preview))
+ {
+ %this.preview.clearSamples();
+ }
+ if(isObject(%this.profileForm))
+ {
+ %this.profileForm.unbind();
+ }
+ if(isObject(%this.themeForm))
+ {
+ %this.themeForm.unbind();
+ }
+ if(isObject(%this.borderForm))
+ {
+ %this.borderForm.unbind();
+ }
+ if(isObject(%this.cursorForm))
+ {
+ %this.cursorForm.unbind();
+ }
+ if(isObject(%this.borderChain))
+ {
+ %this.unbindBorderSetters();
+ }
+
+ // The library persists on GuiEditor; just detach from it.
+ if(isObject(%this.library))
+ {
+ %this.library.dialog = "";
+ }
+
+ editorMode(true);
+}
+
+//-----------------------------------------------------------------------------
+// Selection.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorDialog::onTreeSelect(%this, %proxy)
+{
+ %this.currentProxy = %proxy;
+ %this.currentRoot = "";
+ %this.currentMember = "";
+
+ %kind = %proxy.kind;
+ if(%kind $= "theme")
+ {
+ %this.currentRoot = %proxy.target;
+ %this.currentMember = %proxy.target;
+ }
+ else if(%kind $= "category")
+ {
+ %this.currentRoot = %proxy.theme;
+ %this.currentMember = %proxy.theme.getProfile(%proxy.category);
+ }
+ else if(%kind $= "border")
+ {
+ %this.currentRoot = %proxy.theme;
+ %this.currentMember = %proxy.theme.getBorder(%proxy.category);
+ }
+ else if(%kind $= "cursorCategory")
+ {
+ %this.currentRoot = %proxy.theme;
+ %this.currentMember = %proxy.theme.getCursor(%proxy.category);
+ }
+ else if(%kind $= "cursorExtra")
+ {
+ %this.currentRoot = %proxy.theme;
+ %this.currentMember = %proxy.target;
+ }
+ else if(%kind $= "extra")
+ {
+ %this.currentRoot = %proxy.theme;
+ %this.currentMember = %proxy.target;
+ }
+ else if(%kind $= "standalone")
+ {
+ %this.currentRoot = isObject(%proxy.root) ? %proxy.root : %proxy.target;
+ %this.currentMember = %proxy.target;
+ }
+
+ // The Properties window holds three custom panes, one per node kind: a theme
+ // gets the theme form, a border gets the border form, and a profile gets the
+ // profile form.
+ if(%this.isHeaderKind(%kind))
+ {
+ // Header rows ("Gui Themes", "Profiles", "Borders", "Stand Alone") are
+ // grouping labels with nothing to edit. Every pane hides, so the window
+ // goes empty instead of leaving the last profile's rows on screen -
+ // unbind() only drops the binding, it does not clear what is drawn.
+ %this.hideMemberPanes();
+ }
+ else if(%kind $= "theme")
+ {
+ %this.hideMemberPanes();
+ %this.formScroller.setVisible(true);
+ %this.themeForm.bindTheme(%this.currentMember);
+ }
+ else if(%kind $= "border")
+ {
+ %this.hideMemberPanes();
+ %this.borderFormScroller.setVisible(true);
+ %this.borderForm.bind(%this.currentMember, %proxy.treeLabel);
+ }
+ else if(%this.isCursorKind(%kind))
+ {
+ %this.hideMemberPanes();
+ %this.cursorFormScroller.setVisible(true);
+ %this.cursorForm.bind(%this.currentMember, %proxy.treeLabel);
+ }
+ else
+ {
+ %this.hideMemberPanes();
+ %this.profileFormScroller.setVisible(true);
+ %this.profileForm.bind(%this.currentMember, %kind);
+ }
+
+ // The Borders pane rides alongside the profile form, but only for profiles.
+ if(%this.isProfileKind(%kind))
+ {
+ %this.showBorders();
+ }
+ else
+ {
+ %this.hideBorders();
+ }
+
+ %this.updatePreview();
+ %this.toolbar.refreshEnabled();
+}
+
+function GuiProfileEditorDialog::updatePreview(%this)
+{
+ if(!isObject(%this.preview))
+ {
+ return;
+ }
+
+ %proxy = %this.currentProxy;
+ %kind = isObject(%proxy) ? %proxy.kind : "";
+
+ if(%kind $= "theme")
+ {
+ %this.preview.showTheme(%proxy.target);
+ }
+ else if(%kind $= "category")
+ {
+ %this.preview.showCategory(%proxy.theme, %proxy.category, %this.currentMember);
+ }
+ else if(%kind $= "extra")
+ {
+ %this.preview.showCategory(%proxy.theme, %proxy.category, %proxy.target);
+ }
+ else if(%kind $= "border")
+ {
+ %this.preview.showBorder(%proxy.theme, %this.currentMember);
+ }
+ else if(%this.isCursorKind(%kind))
+ {
+ %this.preview.showCursor(%proxy.theme, %this.currentMember);
+ }
+ else if(%kind $= "standalone")
+ {
+ %this.preview.showCategory("", %proxy.target.category, %proxy.target);
+ }
+ else
+ {
+ %this.preview.clearSamples();
+ }
+}
+
+// The profile form reports every applied field edit here.
+function GuiProfileEditorDialog::onProfileChanged(%this, %object)
+{
+ if(isObject(%this.currentRoot))
+ {
+ %this.library.markDirty(%this.currentRoot);
+ }
+ %this.schedulePreviewRefresh();
+}
+
+// Rebuild the preview on the next tick instead of right now, coalescing rapid
+// edits. A field commit can arrive from inside an input-event/focus-change
+// callback -- e.g. a border or profile-form text box losing first-responder while
+// the user clicks a live preview sample. Rebuilding there would delete the very
+// sample control the engine is mid-dispatch on, freeing it under its own
+// onTouchDown/setFirstResponder (a use-after-free crash). Deferring runs the
+// rebuild only after the current event has fully unwound.
+function GuiProfileEditorDialog::schedulePreviewRefresh(%this)
+{
+ if(!isObject(%this.preview))
+ {
+ return;
+ }
+ if(isEventPending(%this.previewRefreshEvent))
+ {
+ cancel(%this.previewRefreshEvent);
+ }
+ %this.previewRefreshEvent = %this.schedule(0, "doPreviewRefresh");
+}
+
+function GuiProfileEditorDialog::doPreviewRefresh(%this)
+{
+ %this.previewRefreshEvent = "";
+ if(isObject(%this.preview))
+ {
+ %this.preview.refresh();
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Borders pane.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorDialog::isProfileKind(%this, %kind)
+{
+ return %kind $= "category" || %kind $= "extra" || %kind $= "standalone";
+}
+
+function GuiProfileEditorDialog::isCursorKind(%this, %kind)
+{
+ return %kind $= "cursorCategory" || %kind $= "cursorExtra";
+}
+
+// The tree's grouping rows: the "Gui Themes" root and the "Stand Alone",
+// "Profiles" and "Borders" folders. They carry no editable target.
+function GuiProfileEditorDialog::isHeaderKind(%this, %kind)
+{
+ return %kind $= "root" || %kind $= "folder";
+}
+
+// Drops all four member panes out of the Properties window. Each pane is also
+// unbound so it stops tracking whatever it last showed.
+function GuiProfileEditorDialog::hideMemberPanes(%this)
+{
+ %this.profileFormScroller.setVisible(false);
+ %this.profileForm.unbind();
+ %this.formScroller.setVisible(false);
+ %this.themeForm.unbind();
+ %this.borderFormScroller.setVisible(false);
+ %this.borderForm.unbind();
+ %this.cursorFormScroller.setVisible(false);
+ %this.cursorForm.unbind();
+}
+
+// Build the five setters into the pane chain: the default (no checkbox, full
+// width) then the four sides (checkbox, indented 20px so they read as "under"
+// the default).
+function GuiProfileEditorDialog::buildBorderSetters(%this)
+{
+ %w = 340;
+ %slots = "default" TAB "top" TAB "bottom" TAB "left" TAB "right";
+ %titles = "Default" TAB "Top" TAB "Bottom" TAB "Left" TAB "Right";
+
+ for(%i = 0; %i < 5; %i++)
+ {
+ %slot = getField(%slots, %i);
+ %isDefault = %slot $= "default";
+ %indent = %isDefault ? 0 : 20;
+
+ %setter = new GuiChainCtrl()
+ {
+ class = "GuiProfileEditorBorderSetter";
+ Position = %indent SPC 0;
+ slot = %slot;
+ slotTitle = getField(%titles, %i);
+ hasCheckbox = !%isDefault;
+ setterWidth = %w - %indent;
+ dialog = %this;
+ };
+ %this.borderChain.add(%setter);
+ %this.borderSetter[%slot] = %setter;
+ }
+}
+
+function GuiProfileEditorDialog::bindBorderSetters(%this)
+{
+ if(!isObject(%this.currentMember))
+ {
+ return;
+ }
+ %container = %this.currentRoot;
+
+ // The default binds first so the sides can grey out the border it resolves to.
+ %this.borderSetter["default"].bind(%this.currentMember, %container, "");
+ %disable = %this.borderSetter["default"].currentCategory();
+
+ %sides = "top" TAB "bottom" TAB "left" TAB "right";
+ for(%i = 0; %i < 4; %i++)
+ {
+ %this.borderSetter[getField(%sides, %i)].bind(%this.currentMember, %container, %disable);
+ }
+}
+
+function GuiProfileEditorDialog::unbindBorderSetters(%this)
+{
+ %slots = "default" TAB "top" TAB "bottom" TAB "left" TAB "right";
+ for(%i = 0; %i < 5; %i++)
+ {
+ %setter = %this.borderSetter[getField(%slots, %i)];
+ if(isObject(%setter))
+ {
+ %setter.unbind();
+ }
+ }
+}
+
+// After the Default setter's border changes, update which border each side list
+// greys out (the one the default now resolves to).
+function GuiProfileEditorDialog::refreshSideDisables(%this)
+{
+ if(!isObject(%this.borderSetter["default"]))
+ {
+ return;
+ }
+ %disable = %this.borderSetter["default"].currentCategory();
+ %sides = "top" TAB "bottom" TAB "left" TAB "right";
+ for(%i = 0; %i < 4; %i++)
+ {
+ %setter = %this.borderSetter[getField(%sides, %i)];
+ %setter.clearIfMatches(%disable);
+ %setter.applyDisable(%disable);
+ }
+}
+
+function GuiProfileEditorDialog::showBorders(%this)
+{
+ %this.frames.setFrameSize(%this.bordersFrame, 360);
+ %this.bordersWindow.setVisible(true);
+ %this.bindBorderSetters();
+}
+
+function GuiProfileEditorDialog::hideBorders(%this)
+{
+ %this.unbindBorderSetters();
+ if(isObject(%this.bordersWindow))
+ {
+ %this.bordersWindow.setVisible(false);
+ }
+ %this.frames.setFrameSize(%this.bordersFrame, 0);
+}
+
+// The theme's six named border categories for the dropdowns; a standalone
+// bundle (no theme) has none.
+function GuiProfileEditorDialog::borderNamesFor(%this, %container)
+{
+ if(isObject(%container) && %container.getClassName() $= "GuiProfileTheme")
+ {
+ return %container.getBorderCategoryNames();
+ }
+ return "";
+}
+
+function GuiProfileEditorDialog::borderObjectFor(%this, %container, %category)
+{
+ if(isObject(%container) && %container.getClassName() $= "GuiProfileTheme")
+ {
+ return %container.getBorder(%category);
+ }
+ return "";
+}
+
+// Create a single-use custom border owned by the container (the caller seeds it).
+// Named outside editor mode, like everything else the library names, so the name
+// a profile stores for its border actually resolves (see beginNaming).
+function GuiProfileEditorDialog::createCustomBorder(%this, %container, %name)
+{
+ if(isObject(%container) && %container.getClassName() $= "GuiProfileTheme")
+ {
+ %this.library.beginNaming();
+ %border = %container.createBorder(%name);
+ %this.library.endNaming();
+ return %border;
+ }
+ // Standalone bundle: a plain custom border added to the bundle, kept ahead
+ // of the profile so the default's object reference resolves on reload.
+ %this.library.beginNaming();
+ %border = new GuiBorderProfile(%name) { isCustom = true; };
+ %this.library.endNaming();
+
+ if(%this.library.isBundle(%container))
+ {
+ %container.add(%border);
+ if(isObject(%this.currentMember))
+ {
+ %container.pushToBack(%this.currentMember);
+ }
+ }
+ return %border;
+}
+
+function GuiProfileEditorDialog::onBorderChanged(%this)
+{
+ if(isObject(%this.currentRoot))
+ {
+ %this.library.markDirty(%this.currentRoot);
+ }
+ %this.schedulePreviewRefresh();
+}
+
+//-----------------------------------------------------------------------------
+// Toolbar enabled-state callbacks.
+//-----------------------------------------------------------------------------
+
+// The two node kinds that are a save root: a theme and a stand-alone profile
+// each carry a name of their own and own a file of their own, which is exactly
+// what Rename and Delete need. Everything else in the tree belongs to one of
+// them - a theme member takes the name its theme gives it and is written into
+// the theme's file.
+function GuiProfileEditorDialog::getRootSelected(%this)
+{
+ if(!isObject(%this.currentProxy))
+ {
+ return false;
+ }
+ %kind = %this.currentProxy.kind;
+ return %kind $= "theme" || %kind $= "standalone";
+}
+
+// The save root the selection stands for: a theme is its own, a stand-alone
+// profile's is the bundle its file holds.
+function GuiProfileEditorDialog::selectedRoot(%this)
+{
+ if(!%this.getRootSelected())
+ {
+ return 0;
+ }
+ return (%this.currentProxy.kind $= "theme") ? %this.currentProxy.target : %this.currentProxy.root;
+}
+
+// The two "new in a category" buttons serve profiles and cursors alike: both
+// families are a category row that can hold extras, so one pair of buttons that
+// asks the selection what it is beats a second pair that would sit greyed out
+// nine tenths of the time.
+function GuiProfileEditorDialog::getCategorySelected(%this)
+{
+ if(!isObject(%this.currentProxy))
+ {
+ return false;
+ }
+ %kind = %this.currentProxy.kind;
+ return %kind $= "category" || %kind $= "cursorCategory";
+}
+
+function GuiProfileEditorDialog::getExtraSelected(%this)
+{
+ if(!isObject(%this.currentProxy))
+ {
+ return false;
+ }
+ %kind = %this.currentProxy.kind;
+ return %kind $= "extra" || %kind $= "cursorExtra";
+}
+
+function GuiProfileEditorDialog::getMemberSelected(%this)
+{
+ if(!isObject(%this.currentProxy))
+ {
+ return false;
+ }
+ %kind = %this.currentProxy.kind;
+ return %kind $= "category" || %kind $= "border" || %kind $= "extra" || %this.isCursorKind(%kind);
+}
+
+// What the two shared "in a category" buttons are about to do. A cursor
+// category and a profile category are both a row that can hold extras, so one
+// pair of buttons serves both - but a tip reading "Profile" while a cursor is
+// selected describes the wrong thing, which is worse than no tip.
+function GuiProfileEditorDialog::newInCategoryTip(%this)
+{
+ %kind = isObject(%this.currentProxy) ? %this.currentProxy.kind : "";
+ return (%kind $= "cursorCategory") ? "New Cursor in Category" : "New Profile in Category";
+}
+
+function GuiProfileEditorDialog::removeExtraTip(%this)
+{
+ %kind = isObject(%this.currentProxy) ? %this.currentProxy.kind : "";
+ return (%kind $= "cursorExtra") ? "Remove Extra Cursor" : "Remove Extra Profile";
+}
+
+//-----------------------------------------------------------------------------
+// Toolbar operations.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorDialog::onNewTheme(%this)
+{
+ %this.openNameDialog("New Theme", "doCreateTheme", "");
+}
+
+function GuiProfileEditorDialog::doCreateTheme(%this, %name)
+{
+ %theme = %this.library.createTheme(%name);
+ if(isObject(%theme))
+ {
+ %this.tree.refresh();
+ }
+}
+
+// One button for both things that carry a name of their own: a theme, and a
+// stand-alone profile. A theme member has no name to rename - it takes the one
+// its theme gives it.
+function GuiProfileEditorDialog::onRename(%this)
+{
+ if(!%this.getRootSelected())
+ {
+ return;
+ }
+
+ %isTheme = %this.currentProxy.kind $= "theme";
+ %title = %isTheme ? "Rename Theme" : "Rename Stand Alone Profile";
+ %this.openNameDialog(%title, "doRename", %this.currentProxy.target.getName());
+}
+
+function GuiProfileEditorDialog::doRename(%this, %name)
+{
+ if(!%this.getRootSelected())
+ {
+ return;
+ }
+
+ %proxy = %this.currentProxy;
+ if(%proxy.kind $= "theme")
+ {
+ if(!%this.library.renameThemeTo(%proxy.target, %name))
+ {
+ return;
+ }
+ // The theme form is the pane showing for a theme node; refresh
+ // its Name label to the new name.
+ %this.themeForm.bindTheme(%proxy.target);
+ }
+ else
+ {
+ if(!%this.library.renameStandaloneTo(%proxy.root, %name))
+ {
+ return;
+ }
+ // Likewise the profile pane, whose header names the profile.
+ %this.profileForm.bind(%proxy.target, %proxy.kind);
+ }
+
+ %this.tree.refresh();
+}
+
+function GuiProfileEditorDialog::onDelete(%this)
+{
+ if(!%this.getRootSelected())
+ {
+ return;
+ }
+
+ %name = %this.currentProxy.target.getName();
+ if(%this.currentProxy.kind $= "theme")
+ {
+ %title = "Delete Theme";
+ %message = "Delete the theme \"" @ %name @ "\" and all of its profiles? The file is removed when you save.";
+ }
+ else
+ {
+ // Nothing can list the Guis that name this profile, so say plainly what
+ // becomes of them rather than pretending to have checked.
+ %title = "Delete Stand Alone Profile";
+ %message = "Delete the stand alone profile \"" @ %name @ "\"? The file is removed when you save, and any control still asking for it falls back to the default profile.";
+ }
+
+ %this.openConfirmDialog(%title, %message, "Delete", "doDelete");
+}
+
+function GuiProfileEditorDialog::doDelete(%this)
+{
+ %root = %this.selectedRoot();
+ if(!isObject(%root))
+ {
+ return;
+ }
+
+ %isTheme = %this.currentProxy.kind $= "theme";
+
+ // Detach everything that renders or inspects what is about to die: the
+ // preview samples wear these profiles, the member panes are bound to one,
+ // and the Borders pane's setters hold both the profile and its container.
+ %this.preview.clearSamples();
+ %this.hideMemberPanes();
+ %this.hideBorders();
+ %this.currentProxy = "";
+ %this.currentRoot = "";
+ %this.currentMember = "";
+
+ if(%isTheme)
+ {
+ %this.library.deleteTheme(%root);
+ }
+ else
+ {
+ %this.library.deleteStandalone(%root);
+ }
+
+ %this.tree.refresh();
+ %this.toolbar.refreshEnabled();
+}
+
+function GuiProfileEditorDialog::onNewProfile(%this)
+{
+ if(!%this.getCategorySelected())
+ {
+ return;
+ }
+
+ %theme = %this.currentProxy.theme;
+ %category = %this.currentProxy.category;
+
+ if(%this.currentProxy.kind $= "cursorCategory")
+ {
+ %member = %this.library.createExtraCursor(%theme, %category);
+ }
+ else
+ {
+ %member = %this.library.createExtraProfile(%theme, %category);
+ }
+
+ if(isObject(%member))
+ {
+ %this.tree.refresh();
+ }
+}
+
+function GuiProfileEditorDialog::onRemoveProfile(%this)
+{
+ if(!%this.getExtraSelected())
+ {
+ return;
+ }
+ %theme = %this.currentProxy.theme;
+ %member = %this.currentProxy.target;
+ %isCursor = %this.currentProxy.kind $= "cursorExtra";
+
+ %this.preview.clearSamples();
+ %this.hideMemberPanes();
+ %this.currentProxy = "";
+ %this.currentRoot = "";
+ %this.currentMember = "";
+
+ %orphanedArt = "";
+ if(%isCursor)
+ {
+ %orphanedArt = %this.library.removeExtraCursor(%theme, %member);
+ }
+ else
+ {
+ %this.library.removeExtraProfile(%theme, %member);
+ }
+
+ %this.tree.refresh();
+ %this.toolbar.refreshEnabled();
+
+ // Removing a cursor leaves its picture behind. Deleting that as a side
+ // effect would be a file thrown away without being asked about, and keeping
+ // it always would litter the folder with the art of every cursor ever tried.
+ // So ask -- but only when the answer is not already obvious: the library
+ // hands back a path only for art it made, that nothing else is using, and
+ // that is really on disk.
+ if(%orphanedArt !$= "")
+ {
+ %this.doomedCursorArt = %orphanedArt;
+ %this.openConfirmDialog("Delete Image",
+ "The cursor is gone. Its image file, " @ fileName(%orphanedArt) @
+ ", is not used by any other cursor. Delete it as well?",
+ "Delete Image", "doDeleteCursorArt");
+ }
+}
+
+// The confirm arm of the question above. Cancel is the other, and it does
+// nothing at all: the file stays where it is.
+function GuiProfileEditorDialog::doDeleteCursorArt(%this)
+{
+ if(%this.doomedCursorArt !$= "")
+ {
+ %this.library.deleteCursorArt(%this.doomedCursorArt);
+ %this.doomedCursorArt = "";
+ }
+}
+
+function GuiProfileEditorDialog::onNewStandalone(%this)
+{
+ %this.openNameDialog("New Stand Alone Profile", "doCreateStandalone", "");
+}
+
+function GuiProfileEditorDialog::doCreateStandalone(%this, %name)
+{
+ %profile = %this.library.createStandalone(%name);
+ if(isObject(%profile))
+ {
+ %this.tree.refresh();
+ }
+}
+
+function GuiProfileEditorDialog::onResetMember(%this)
+{
+ if(!%this.getMemberSelected() || !isObject(%this.currentMember))
+ {
+ return;
+ }
+
+ %this.currentRoot.resetProfile(%this.currentMember);
+
+ // Reload whichever pane is showing the member the overrides were cleared on.
+ if(%this.currentProxy.kind $= "border")
+ {
+ %this.borderForm.bind(%this.currentMember, %this.currentProxy.treeLabel);
+ }
+ else if(%this.isCursorKind(%this.currentProxy.kind))
+ {
+ %this.cursorForm.refresh();
+ }
+ else
+ {
+ %this.profileForm.refresh();
+ }
+
+ %this.library.markDirty(%this.currentRoot);
+ %this.updatePreview();
+}
+
+//-----------------------------------------------------------------------------
+// Helper dialogs.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorDialog::openNameDialog(%this, %title, %callback, %defaultName)
+{
+ %width = 400;
+ %height = 130;
+ %dialog = new GuiControl()
+ {
+ class = "GuiProfileEditorNameDialog";
+ superclass = "EditorDialog";
+ dialogSize = (%width + 8) SPC (%height + 8);
+ dialogCanClose = true;
+ dialogText = %title;
+ callbackTarget = %this;
+ callbackMethod = %callback;
+ defaultName = %defaultName;
+ };
+ %dialog.init(%width, %height);
+ %this.childDialog = %dialog;
+
+ Canvas.pushDialog(%dialog);
+}
+
+function GuiProfileEditorDialog::openConfirmDialog(%this, %title, %message, %confirmText, %callback)
+{
+ %width = 500;
+
+ // Grow the dialog to fit the message rather than clipping it: the text wraps
+ // at roughly 54 characters to a line at this width, and the dialog needs
+ // room for the buttons under it. Two lines is the floor, so the short
+ // messages keep the shape they had.
+ %lines = mGetMax(mCeil(strlen(%message) / 54), 2);
+ %height = 100 + (%lines * 24);
+ %dialog = new GuiControl()
+ {
+ class = "GuiProfileEditorConfirmDialog";
+ superclass = "EditorDialog";
+ dialogSize = (%width + 8) SPC (%height + 8);
+ dialogCanClose = true;
+ dialogText = %title;
+ message = %message;
+ confirmText = %confirmText;
+ callbackTarget = %this;
+ callbackMethod = %callback;
+ };
+ %dialog.init(%width, %height);
+ %this.childDialog = %dialog;
+
+ Canvas.pushDialog(%dialog);
+}
+
+//-----------------------------------------------------------------------------
+// Close protocol. The window X and the Cancel button both land in onClose;
+// discarding reverts every dirty theme by reloading it from its file (the
+// themes themselves persist beyond the dialog).
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorDialog::onCancel(%this)
+{
+ %this.onClose();
+}
+
+function GuiProfileEditorDialog::onClose(%this)
+{
+ if(%this.library.isDirty())
+ {
+ %this.openConfirmDialog("Discard Changes", "You have unsaved theme changes. Discard them?", "Discard", "discardAndClose");
+ return;
+ }
+ %this.closeNow();
+}
+
+function GuiProfileEditorDialog::onSave(%this)
+{
+ %this.library.saveAll();
+ %this.closeNow();
+}
+
+function GuiProfileEditorDialog::discardAndClose(%this)
+{
+ // Reverting deletes dirty themes and reloads them from their files, so
+ // detach everything that renders or inspects members first.
+ %this.preview.clearSamples();
+ %this.profileForm.unbind();
+ %this.borderForm.unbind();
+ %this.themeForm.unbind();
+ %this.library.revertAll();
+ %this.closeNow();
+}
+
+function GuiProfileEditorDialog::closeNow(%this)
+{
+ Canvas.popDialog(%this);
+ EditorCore.dialog = %this;
+ EditorCore.schedule(100, "deleteDialog");
+}
diff --git a/editor/GuiEditor/scripts/GuiProfileEditorFieldSpec.cs b/editor/GuiEditor/scripts/GuiProfileEditorFieldSpec.cs
new file mode 100644
index 000000000..486f5ad1b
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiProfileEditorFieldSpec.cs
@@ -0,0 +1,332 @@
+
+//-----------------------------------------------------------------------------
+// The context model behind the Gui Profile Editor's profile pane: which of a
+// GuiControlProfile's fields actually matter for the control the profile is
+// made for.
+//
+// A profile already records its purpose in its "category" field -- one of the
+// engine-defined names in GuiProfileTheme::smProfileCategories[] -- so that is
+// the only filter input. Each category is described by three things:
+//
+// textClass How the category's control draws text with this profile:
+// full through GuiControl::renderText -- every font field
+// applies, including align/vAlign/textOffset (those are
+// read nowhere else; see guiControl.cc renderText).
+// direct through dglDrawText without renderText, so the face,
+// size and colors apply but the layout fields do not.
+// glyph no text at all, but getFontColor(state) tints a drawn
+// icon (scroll-bar arrows, window title-bar buttons).
+// none never draws text or reads a font color.
+// states Which of normal/HL/SL/NA the control actually passes to
+// renderUniversalRect, as four flags. Unreachable states are
+// greyed rather than hidden, so a value is never lost.
+// flags focus tab + canKeyFocus mean something here.
+// caretBeam cursorColor is the text caret.
+// caretRect cursorColor is the keyboard-focus rectangle.
+// textsel fillColorTextSL + fontColorTextSL are used.
+// circle the control draws a circle or ring, which reads only
+// the default border -- the four sides do nothing.
+// alignOn keep "align" even though the class would drop it.
+// alignOff drop "align" even though the class would keep it.
+//
+// Fields absent from the seven groups below are never shown at all, even with
+// Show All: SimObject plumbing (class, superclass, hidden, locked, parentGroup,
+// internalName, canSaveDynamicFields), theme bookkeeping (category,
+// themeOverrides), the border references the Borders pane owns, fontColors0-6,
+// which the inspector's array expansion duplicates from the seven named aliases,
+// and fontDirectory, which the editor owns: every project keeps its font caches
+// in one folder (GuiProfileEditorLibrary::getFontsPath), so there is nothing for
+// a profile to decide.
+//
+// The spec is pure data with no UI; the pane owns one and asks it what to show.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorFieldSpec::onAdd(%this)
+{
+ // category TAB textClass TAB states TAB flags. States read normal, HL, SL,
+ // NA. Every entry is derived from the control's render path, not from the
+ // theme recipe -- a recipe stamps far more than its control ever reads.
+ %table =
+ "Empty" TAB "none" TAB "1000" TAB "" NL
+ "Tooltip" TAB "direct" TAB "1000" TAB "" NL
+ "Panel" TAB "none" TAB "1000" TAB "" NL
+ "Button" TAB "full" TAB "1111" TAB "focus" NL
+ "CheckBox" TAB "full" TAB "1111" TAB "focus caretRect" NL
+ "Radio" TAB "full" TAB "1111" TAB "focus caretRect circle" NL
+ "Label" TAB "full" TAB "1000" TAB "" NL
+ "TextEdit" TAB "full" TAB "1111" TAB "focus caretBeam textsel" NL
+ "Scroll" TAB "none" TAB "1000" TAB "" NL
+ "ScrollTrack" TAB "none" TAB "1001" TAB "" NL
+ "ScrollThumb" TAB "none" TAB "1110" TAB "" NL
+ "ScrollArrow" TAB "glyph" TAB "1111" TAB "" NL
+ "TabBook" TAB "none" TAB "1000" TAB "" NL
+ "Tab" TAB "full" TAB "1111" TAB "" NL
+ "TabPage" TAB "none" TAB "1000" TAB "" NL
+ "ListBox" TAB "full" TAB "1111" TAB "focus" NL
+ "DropDown" TAB "full" TAB "1111" TAB "focus caretRect" NL
+ "DropDownItem" TAB "full" TAB "1111" TAB "" NL
+ "Window" TAB "full" TAB "1111" TAB "" NL
+ "WindowContent" TAB "none" TAB "1111" TAB "" NL
+ "WindowButton" TAB "glyph" TAB "1111" TAB "alignOn" NL
+ "WindowCloseButton" TAB "glyph" TAB "1111" TAB "alignOn" NL
+ "MenuBar" TAB "none" TAB "1000" TAB "focus" NL
+ "Menu" TAB "full" TAB "1111" TAB "" NL
+ "MenuItem" TAB "full" TAB "1111" TAB "alignOff" NL
+ "MenuContent" TAB "none" TAB "1000" TAB "" NL
+ "Overlay" TAB "none" TAB "1000" TAB "" NL
+ "Progress" TAB "full" TAB "1110" TAB "" NL
+ "TreeView" TAB "full" TAB "1111" TAB "focus" NL
+ "FrameSet" TAB "none" TAB "1111" TAB "" NL
+ "FrameSetDropButton" TAB "none" TAB "1110" TAB "" NL
+ "ColorPicker" TAB "full" TAB "1111" TAB "focus" NL
+ "ColorSelector" TAB "none" TAB "1110" TAB "circle" NL
+ "ColorPopup" TAB "none" TAB "1000" TAB "" NL
+ "DragAndDrop" TAB "full" TAB "1000" TAB "" NL
+ "Slider" TAB "direct" TAB "1000" TAB "" NL
+ "SliderThumb" TAB "none" TAB "1110" TAB "";
+
+ %this.categoryNames = "";
+ %count = getRecordCount(%table);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %rec = getRecord(%table, %i);
+ %name = trim(getField(%rec, 0));
+
+ // "class" is a real SimObject field, so the table uses textClass.
+ %this.textClass[%name] = trim(getField(%rec, 1));
+ %this.liveStates[%name] = trim(getField(%rec, 2));
+ %this.flags[%name] = trim(getField(%rec, 3));
+
+ %this.categoryNames = (%this.categoryNames $= "") ? %name : (%this.categoryNames TAB %name);
+ }
+}
+
+//-----------------------------------------------------------------------------
+// The field groups. Membership of one of these is what makes a field showable
+// at all; the predicates below decide whether the current category wants it.
+//-----------------------------------------------------------------------------
+
+// Shown for every category: every profile renders through renderUniversalRect,
+// which takes the image path before the fill path, so images always apply.
+function GuiProfileEditorFieldSpec::universalFields(%this)
+{
+ return "fillColor fillColorHL fillColorSL fillColorNA imageAsset bitmap";
+}
+
+// The typeface itself: needed by anything that rasterizes glyphs.
+function GuiProfileEditorFieldSpec::fontFaceFields(%this)
+{
+ return "fontType fontSize fontCharset";
+}
+
+// Read only inside GuiControl::renderText, so only the "full" class needs them.
+function GuiProfileEditorFieldSpec::textLayoutFields(%this)
+{
+ return "align vAlign textOffset";
+}
+
+// The four state colors, used for glyphs and for icons drawn in getFontColor.
+function GuiProfileEditorFieldSpec::fontColorFields(%this)
+{
+ return "fontColor fontColorHL fontColorSL fontColorNA";
+}
+
+// Reachable only as \c4, \c5 and \c7-\c9 escapes inside drawn text (the color
+// table dglDrawText receives), so they need a category that draws glyphs.
+function GuiProfileEditorFieldSpec::richTextFields(%this)
+{
+ return "fontColorLink fontColorLinkHL fontColors7 fontColors8 fontColors9";
+}
+
+function GuiProfileEditorFieldSpec::focusFields(%this)
+{
+ return "tab canKeyFocus";
+}
+
+// The text-selection pair, whose only consumer is GuiTextEditCtrl.
+function GuiProfileEditorFieldSpec::textSelectionFields(%this)
+{
+ return "fillColorTextSL fontColorTextSL";
+}
+
+// Every field the pane can ever show, in section order.
+function GuiProfileEditorFieldSpec::allFields(%this)
+{
+ return %this.universalFields() SPC %this.fontFaceFields() SPC
+ %this.textLayoutFields() SPC %this.fontColorFields() SPC
+ %this.richTextFields() SPC %this.focusFields() SPC
+ %this.textSelectionFields() SPC "cursorColor";
+}
+
+// Space-delimited membership. Wrapping both sides in spaces keeps fontColor
+// from matching inside fontColorHL.
+function GuiProfileEditorFieldSpec::listHas(%this, %list, %item)
+{
+ return strstr(" " @ %list @ " ", " " @ %item @ " ") >= 0;
+}
+
+//-----------------------------------------------------------------------------
+// Category lookup. An empty or unrecognized category (a fresh standalone
+// profile, or a category this table has not caught up with) is treated as
+// unknown, and an unknown category shows everything -- never less.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorFieldSpec::isKnownCategory(%this, %category)
+{
+ return %category !$= "" && %this.textClass[%category] !$= "";
+}
+
+function GuiProfileEditorFieldSpec::textClassFor(%this, %category)
+{
+ return %this.isKnownCategory(%category) ? %this.textClass[%category] : "full";
+}
+
+function GuiProfileEditorFieldSpec::hasFlag(%this, %category, %flag)
+{
+ if(!%this.isKnownCategory(%category))
+ {
+ return true;
+ }
+ return %this.listHas(%this.flags[%category], %flag);
+}
+
+// True if the control ever renders in this state (0 = normal, 1 = HL, 2 = SL,
+// 3 = NA). Unknown categories report every state live.
+function GuiProfileEditorFieldSpec::isStateLive(%this, %category, %stateIndex)
+{
+ if(!%this.isKnownCategory(%category))
+ {
+ return true;
+ }
+ return getSubStr(%this.liveStates[%category], %stateIndex, 1) $= "1";
+}
+
+// The label cursorColor should wear: the same field paints the text caret in a
+// GuiTextEditCtrl and the keyboard-focus rectangle on a checkbox or drop-down.
+function GuiProfileEditorFieldSpec::cursorLabelFor(%this, %category)
+{
+ return %this.hasFlag(%category, "caretRect") ? "Focus Rect Color" : "Caret Color";
+}
+
+//-----------------------------------------------------------------------------
+// The single filter predicate. Everything the pane hides or shows goes through
+// here, so the rules live in exactly one place.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorFieldSpec::isFieldVisible(%this, %category, %field, %showAll)
+{
+ // Fields outside the groups are never shown, Show All included.
+ if(!%this.listHas(%this.allFields(), %field))
+ {
+ return false;
+ }
+
+ if(%showAll)
+ {
+ return true;
+ }
+
+ if(%this.listHas(%this.universalFields(), %field))
+ {
+ return true;
+ }
+
+ %class = %this.textClassFor(%category);
+ %drawsGlyphs = %class $= "full" || %class $= "direct";
+ %usesFontColor = %drawsGlyphs || %class $= "glyph";
+
+ if(%field $= "align")
+ {
+ // Two categories disagree with their class: the window title-bar buttons
+ // read align to pick their edge (guiWindowCtrl.cc renderButton) while a
+ // menu item has align overwritten every render (guiMenuBarCtrl.cc), so
+ // editing it there would be a lie.
+ if(%this.hasFlag(%category, "alignOff"))
+ {
+ return false;
+ }
+ if(%this.hasFlag(%category, "alignOn"))
+ {
+ return true;
+ }
+ return %class $= "full";
+ }
+
+ if(%this.listHas(%this.textLayoutFields(), %field))
+ {
+ return %class $= "full";
+ }
+
+ if(%this.listHas(%this.fontFaceFields(), %field))
+ {
+ return %drawsGlyphs;
+ }
+
+ if(%this.listHas(%this.fontColorFields(), %field))
+ {
+ return %usesFontColor;
+ }
+
+ if(%this.listHas(%this.richTextFields(), %field))
+ {
+ return %drawsGlyphs;
+ }
+
+ if(%this.listHas(%this.focusFields(), %field))
+ {
+ return %this.hasFlag(%category, "focus");
+ }
+
+ if(%this.listHas(%this.textSelectionFields(), %field))
+ {
+ return %this.hasFlag(%category, "textsel");
+ }
+
+ if(%field $= "cursorColor")
+ {
+ return %this.hasFlag(%category, "caretBeam") || %this.hasFlag(%category, "caretRect");
+ }
+
+ return false;
+}
+
+// True when any of the fields is visible, so a section with nothing left to
+// show can hide its whole panel.
+function GuiProfileEditorFieldSpec::anyFieldVisible(%this, %category, %fields, %showAll)
+{
+ %count = getWordCount(%fields);
+ for(%i = 0; %i < %count; %i++)
+ {
+ if(%this.isFieldVisible(%category, getWord(%fields, %i), %showAll))
+ {
+ return true;
+ }
+ }
+ return false;
+}
+
+// Confirm the table still matches the engine's category list. The editor never
+// calls this; the smoke test does, so a category added to
+// GuiProfileTheme::smProfileCategories[] cannot drift unnoticed.
+//
+// %engineNames is what GuiProfileTheme::getCategoryNames() returns: a
+// space-separated list. Its casing will not always match this file's, because
+// the names are string-table entries and StringTable::insert is
+// case-insensitive -- "Empty" comes back as whatever casing the engine
+// interned first. That costs nothing here: a dynamic field name is interned the
+// same way, so textClass["empty"] and textClass["Empty"] are one slot.
+function GuiProfileEditorFieldSpec::findMissingCategories(%this, %engineNames)
+{
+ %missing = "";
+ %count = getWordCount(%engineNames);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %name = getWord(%engineNames, %i);
+ if(%name $= "" || %this.textClass[%name] !$= "")
+ {
+ continue;
+ }
+ %missing = (%missing $= "") ? %name : (%missing SPC %name);
+ }
+ return %missing;
+}
diff --git a/editor/GuiEditor/scripts/GuiProfileEditorLibrary.cs b/editor/GuiEditor/scripts/GuiProfileEditorLibrary.cs
new file mode 100644
index 000000000..90f288d17
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiProfileEditorLibrary.cs
@@ -0,0 +1,1827 @@
+
+//-----------------------------------------------------------------------------
+// The data side of the Gui Profile Editor. Owned by GuiEditor and persistent
+// across dialog sessions, so theme member profiles stay alive for the guis
+// that reference them (and appear in the Gui Editor's profile dropdowns).
+// Loads themes and standalone profiles from the project's themes folder,
+// maintains the proxy hierarchy the dialog tree displays, tracks dirty
+// roots, saves them to their files, and reverts them by re-reading those
+// files.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorLibrary::onAdd(%this)
+{
+ %this.themeGroup = new SimGroup();
+
+ %this.proxyRoot = new SimGroup()
+ {
+ kind = "root";
+ treeLabel = "Gui Themes";
+ };
+
+ %this.standaloneFolder = new SimGroup()
+ {
+ kind = "folder";
+ treeLabel = "Stand Alone";
+ };
+ %this.proxyRoot.add(%this.standaloneFolder);
+
+ %this.dirtySet = new SimSet();
+ %this.doomedFileCount = 0;
+
+ // Filled on first use and invalidated by a bake; see getFontFaceList.
+ %this.fontFaceList = "";
+}
+
+function GuiProfileEditorLibrary::onRemove(%this)
+{
+ if(isObject(%this.dirtySet))
+ {
+ %this.dirtySet.delete();
+ }
+ if(isObject(%this.proxyRoot))
+ {
+ %this.proxyRoot.delete();
+ }
+ if(isObject(%this.themeGroup))
+ {
+ // What this library read, it owns. A theme goes down with its members,
+ // but a bundle is a set and does not own the profile it names, so each
+ // root is taken down through deleteRoot rather than left to the group.
+ for(%i = %this.themeGroup.getCount() - 1; %i >= 0; %i--)
+ {
+ %this.deleteRoot(%this.themeGroup.getObject(%i));
+ }
+ %this.themeGroup.delete();
+ }
+}
+
+function GuiProfileEditorLibrary::getThemesPath(%this)
+{
+ // An explicitly set project folder always wins.
+ if(ProjectManager.projectFolder !$= "")
+ {
+ %projectPath = pathConcat(getMainDotCsDir(), ProjectManager.getProjectFolder());
+ return pathConcat(%projectPath, "themes");
+ }
+
+ // Otherwise anchor on the loaded AppCore module, which sits at
+ // /AppCore/. This holds whether getModulePath returns
+ // a relative or an absolute path; ProjectManager's derivation caches a
+ // bogus value when the module path is relative or the module database
+ // state shifts, which sent themes to the wrong folder.
+ %appCore = ModuleDatabase.findModule("AppCore", 1);
+ if(isObject(%appCore))
+ {
+ %projectPath = filePath(filePath(%appCore.getModulePath()));
+ return pathConcat(getMainDotCsDir(), %projectPath, "themes");
+ }
+
+ %projectPath = pathConcat(getMainDotCsDir(), ProjectManager.getProjectFolder());
+ return pathConcat(%projectPath, "themes");
+}
+
+// The one folder a project keeps its font caches in. Predetermined rather than
+// chosen: a cache is keyed by face and size alone, so a second location can only
+// hold a duplicate of what the first one already has, and asking the developer
+// where to put it is a question with no useful answer.
+//
+// Every theme and standalone profile the editor creates is pointed here, which
+// is also what the game reads at runtime -- a member profile carries the path
+// stamped from its theme, and $GUI::fontCacheDirectory (set by AppCore) names
+// the same folder for anything that carries none.
+//
+// Not $GUI::fontCacheDirectory itself: while the editor is loaded, EditorCore
+// overrides that with its OWN font folder, so reading it here would send a
+// game's caches into editor/EditorCore/gui/fonts.
+function GuiProfileEditorLibrary::getFontsPath(%this)
+{
+ return pathConcat(%this.getThemesPath(), "fonts");
+}
+
+// The same path a theme file stores: relative to the game root, so a project
+// stays portable.
+function GuiProfileEditorLibrary::getRelativeFontsPath(%this)
+{
+ return makeRelativePath(%this.getFontsPath(), getMainDotCsDir());
+}
+
+//-----------------------------------------------------------------------------
+// Cursor art.
+//
+// Unlike fonts, which are keyed by face and size and so can share one folder,
+// every theme gets a folder of its own: two themes in a project may want
+// cursors that look nothing alike - a menu pointer and a combat reticle - and a
+// shared folder would mean one theme overwriting the other's files.
+//
+// The art is copied rather than referenced so a theme and its cursors travel
+// together, and it is copied from the stock grayscale set so a new theme's
+// cursors are tinted to its palette from the moment it exists.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorLibrary::getThemeCursorsPath(%this, %theme)
+{
+ if(!isObject(%theme) || %theme.getName() $= "")
+ {
+ return "";
+ }
+ return pathConcat(%this.getThemesPath(), "cursors", %theme.getName());
+}
+
+// The stock set to copy from: the project's own AppCore if it has one, and
+// otherwise the editor's, which is always loaded. The two hold the same seven
+// files under the same names.
+function GuiProfileEditorLibrary::getStockCursorsPath(%this)
+{
+ %appCore = ModuleDatabase.findModule("AppCore", 1);
+ if(isObject(%appCore))
+ {
+ %path = pathConcat(makeFullPath(%appCore.getModulePath(), getMainDotCsDir()), "gui/images/cursors");
+ if(isDirectory(%path))
+ {
+ return %path;
+ }
+ }
+
+ %editorCore = EditorManager.findModule("EditorCore", 1);
+ if(isObject(%editorCore))
+ {
+ return pathConcat(makeFullPath(%editorCore.getModulePath(), getMainDotCsDir()), "Themes/BaseTheme/images/cursors");
+ }
+
+ return "";
+}
+
+// Give %theme its own copy of the stock art and point it at the folder. Safe to
+// call on every load: pathCopy is asked not to overwrite, so art the user has
+// replaced or edited is never clobbered.
+function GuiProfileEditorLibrary::seedThemeCursors(%this, %theme)
+{
+ %target = %this.getThemeCursorsPath(%theme);
+ if(%target $= "")
+ {
+ return false;
+ }
+
+ %source = %this.getStockCursorsPath();
+ if(%source $= "" || !isDirectory(%source))
+ {
+ warn("GuiProfileEditorLibrary::seedThemeCursors: no stock cursor art to copy from.");
+ return false;
+ }
+
+ createPath(%target @ "/");
+
+ %categories = %theme.getCursorCategoryNames();
+ for(%i = 0; %i < getWordCount(%categories); %i++)
+ {
+ %file = %theme.getCursorStockFile(getWord(%categories, %i));
+ if(%file !$= "")
+ {
+ pathCopy(pathConcat(%source, %file), pathConcat(%target, %file));
+ }
+ }
+
+ // Assigning the folder restamps, which fills in the bitmap of any cursor
+ // that has none yet. One already pointing at art keeps it.
+ %directory = makeRelativePath(%target, getMainDotCsDir());
+ if(%theme.cursorDirectory !$= %directory)
+ {
+ %theme.cursorDirectory = %directory;
+ }
+
+ return true;
+}
+
+// Follow a theme rename with its art. The files are named for nothing but their
+// category, so this is a folder copy plus a rewrite of every member still
+// pointing into the old folder; art the user pointed somewhere else entirely is
+// left alone. The old folder is dropped on save, like any other doomed file.
+function GuiProfileEditorLibrary::moveThemeCursors(%this, %theme, %oldName)
+{
+ %target = %this.getThemeCursorsPath(%theme);
+ %source = pathConcat(%this.getThemesPath(), "cursors", %oldName);
+ if(%target $= "" || %oldName $= "" || %source $= %target || !isDirectory(%source))
+ {
+ // Nothing to move: seeding gives the renamed theme a folder of its own.
+ return %this.seedThemeCursors(%theme);
+ }
+
+ createPath(%target @ "/");
+ %newPrefix = makeRelativePath(%target, getMainDotCsDir());
+
+ %categories = %theme.getCursorCategoryNames();
+ for(%i = 0; %i < getWordCount(%categories); %i++)
+ {
+ %category = getWord(%categories, %i);
+
+ // The stock file for the category, whether or not anything points at it
+ // yet: a member with no art is filled from it at the next restamp, and
+ // that has to find it in the new folder.
+ %this.moveCursorFile(%theme.getCursorStockFile(%category), %source, %target);
+
+ // Then every member's own file. Driven by what the members actually
+ // name rather than by the stock list, because an extra's art is named
+ // after the member -- a rename that copied only the stock files left
+ // every extra pointing into the new folder at a file still sitting in
+ // the old one.
+ %members = %theme.getCursors(%category);
+ for(%m = 0; %m < getWordCount(%members); %m++)
+ {
+ %this.moveCursorArt(getWord(%members, %m), %source, %target, %newPrefix);
+ }
+ }
+
+ %theme.cursorDirectory = %newPrefix;
+ return true;
+}
+
+// Copy one file between the two folders and drop the original at the next save.
+// Silent about a file that is not there: the stock art of a category whose
+// member was pointed elsewhere never existed in the old folder either.
+function GuiProfileEditorLibrary::moveCursorFile(%this, %name, %source, %target)
+{
+ if(%name $= "")
+ {
+ return;
+ }
+
+ %from = pathConcat(%source, %name);
+ if(pathCopy(%from, pathConcat(%target, %name)))
+ {
+ %this.doomFile(%from);
+ }
+}
+
+// Follow one cursor's art into the new folder, if that is where it lives. Art
+// the user chose from somewhere else is left exactly where it is - it is not
+// the theme's to move.
+function GuiProfileEditorLibrary::moveCursorArt(%this, %cursor, %source, %target, %newPrefix)
+{
+ %current = %cursor.bitmapName;
+ if(%current $= "" || filePath(makeFullPath(%current, getMainDotCsDir())) !$= %source)
+ {
+ return;
+ }
+
+ %name = fileName(%current);
+ %this.moveCursorFile(%name, %source, %target);
+ %cursor.bitmapName = pathConcat(%newPrefix, %name);
+}
+
+// Point a loaded or new root at the project's font folder. A theme restamps its
+// members on assignment, so its profiles follow along (an overridden field is
+// left alone, as with any other stamp).
+function GuiProfileEditorLibrary::applyFontsPath(%this, %root)
+{
+ %dir = %this.getRelativeFontsPath();
+
+ if(%root.getClassName() $= "GuiProfileTheme")
+ {
+ if(%root.fontDirectory !$= %dir)
+ {
+ %root.fontDirectory = %dir;
+ }
+ return;
+ }
+
+ %profile = (%root.getClassName() $= "GuiControlProfile") ? %root : %this.bundleProfile(%root);
+ if(isObject(%profile) && %profile.fontDirectory !$= %dir)
+ {
+ %profile.fontDirectory = %dir;
+ }
+}
+
+// Every theme this editor knows about: the ones AppCore loaded at boot, which
+// stay in the Gui data group, and the ones this library read or created, which
+// live in its own group. A theme is in exactly one of the two, so the lists never
+// overlap.
+function GuiProfileEditorLibrary::getThemes(%this)
+{
+ %themes = %this.collectThemes(GuiDataGroup, "");
+ return %this.collectThemes(%this.themeGroup, %themes);
+}
+
+function GuiProfileEditorLibrary::collectThemes(%this, %group, %themes)
+{
+ if(!isObject(%group))
+ {
+ return %themes;
+ }
+
+ for(%i = 0; %i < %group.getCount(); %i++)
+ {
+ %object = %group.getObject(%i);
+ if(%object.getClassName() $= "GuiProfileTheme")
+ {
+ %themes = (%themes $= "") ? %object.getId() : (%themes SPC %object.getId());
+ }
+ }
+
+ return %themes;
+}
+
+// Find a profile by name among the ones this library knows about - the members
+// of every loaded theme and the stand-alone profiles.
+//
+// Needed because isObject/nameToID cannot always answer. The Gui Editor runs the
+// engine in editor mode, where SimObject::assignName stashes an object's name
+// instead of registering it, so that naming a control being edited does not
+// create a global. Anything created during an editor session is invisible to the
+// name dictionary until it has been saved and read back.
+function GuiProfileEditorLibrary::findProfileByName(%this, %name)
+{
+ if(%name $= "")
+ {
+ return 0;
+ }
+
+ %themes = %this.getThemes();
+ for(%i = 0; %i < getWordCount(%themes); %i++)
+ {
+ %theme = getWord(%themes, %i);
+ %categories = %theme.getCategoryNames();
+ for(%c = 0; %c < getWordCount(%categories); %c++)
+ {
+ %members = %theme.getProfiles(getWord(%categories, %c));
+ for(%m = 0; %m < getWordCount(%members); %m++)
+ {
+ %member = getWord(%members, %m);
+ if(%member.getName() $= %name)
+ {
+ return %member;
+ }
+ }
+ }
+ }
+
+ for(%i = 0; %i < %this.standaloneFolder.getCount(); %i++)
+ {
+ %profile = %this.standaloneFolder.getObject(%i).target;
+ if(isObject(%profile) && %profile.getName() $= %name)
+ {
+ return %profile;
+ }
+ }
+
+ return 0;
+}
+
+// The cursor counterpart of findProfileByName, and needed for the same reason:
+// a cursor made during an editor session may carry a name the Sim dictionary
+// never registered, and reading its slot as empty would let a re-theme quietly
+// overwrite a deliberate choice.
+function GuiProfileEditorLibrary::findCursorByName(%this, %name)
+{
+ if(%name $= "")
+ {
+ return 0;
+ }
+
+ %themes = %this.getThemes();
+ for(%i = 0; %i < getWordCount(%themes); %i++)
+ {
+ %theme = getWord(%themes, %i);
+ %categories = %theme.getCursorCategoryNames();
+ for(%c = 0; %c < getWordCount(%categories); %c++)
+ {
+ %members = %theme.getCursors(getWord(%categories, %c));
+ for(%m = 0; %m < getWordCount(%members); %m++)
+ {
+ %member = getWord(%members, %m);
+ if(%member.getName() $= %name)
+ {
+ return %member;
+ }
+ }
+ }
+ }
+
+ return 0;
+}
+
+// Is this one of the stand-alone profiles the editor manages? Asked before an
+// apply overwrites a slot: a stand-alone profile is the supported alternative to
+// theming and is left alone unless the user says otherwise. A script profile -
+// GuiDefaultProfile, one of AppCore's - is not one of these and gets no such
+// protection.
+function GuiProfileEditorLibrary::isStandaloneProfile(%this, %profile)
+{
+ for(%i = 0; %i < %this.standaloneFolder.getCount(); %i++)
+ {
+ if(%this.standaloneFolder.getObject(%i).target == %profile)
+ {
+ return true;
+ }
+ }
+ return false;
+}
+
+// The standalone profiles stamped for one category, as a space-separated list
+// of ids. Asked by the Gui Editor's properties pane, which offers a control's
+// profile slot the members of the slot's category and nothing else.
+//
+// %category is matched exactly, and "" is a real answer rather than a wildcard:
+// a standalone starts with no category (the profile form shows that as "Any")
+// and the pane treats those differently from a stamped one -- offering them
+// wherever a slot is already on show, but never letting one be the reason a
+// slot appears.
+function GuiProfileEditorLibrary::getStandaloneProfiles(%this, %category)
+{
+ %list = "";
+ for(%i = 0; %i < %this.standaloneFolder.getCount(); %i++)
+ {
+ %profile = %this.standaloneFolder.getObject(%i).target;
+ if(!isObject(%profile) || %profile.category !$= %category)
+ {
+ continue;
+ }
+ %list = (%list $= "") ? %profile.getId() : (%list SPC %profile.getId());
+ }
+ return %list;
+}
+
+// Load any theme files not already loaded. Safe to call on every dialog
+// open: files belonging to live objects are skipped.
+function GuiProfileEditorLibrary::scanThemes(%this)
+{
+ // AppCore reads the project's themes at boot, and the editor loads the
+ // project's AppCore - so by the time this runs the objects usually exist
+ // already. Take them over rather than reading their files a second time,
+ // which would produce a second theme colliding on every member name.
+ %this.adoptLoadedThemes();
+
+ %path = %this.getThemesPath();
+ createPath(%path @ "/");
+
+ %pattern = %path @ "/*.taml";
+ for(%file = findFirstFile(%pattern); %file !$= ""; %file = findNextFile(%pattern))
+ {
+ if(%this.loadedFile[%file])
+ {
+ continue;
+ }
+
+ %object = TAMLRead(%file);
+ if(!isObject(%object))
+ {
+ warn("GuiProfileEditorLibrary::scanThemes: could not read " @ %file);
+ continue;
+ }
+
+ %this.adoptFile(%object, %file);
+ }
+}
+
+// Take a theme or stand-alone profile just read from disk into the library:
+// hold it, remember where it came from, point it at the project's font folder
+// and build its tree proxies. Returns what the library now tracks - the bundle,
+// for a stand-alone profile - or 0 when the file held nothing usable, in which
+// case what was read has been deleted.
+//
+// Shared by the initial scan and by revert, which re-reads the same files.
+function GuiProfileEditorLibrary::adoptFile(%this, %object, %file)
+{
+ %class = %object.getClassName();
+
+ if(%class $= "GuiProfileTheme")
+ {
+ if(%object.getName() $= "")
+ {
+ warn("GuiProfileEditorLibrary::adoptFile: skipping " @ %file @ " - the theme has no name (possibly a name collision).");
+ %object.delete();
+ return 0;
+ }
+
+ %this.themeGroup.add(%object);
+ %this.rememberFile(%object, %file);
+ // A theme written before the font folder was fixed (or copied in from
+ // another project) names a folder that is no longer where this project
+ // keeps its caches. Repair it on load, but don't mark the theme dirty
+ // over it: the corrected path is written the next time it is saved.
+ %this.applyFontsPath(%object);
+ // Likewise the cursor art: a theme that arrived from another project, or
+ // one written before cursors existed, gets its own folder here rather
+ // than having none.
+ %this.seedThemeCursors(%object);
+ %this.addThemeProxies(%object);
+ return %object;
+ }
+
+ if(!%this.isBundle(%object) && %class !$= "GuiControlProfile")
+ {
+ warn("GuiProfileEditorLibrary::adoptFile: skipping " @ %file @ " - not a theme or profile.");
+ %object.delete();
+ return 0;
+ }
+
+ %bundle = %this.adoptStandalone(%object);
+ if(!isObject(%bundle))
+ {
+ warn("GuiProfileEditorLibrary::adoptFile: skipping " @ %file @ " - the bundle holds no profile.");
+ %object.delete();
+ return 0;
+ }
+
+ %this.themeGroup.add(%bundle);
+ %this.rememberFile(%bundle, %file);
+ %this.applyFontsPath(%bundle);
+ %this.addStandaloneProxy(%this.bundleProfile(%bundle), %bundle);
+ return %bundle;
+}
+
+function GuiProfileEditorLibrary::rememberFile(%this, %root, %file)
+{
+ %this.sourceFile[%root.getId()] = %file;
+ %this.loadedFile[%file] = true;
+}
+
+// Bring a stand-alone profile file into the bundle shape this library keeps:
+// a bare profile is wrapped, a SimSet bundle is already right, and a SimGroup
+// bundle - what versions before this wrote - is rebuilt.
+//
+// The rebuild matters: a SimGroup owns what it holds, so reading one took the
+// profile out of the Gui data group, and that group is the only place the
+// engine looks when it fills a control's Profile dropdown. Hand the children
+// back and rewrap them in a set, which serializes identically without claiming
+// them. The file itself is corrected the next time it is saved.
+function GuiProfileEditorLibrary::adoptStandalone(%this, %object)
+{
+ if(%object.getClassName() $= "GuiControlProfile")
+ {
+ return %this.wrapStandalone(%object);
+ }
+
+ if(!isObject(%this.bundleProfile(%object)))
+ {
+ return 0;
+ }
+
+ if(!%object.isMemberOfClass("SimGroup"))
+ {
+ return %object;
+ }
+
+ %bundle = %this.newBundle();
+ while(%object.getCount() > 0)
+ {
+ // Reparenting to the Gui data group is what removes the child from the
+ // old group, so the loop drains it; the set add that follows leaves it
+ // where it now belongs. Order is preserved, which the borders rely on.
+ %child = %object.getObject(0);
+ GuiDataGroup.add(%child);
+ %bundle.add(%child);
+ }
+ %object.delete();
+ return %bundle;
+}
+
+// Take over the themes already in memory: give each one a proxy and a source
+// file so the tree, dirty tracking and saving treat it like any other, and mark
+// its file loaded so the scan above skips it.
+//
+// They are deliberately left in the Gui data group rather than moved into
+// themeGroup: a theme AppCore loaded belongs to the running project, and this
+// library's onRemove deletes what themeGroup holds.
+function GuiProfileEditorLibrary::adoptLoadedThemes(%this)
+{
+ if(!isObject(GuiDataGroup))
+ {
+ return;
+ }
+
+ for(%i = 0; %i < GuiDataGroup.getCount(); %i++)
+ {
+ %theme = GuiDataGroup.getObject(%i);
+ if(%theme.getClassName() !$= "GuiProfileTheme" || isObject(%this.themeProxy[%theme.getId()]))
+ {
+ continue;
+ }
+
+ if(%theme.getName() $= "")
+ {
+ warn("GuiProfileEditorLibrary::adoptLoadedThemes: skipping an unnamed theme.");
+ continue;
+ }
+
+ // Where the file is, rather than where it was read from. Nothing records
+ // the latter on the theme: a dynamic field would be written into the theme
+ // file itself (an absolute path, on whichever machine last loaded it), and
+ // saving names the file after the theme anyway - so the two only differ for
+ // a file somebody renamed by hand.
+ %file = pathConcat(%this.getThemesPath(), %theme.getName() @ ".taml");
+
+ %this.sourceFile[%theme.getId()] = %file;
+ %this.loadedFile[%file] = true;
+ %this.applyFontsPath(%theme);
+ %this.seedThemeCursors(%theme);
+ %this.addThemeProxies(%theme);
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Proxy tree.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorLibrary::addThemeProxies(%this, %theme)
+{
+ %proxy = new SimGroup()
+ {
+ kind = "theme";
+ target = %theme;
+ baseLabel = %theme.getName();
+ treeLabel = %theme.getName();
+ };
+ %this.themeProxy[%theme.getId()] = %proxy;
+
+ %profileFolder = new SimGroup()
+ {
+ kind = "folder";
+ treeLabel = "Profiles";
+ };
+ %proxy.add(%profileFolder);
+
+ %categoryNames = %theme.getCategoryNames();
+ for(%i = 0; %i < getWordCount(%categoryNames); %i++)
+ {
+ %category = getWord(%categoryNames, %i);
+ %categoryProxy = new SimGroup()
+ {
+ kind = "category";
+ theme = %theme;
+ category = %category;
+ treeLabel = %category;
+ };
+ %this.categoryProxy[%theme.getId() @ "_" @ %category] = %categoryProxy;
+ %profileFolder.add(%categoryProxy);
+
+ // A loaded theme file can carry extra profiles in this category.
+ %profiles = %theme.getProfiles(%category);
+ for(%p = 1; %p < getWordCount(%profiles); %p++)
+ {
+ %this.addExtraProxy(%theme, %category, getWord(%profiles, %p));
+ }
+ }
+
+ %borderFolder = new SimGroup()
+ {
+ kind = "folder";
+ treeLabel = "Borders";
+ };
+ %proxy.add(%borderFolder);
+
+ %borderNames = %theme.getBorderCategoryNames();
+ for(%i = 0; %i < getWordCount(%borderNames); %i++)
+ {
+ %category = getWord(%borderNames, %i);
+ %borderProxy = new ScriptObject()
+ {
+ kind = "border";
+ theme = %theme;
+ category = %category;
+ treeLabel = %category;
+ };
+ %borderFolder.add(%borderProxy);
+ }
+
+ // Cursors follow the profile shape rather than the border one: a category
+ // row that holds the default member, with any extras beneath it. A theme
+ // offering two cursors for the same job is what makes the Gui Editor show a
+ // choice on a control's cursor slot.
+ %cursorFolder = new SimGroup()
+ {
+ kind = "folder";
+ treeLabel = "Cursors";
+ };
+ %proxy.add(%cursorFolder);
+
+ %cursorNames = %theme.getCursorCategoryNames();
+ for(%i = 0; %i < getWordCount(%cursorNames); %i++)
+ {
+ %category = getWord(%cursorNames, %i);
+ %cursorProxy = new SimGroup()
+ {
+ kind = "cursorCategory";
+ theme = %theme;
+ category = %category;
+ treeLabel = %category;
+ };
+ %this.cursorCategoryProxy[%theme.getId() @ "_" @ %category] = %cursorProxy;
+ %cursorFolder.add(%cursorProxy);
+
+ %cursors = %theme.getCursors(%category);
+ for(%c = 1; %c < getWordCount(%cursors); %c++)
+ {
+ %this.addCursorExtraProxy(%theme, %category, getWord(%cursors, %c));
+ }
+ }
+
+ %this.proxyRoot.add(%proxy);
+
+ // The Stand Alone folder always stays at the bottom of the tree.
+ %this.proxyRoot.pushToBack(%this.standaloneFolder);
+}
+
+function GuiProfileEditorLibrary::addExtraProxy(%this, %theme, %category, %profile)
+{
+ %categoryProxy = %this.categoryProxy[%theme.getId() @ "_" @ %category];
+ if(!isObject(%categoryProxy))
+ {
+ return;
+ }
+
+ %label = %profile.getName();
+ if(%label $= "")
+ {
+ %label = "(unnamed)";
+ }
+
+ %leaf = new ScriptObject()
+ {
+ kind = "extra";
+ theme = %theme;
+ target = %profile;
+ category = %category;
+ treeLabel = %label;
+ };
+ %this.extraProxy[%profile.getId()] = %leaf;
+ %categoryProxy.add(%leaf);
+}
+
+function GuiProfileEditorLibrary::addCursorExtraProxy(%this, %theme, %category, %cursor)
+{
+ %categoryProxy = %this.cursorCategoryProxy[%theme.getId() @ "_" @ %category];
+ if(!isObject(%categoryProxy))
+ {
+ return;
+ }
+
+ %label = %cursor.getName();
+ if(%label $= "")
+ {
+ %label = "(unnamed)";
+ }
+
+ %leaf = new ScriptObject()
+ {
+ kind = "cursorExtra";
+ theme = %theme;
+ target = %cursor;
+ category = %category;
+ treeLabel = %label;
+ };
+ %this.cursorExtraProxy[%cursor.getId()] = %leaf;
+ %categoryProxy.add(%leaf);
+}
+
+function GuiProfileEditorLibrary::addStandaloneProxy(%this, %profile, %bundle)
+{
+ %label = %profile.getName();
+ if(%label $= "")
+ {
+ %label = "(unnamed)";
+ }
+
+ %leaf = new ScriptObject()
+ {
+ kind = "standalone";
+ target = %profile;
+ root = %bundle;
+ baseLabel = %label;
+ treeLabel = %label;
+ };
+ %this.standaloneProxy[%bundle.getId()] = %leaf;
+ %this.standaloneFolder.add(%leaf);
+}
+
+// The GuiControlProfile inside a standalone bundle.
+function GuiProfileEditorLibrary::bundleProfile(%this, %bundle)
+{
+ for(%i = 0; %i < %bundle.getCount(); %i++)
+ {
+ %child = %bundle.getObject(%i);
+ if(%child.getClassName() $= "GuiControlProfile")
+ {
+ return %child;
+ }
+ }
+ return 0;
+}
+
+// Is this a stand-alone profile's save root? SimGroup derives from SimSet, so
+// this also answers yes for the bundles older versions wrote (which
+// adoptStandalone rebuilds on load).
+function GuiProfileEditorLibrary::isBundle(%this, %object)
+{
+ return isObject(%object) && %object.isMemberOfClass("SimSet");
+}
+
+// A stand-alone profile is saved with the custom borders it wears, so the file
+// needs one object naming them all. That object is a SimSet and not a SimGroup
+// on purpose: a group takes what it holds out of whatever group it was in, and
+// a profile belongs in the Gui data group - GuiControlProfile::onAdd puts it
+// there, and it is the only place the engine looks when it fills a control's
+// Profile dropdown. A set serializes its children exactly the same way and
+// leaves their membership alone, which is how a theme's members behave too.
+//
+// The cost is that a set does not own what it holds: deleteRoot deletes the
+// contents explicitly.
+function GuiProfileEditorLibrary::newBundle(%this)
+{
+ return new SimSet() { class = "GuiProfileBundle"; };
+}
+
+// Wrap a bare profile in a fresh bundle (used for creation and legacy loads).
+function GuiProfileEditorLibrary::wrapStandalone(%this, %profile)
+{
+ %bundle = %this.newBundle();
+ %bundle.add(%profile);
+ return %bundle;
+}
+
+// Destroy a save root and everything it stands for. A theme owns its members
+// and takes them with it; a bundle is a set, which by design does not, so its
+// profile and custom borders go explicitly.
+function GuiProfileEditorLibrary::deleteRoot(%this, %root)
+{
+ if(!isObject(%root))
+ {
+ return;
+ }
+
+ if(%this.isBundle(%root))
+ {
+ %root.deleteObjects();
+ }
+ %root.delete();
+}
+
+function GuiProfileEditorLibrary::getRootProxy(%this, %root)
+{
+ %proxy = %this.themeProxy[%root.getId()];
+ if(!isObject(%proxy))
+ {
+ %proxy = %this.standaloneProxy[%root.getId()];
+ }
+ return %proxy;
+}
+
+function GuiProfileEditorLibrary::removeProxiesFor(%this, %root)
+{
+ %proxy = %this.getRootProxy(%root);
+ if(isObject(%proxy))
+ {
+ %proxy.delete();
+ }
+ %this.themeProxy[%root.getId()] = "";
+ %this.standaloneProxy[%root.getId()] = "";
+}
+
+//-----------------------------------------------------------------------------
+// Dirty tracking.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorLibrary::markDirty(%this, %root)
+{
+ if(!isObject(%root))
+ {
+ return;
+ }
+
+ if(!%this.dirtySet.isMember(%root))
+ {
+ %this.dirtySet.add(%root);
+ }
+
+ %proxy = %this.getRootProxy(%root);
+ if(isObject(%proxy) && !%proxy.isDirtyMarked)
+ {
+ %proxy.isDirtyMarked = true;
+ %proxy.treeLabel = %proxy.baseLabel @ " *";
+ if(isObject(%this.dialog))
+ {
+ %this.dialog.tree.refresh();
+ }
+ }
+}
+
+function GuiProfileEditorLibrary::unmarkDirty(%this, %root)
+{
+ %proxy = %this.getRootProxy(%root);
+ if(isObject(%proxy) && %proxy.isDirtyMarked)
+ {
+ %proxy.isDirtyMarked = false;
+ %proxy.treeLabel = %proxy.baseLabel;
+ }
+}
+
+function GuiProfileEditorLibrary::isDirty(%this)
+{
+ return (%this.dirtySet.getCount() > 0 || %this.doomedFileCount > 0);
+}
+
+//-----------------------------------------------------------------------------
+// Save and revert.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorLibrary::saveAll(%this)
+{
+ %path = %this.getThemesPath();
+ createPath(%path @ "/");
+
+ while(%this.dirtySet.getCount() > 0)
+ {
+ %root = %this.dirtySet.getObject(0);
+ %this.dirtySet.remove(%root);
+
+ %file = %this.sourceFile[%root.getId()];
+ if(%file $= "")
+ {
+ %rootName = %this.rootName(%root);
+ if(%rootName $= "")
+ {
+ warn("GuiProfileEditorLibrary::saveAll: cannot save an unnamed theme or profile - name it first.");
+ continue;
+ }
+ %file = pathConcat(%path, %rootName @ ".taml");
+ }
+
+ TAMLWrite(%root, %file);
+ %this.sourceFile[%root.getId()] = %file;
+ %this.loadedFile[%file] = true;
+ %this.unmarkDirty(%root);
+
+ // Bake the font caches the saved theme needs. This is the one place a
+ // pause is acceptable, and it is why nothing bakes while editing.
+ %this.bakeFontsFor(%root);
+ }
+
+ for(%i = 0; %i < %this.doomedFileCount; %i++)
+ {
+ if(isFile(%this.doomedFile[%i]))
+ {
+ fileDelete(%this.doomedFile[%i]);
+ }
+ }
+ %this.doomedFileCount = 0;
+}
+
+// Discard the session's edits: every dirty root is deleted and, when it has
+// a source file, re-read from it. Doomed files are simply forgotten (they
+// were never deleted from disk), so a deleted theme returns at the next
+// scan.
+function GuiProfileEditorLibrary::revertAll(%this)
+{
+ while(%this.dirtySet.getCount() > 0)
+ {
+ %root = %this.dirtySet.getObject(0);
+ %this.dirtySet.remove(%root);
+
+ %file = %this.sourceFile[%root.getId()];
+ %this.removeProxiesFor(%root);
+ %this.releaseRoot(%root);
+ %this.deleteRoot(%root);
+
+ if(%file !$= "" && isFile(%file))
+ {
+ %object = TAMLRead(%file);
+ if(isObject(%object) && isObject(%this.adoptFile(%object, %file)))
+ {
+ continue;
+ }
+
+ warn("GuiProfileEditorLibrary::revertAll: could not re-read " @ %file);
+ %this.loadedFile[%file] = "";
+ }
+ else if(%file !$= "")
+ {
+ %this.loadedFile[%file] = "";
+ }
+ }
+
+ %this.doomedFileCount = 0;
+
+ // The re-read themes are new objects; the Gui that was wearing the old ones
+ // was left on GuiDefaultProfile by releaseRoot and has to be put back.
+ if(isObject(%this.owner))
+ {
+ %this.owner.reattachTheme();
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Freeing a theme or profile the Gui under edit may be wearing.
+//
+// A control's profile field is a raw pointer that nothing updates when the
+// profile goes away, so anything about to delete one tells the owner first and
+// lets it move the affected controls onto GuiDefaultProfile.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorLibrary::releaseRoot(%this, %root)
+{
+ if(!isObject(%this.owner))
+ {
+ return;
+ }
+
+ if(%root.getClassName() $= "GuiProfileTheme")
+ {
+ %this.owner.detachTheme(%root, 0);
+ }
+ else
+ {
+ %this.owner.detachTheme(0, %this.bundleProfile(%root));
+ }
+}
+
+function GuiProfileEditorLibrary::releaseProfile(%this, %profile)
+{
+ if(isObject(%this.owner))
+ {
+ %this.owner.detachTheme(0, %profile);
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Naming.
+//
+// A profile has to have a real, registered name: a Gui refers to its profile by
+// name, and that is what TAML writes on both sides. But the Gui Editor runs the
+// engine in editor mode, where SimObject::assignName stashes the name on the
+// object instead of registering it - which is right for a control being edited
+// (naming one should not create a global) and wrong for everything this library
+// makes. Step out of editor mode while naming, and put it back.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorLibrary::beginNaming(%this)
+{
+ %this.namingLeftEditorMode = isEditorMode();
+ if(%this.namingLeftEditorMode)
+ {
+ editorMode(false);
+ }
+}
+
+function GuiProfileEditorLibrary::endNaming(%this)
+{
+ if(%this.namingLeftEditorMode)
+ {
+ editorMode(true);
+ }
+ %this.namingLeftEditorMode = false;
+}
+
+//-----------------------------------------------------------------------------
+// Operations.
+//-----------------------------------------------------------------------------
+
+// Mark a file for deletion at the next save. Nothing is removed from disk until
+// then, so Cancel keeps it.
+function GuiProfileEditorLibrary::doomFile(%this, %file)
+{
+ if(%file $= "")
+ {
+ return;
+ }
+ %this.doomedFile[%this.doomedFileCount] = %file;
+ %this.doomedFileCount++;
+}
+
+function GuiProfileEditorLibrary::doomSourceFile(%this, %root)
+{
+ %file = %this.sourceFile[%root.getId()];
+ if(%file !$= "")
+ {
+ %this.doomFile(%file);
+ %this.sourceFile[%root.getId()] = "";
+ %this.loadedFile[%file] = "";
+ }
+}
+
+function GuiProfileEditorLibrary::createTheme(%this, %name)
+{
+ if(%name $= "" || isObject(%name))
+ {
+ warn("GuiProfileEditorLibrary::createTheme: the name '" @ %name @ "' is empty or already taken.");
+ return 0;
+ }
+
+ %this.beginNaming();
+ %theme = new GuiProfileTheme();
+ %this.themeGroup.add(%theme);
+ %named = %theme.renameTheme(%name);
+ %this.endNaming();
+
+ if(!%named)
+ {
+ %theme.delete();
+ return 0;
+ }
+
+ // Friendlier starting point than the C++ ctor defaults: borders on so the
+ // recipes' bevels and rims actually show (a theme looks intentional out of
+ // the box) and a readable 16px base font. Each assignment restamps, fine for
+ // a fresh theme.
+ %theme.borderSize = 1;
+ %theme.fontSize = 16;
+ %this.applyFontsPath(%theme);
+ %this.seedThemeCursors(%theme);
+
+ %this.sourceFile[%theme.getId()] = "";
+ %this.addThemeProxies(%theme);
+ %this.markDirty(%theme);
+ return %theme;
+}
+
+function GuiProfileEditorLibrary::deleteTheme(%this, %theme)
+{
+ // The file is only removed on save; cancel keeps it (and the next scan
+ // reloads it).
+ %this.doomSourceFile(%theme);
+
+ %this.dirtySet.removeIfMember(%theme);
+ %this.removeProxiesFor(%theme);
+ %this.releaseRoot(%theme);
+ %this.deleteRoot(%theme);
+}
+
+function GuiProfileEditorLibrary::renameThemeTo(%this, %theme, %name)
+{
+ %oldName = %theme.getName();
+
+ %this.beginNaming();
+ %renamed = %theme.renameTheme(%name);
+ %this.endNaming();
+
+ if(!%renamed)
+ {
+ return false;
+ }
+
+ // The art folder is named for the theme, so it follows the rename. Members
+ // still pointing into the old folder are repointed; art the user chose from
+ // somewhere else is left where it is.
+ %this.moveThemeCursors(%theme, %oldName);
+
+ // The old file no longer matches the theme; replace it on save.
+ %this.doomSourceFile(%theme);
+
+ %proxy = %this.themeProxy[%theme.getId()];
+ if(isObject(%proxy))
+ {
+ %proxy.baseLabel = %name;
+ %proxy.isDirtyMarked = false;
+ }
+ %this.markDirty(%theme);
+
+ // Member names changed too, so refresh every extra label in this theme.
+ %categoryNames = %theme.getCategoryNames();
+ for(%i = 0; %i < getWordCount(%categoryNames); %i++)
+ {
+ %profiles = %theme.getProfiles(getWord(%categoryNames, %i));
+ for(%p = 1; %p < getWordCount(%profiles); %p++)
+ {
+ %profile = getWord(%profiles, %p);
+ %leaf = %this.extraProxy[%profile.getId()];
+ if(isObject(%leaf))
+ {
+ %leaf.treeLabel = %profile.getName();
+ }
+ }
+ }
+
+ %cursorNames = %theme.getCursorCategoryNames();
+ for(%i = 0; %i < getWordCount(%cursorNames); %i++)
+ {
+ %cursors = %theme.getCursors(getWord(%cursorNames, %i));
+ for(%c = 1; %c < getWordCount(%cursors); %c++)
+ {
+ %cursor = getWord(%cursors, %c);
+ %leaf = %this.cursorExtraProxy[%cursor.getId()];
+ if(isObject(%leaf))
+ {
+ %leaf.treeLabel = %cursor.getName();
+ }
+ }
+ }
+
+ return true;
+}
+
+function GuiProfileEditorLibrary::createExtraProfile(%this, %theme, %category)
+{
+ %this.beginNaming();
+ %profile = %theme.createProfile(%category);
+ %this.endNaming();
+
+ if(!isObject(%profile))
+ {
+ return 0;
+ }
+
+ %this.addExtraProxy(%theme, %category, %profile);
+ %this.markDirty(%theme);
+ return %profile;
+}
+
+// A second (or third) cursor in a category, with art of its own so editing it
+// cannot change what the category's default looks like. The copy is named for
+// the member, which is what keeps two extras in one category apart.
+function GuiProfileEditorLibrary::createExtraCursor(%this, %theme, %category)
+{
+ %this.beginNaming();
+ %cursor = %theme.createCursor(%category);
+ %this.endNaming();
+
+ if(!isObject(%cursor))
+ {
+ return 0;
+ }
+
+ %folder = %this.getThemeCursorsPath(%theme);
+ %stock = %theme.getCursorStockFile(%category);
+ if(%folder !$= "" && %stock !$= "")
+ {
+ %file = pathConcat(%folder, %cursor.getName() @ fileExt(%stock));
+ if(pathCopy(pathConcat(%folder, %stock), %file))
+ {
+ %cursor.bitmapName = makeRelativePath(%file, getMainDotCsDir());
+ }
+ }
+
+ %this.addCursorExtraProxy(%theme, %category, %cursor);
+ %this.markDirty(%theme);
+ return %cursor;
+}
+
+// Removes the cursor and answers with the art file it leaves behind, or "" if
+// there is nothing worth asking about. The file is NOT deleted here: the art
+// may be the user's own, it may still be in use, and either way throwing away
+// a picture is not something to do as a side effect of removing the thing that
+// happened to be pointing at it. The caller asks; deleteCursorArt does it.
+function GuiProfileEditorLibrary::removeExtraCursor(%this, %theme, %cursor)
+{
+ %leaf = %this.cursorExtraProxy[%cursor.getId()];
+ %bitmap = %cursor.bitmapName;
+
+ %removed = %theme.removeCursor(%cursor);
+ if(!%removed)
+ {
+ return "";
+ }
+
+ if(isObject(%leaf))
+ {
+ %leaf.delete();
+ }
+ %this.cursorExtraProxy[%cursor.getId()] = "";
+ %this.markDirty(%theme);
+
+ return %this.orphanedCursorArt(%theme, %bitmap);
+}
+
+// Is this art now unused, ours to remove, and actually there? Only then is
+// there a question to ask. Called after the cursor is gone, so "unused" is a
+// plain search of what remains.
+//
+// Three ways to answer no, and each is a file that must survive:
+// - a path outside the theme's own cursor folder is the user's own picture,
+// chosen with the Find button; this editor did not put it there.
+// - a path another cursor still names is in use, and the obvious case is an
+// extra pointed at the category's stock art, which the default member uses.
+// - a path with no file behind it has nothing to delete.
+function GuiProfileEditorLibrary::orphanedCursorArt(%this, %theme, %bitmap)
+{
+ if(%bitmap $= "")
+ {
+ return "";
+ }
+
+ %full = makeFullPath(%bitmap, getMainDotCsDir());
+ %folder = %this.getThemeCursorsPath(%theme);
+ if(%folder $= "" || filePath(%full) !$= %folder)
+ {
+ return "";
+ }
+
+ if(%this.isCursorArtInUse(%bitmap) || !isFile(%full))
+ {
+ return "";
+ }
+
+ return %full;
+}
+
+// Does any cursor in any loaded theme still name this art? Compared as the
+// relative paths a theme stores, which is the one form they all agree on.
+function GuiProfileEditorLibrary::isCursorArtInUse(%this, %bitmap)
+{
+ %themes = %this.getThemes();
+ for(%i = 0; %i < getWordCount(%themes); %i++)
+ {
+ %theme = getWord(%themes, %i);
+ %categories = %theme.getCursorCategoryNames();
+ for(%c = 0; %c < getWordCount(%categories); %c++)
+ {
+ %members = %theme.getCursors(getWord(%categories, %c));
+ for(%m = 0; %m < getWordCount(%members); %m++)
+ {
+ if(getWord(%members, %m).bitmapName $= %bitmap)
+ {
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+}
+
+// Drop the file at the next save, like every other file this editor removes -
+// so Cancel keeps it, exactly as Cancel keeps a deleted theme.
+function GuiProfileEditorLibrary::deleteCursorArt(%this, %file)
+{
+ %this.doomFile(%file);
+}
+
+function GuiProfileEditorLibrary::removeExtraProfile(%this, %theme, %profile)
+{
+ // Only an extra has a leaf, and only an extra can be removed - so this is also
+ // the test for "is about to be deleted", which is when the Gui has to let go
+ // of it.
+ %leaf = %this.extraProxy[%profile.getId()];
+ if(isObject(%leaf))
+ {
+ %this.releaseProfile(%profile);
+ }
+
+ %removed = %theme.removeProfile(%profile);
+ if(!%removed)
+ {
+ return false;
+ }
+
+ if(isObject(%leaf))
+ {
+ %leaf.delete();
+ }
+ %this.extraProxy[%profile.getId()] = "";
+ %this.markDirty(%theme);
+ return true;
+}
+
+// The filename stem for a save root: a theme/profile by its own name, a
+// standalone bundle by its inner profile's name.
+function GuiProfileEditorLibrary::rootName(%this, %root)
+{
+ if(%this.isBundle(%root))
+ {
+ %profile = %this.bundleProfile(%root);
+ return isObject(%profile) ? %profile.getName() : "";
+ }
+ return %root.getName();
+}
+
+function GuiProfileEditorLibrary::createStandalone(%this, %name)
+{
+ if(%name $= "" || isObject(%name))
+ {
+ warn("GuiProfileEditorLibrary::createStandalone: the name '" @ %name @ "' is empty or already taken.");
+ return 0;
+ }
+
+ %this.beginNaming();
+ %profile = new GuiControlProfile(%name);
+ %this.endNaming();
+
+ // Without this the profile carries no font directory, and the first control to
+ // wear it takes $GUI::fontCacheDirectory instead -- the EDITOR's font folder
+ // while the editor is loaded.
+ %this.applyFontsPath(%profile);
+ %bundle = %this.wrapStandalone(%profile);
+ %this.themeGroup.add(%bundle);
+ %this.sourceFile[%bundle.getId()] = "";
+ %this.addStandaloneProxy(%profile, %bundle);
+ %this.markDirty(%bundle);
+ return %profile;
+}
+
+// Delete a stand-alone profile, taking the custom borders it owns with it. As
+// with a theme, the file is only removed on save: cancel keeps it, and the next
+// scan reads it back.
+//
+// Nothing can list the Guis that wear the profile - a saved .gui names it and
+// nothing indexes that - so those are the user's to repair, and a control that
+// asks for a profile no longer there gets GuiDefaultProfile. The document open
+// in the editor is the one case that can be handled, and releaseRoot handles it.
+function GuiProfileEditorLibrary::deleteStandalone(%this, %bundle)
+{
+ %this.doomSourceFile(%bundle);
+
+ %this.dirtySet.removeIfMember(%bundle);
+ %this.removeProxiesFor(%bundle);
+ %this.releaseRoot(%bundle);
+ %this.deleteRoot(%bundle);
+}
+
+// Rename a stand-alone profile. The counterpart to renameThemeTo, and it costs
+// the same thing: a Gui saved earlier names the profile its controls wear, so
+// renaming one leaves those controls looking for a profile that is no longer
+// there. The controls in memory hold the object itself and are unaffected.
+function GuiProfileEditorLibrary::renameStandaloneTo(%this, %bundle, %name)
+{
+ %profile = %this.bundleProfile(%bundle);
+ if(!isObject(%profile))
+ {
+ return false;
+ }
+
+ // Confirming the name dialog without editing it is not an error.
+ if(%profile.getName() $= %name)
+ {
+ return true;
+ }
+
+ if(%name $= "" || isObject(%name))
+ {
+ warn("GuiProfileEditorLibrary::renameStandaloneTo: the name '" @ %name @ "' is empty or already taken.");
+ return false;
+ }
+
+ %this.beginNaming();
+ %profile.setName(%name);
+ %this.endNaming();
+
+ // assignName refuses a name the dictionary already holds and says so on the
+ // console rather than reporting back, so the only way to know is to look.
+ if(%profile.getName() !$= %name)
+ {
+ warn("GuiProfileEditorLibrary::renameStandaloneTo: the engine refused the name '" @ %name @ "'.");
+ return false;
+ }
+
+ // A stand-alone profile's file is named after it, so the old one no longer
+ // matches; replace it on save.
+ %this.doomSourceFile(%bundle);
+
+ %proxy = %this.standaloneProxy[%bundle.getId()];
+ if(isObject(%proxy))
+ {
+ %proxy.baseLabel = %name;
+ %proxy.isDirtyMarked = false;
+ }
+ %this.markDirty(%bundle);
+
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+// Font discovery and cache baking. A theme and an individual profile both name
+// a face, so the enumeration and baking live here on the shared library rather
+// than on either form -- they are reachable from both panes via dialog.library.
+//-----------------------------------------------------------------------------
+
+// The faces to offer: everything installed on this machine, plus anything the
+// project's font folder already holds a cache for, plus whatever the field is
+// set to now. The installed fonts are the point -- a developer styles a theme
+// with a font they have, and Save bakes a cache so it renders on a machine that
+// doesn't. The cached faces matter for the other direction: a theme that arrived
+// with its caches names a face nobody here has installed, and it must stay
+// choosable rather than vanish from the list.
+//
+// Cached on the library because a pane repopulates on every tree selection while
+// this list changes only when a bake writes new files. Enumerating a few hundred
+// families and a directory listing per click is a cost with nothing to show for
+// it. Call invalidateFontFaceList after anything that adds a cache file.
+function GuiProfileEditorLibrary::getFontFaceList(%this, %current)
+{
+ if(%this.fontFaceList $= "")
+ {
+ %this.fontFaceList = %this.sortTabList(
+ %this.mergeTabLists(getInstalledFonts(), %this.enumerateFonts(%this.getFontsPath())));
+ }
+
+ // The current value is merged per call rather than cached: it is the one part
+ // of the list that differs between the profile being edited and the next one.
+ if(%current !$= "" && !%this.tabListContains(%this.fontFaceList, %current))
+ {
+ return %this.sortTabList(%this.fontFaceList TAB %current);
+ }
+
+ return %this.fontFaceList;
+}
+
+function GuiProfileEditorLibrary::invalidateFontFaceList(%this)
+{
+ %this.fontFaceList = "";
+}
+
+// Append the second list's entries that the first one doesn't already have.
+function GuiProfileEditorLibrary::mergeTabLists(%this, %list, %additions)
+{
+ %n = getFieldCount(%additions);
+ for(%i = 0; %i < %n; %i++)
+ {
+ %value = getField(%additions, %i);
+ if(%value $= "" || %this.tabListContains(%list, %value))
+ {
+ continue;
+ }
+ %list = (%list $= "") ? %value : (%list TAB %value);
+ }
+ return %list;
+}
+
+// Returns a tab-separated, sorted, de-duplicated list of face names found in
+// the directory (both source .ttf/.otf faces and baked .uft/.fnt caches).
+function GuiProfileEditorLibrary::enumerateFonts(%this, %dir)
+{
+ if(%dir $= "")
+ {
+ return "";
+ }
+ %path = makeFullPath(%dir, getMainDotCsDir());
+ if(!isDirectory(%path))
+ {
+ return "";
+ }
+
+ %files = getFileList(%path);
+ %out = "";
+ %n = getFieldCount(%files);
+ for(%i = 0; %i < %n; %i++)
+ {
+ %file = getField(%files, %i);
+ if(%file $= "")
+ {
+ continue;
+ }
+
+ %ext = strlwr(fileExt(%file));
+ if(getSubStr(%ext, 0, 1) $= ".")
+ {
+ %ext = getSubStr(%ext, 1, strlen(%ext) - 1);
+ }
+
+ %face = "";
+ if(%ext $= "ttf" || %ext $= "otf")
+ {
+ %face = fileBase(fileName(%file));
+ }
+ else if(%ext $= "uft" || %ext $= "fnt")
+ {
+ %face = %this.faceFromCacheName(fileBase(fileName(%file)));
+ }
+ else
+ {
+ continue;
+ }
+
+ if(%face $= "" || %this.tabListContains(%out, %face))
+ {
+ continue;
+ }
+ %out = (%out $= "") ? %face : (%out TAB %face);
+ }
+
+ return %this.sortTabList(%out);
+}
+
+// A baked cache is named " ().uft"; recover the face.
+function GuiProfileEditorLibrary::faceFromCacheName(%this, %base)
+{
+ %paren = strpos(%base, " (");
+ if(%paren >= 0)
+ {
+ %base = getSubStr(%base, 0, %paren);
+ }
+ %wc = getWordCount(%base);
+ if(%wc >= 2)
+ {
+ %last = getWord(%base, %wc - 1);
+ if(%last $= (%last + 0))
+ {
+ %base = getWords(%base, 0, %wc - 2);
+ }
+ }
+ return trim(%base);
+}
+
+// Case-insensitively, matching how sortTabList orders. A face is the same face
+// however it was capitalized: a cache file named "arial 12 (ansi).uft" and the
+// installed "Arial" are one entry in a font list, not two, and the lookup finds
+// the file either way.
+function GuiProfileEditorLibrary::tabListContains(%this, %list, %value)
+{
+ %n = getFieldCount(%list);
+ for(%i = 0; %i < %n; %i++)
+ {
+ if(stricmp(getField(%list, %i), %value) == 0)
+ {
+ return true;
+ }
+ }
+ return false;
+}
+
+function GuiProfileEditorLibrary::sortTabList(%this, %list)
+{
+ %n = getFieldCount(%list);
+ if(%n < 2)
+ {
+ return %list;
+ }
+ for(%i = 0; %i < %n; %i++)
+ {
+ %arr[%i] = getField(%list, %i);
+ }
+ for(%i = 1; %i < %n; %i++)
+ {
+ %key = %arr[%i];
+ %j = %i - 1;
+ while(%j >= 0 && stricmp(%arr[%j], %key) > 0)
+ {
+ %arr[%j + 1] = %arr[%j];
+ %j--;
+ }
+ %arr[%j + 1] = %key;
+ }
+ %out = %arr[0];
+ for(%i = 1; %i < %n; %i++)
+ {
+ %out = %out TAB %arr[%i];
+ }
+ return %out;
+}
+
+// Fill a drop-down with the faces, selecting %selected. The current selection is
+// never lost, even when the directory did not list it.
+function GuiProfileEditorLibrary::fillFontDropdown(%this, %drop, %faces, %selected)
+{
+ %drop.clearItems();
+
+ %selIndex = -1;
+ %count = 0;
+ %n = getFieldCount(%faces);
+ for(%i = 0; %i < %n; %i++)
+ {
+ %face = getField(%faces, %i);
+ if(%face $= "")
+ {
+ continue;
+ }
+ %drop.addItem(%face);
+ if(%face $= %selected)
+ {
+ %selIndex = %count;
+ }
+ %count++;
+ }
+
+ if(%selIndex < 0 && %selected !$= "")
+ {
+ %drop.insertItem(0, %selected);
+ %selIndex = 0;
+ }
+ if(%selIndex >= 0)
+ {
+ %drop.setSelected(%selIndex);
+ }
+}
+
+// Best-effort bake of one font cache. Does nothing if the cache is already
+// there or the inputs are incomplete.
+//
+// This is deliberately NOT called while the user is editing. Nothing needs it
+// to be: GFont::create synthesizes the face from the platform font and
+// rasterizes glyphs on demand, so the preview draws a newly chosen face or size
+// without any cache at all. The cache only matters once the theme is saved and
+// has to render somewhere the platform font may not exist.
+//
+// Baking on every commit is also expensive enough to be felt. Measured cold on
+// a debug build: populating the whole BMP-0 range costs ~4.3 seconds (and warns
+// once per unmapped code point), where the printable Latin-1 range costs
+// nothing measurable; the old writeSingleFontCache then added a flat ~1.7
+// seconds because it scanned the project for every *.uft and rewrote each one
+// whose name contained the face. Hence the narrow range and the targeted
+// writeFontCache, which writes exactly this face and size.
+function GuiProfileEditorLibrary::bakeFont(%this, %face, %dir, %size)
+{
+ if(%face $= "" || %dir $= "" || %size <= 0)
+ {
+ return;
+ }
+
+ if(%this.cacheFileExists(%dir, %face, %size))
+ {
+ return;
+ }
+
+ %prev = $GUI::fontCacheDirectory;
+ $GUI::fontCacheDirectory = %dir;
+ // Printable Latin-1. Anything outside it still renders -- it just rasterizes
+ // on demand instead of coming from the cache.
+ populateFontCacheRange(%face, %size, 32, 255);
+ writeOneFontCache(%face, %size);
+ $GUI::fontCacheDirectory = %prev;
+}
+
+// Is this face and size already baked? Asked of the filesystem, not isFile:
+// isFile answers out of the resource manager, and GFont::create registers every
+// font it synthesizes under the .uft path it looked for and didn't find. So the
+// moment the editor renders a face, isFile claims its cache exists -- and the
+// bake this guards would skip every font the developer could see, which is all
+// of them.
+function GuiProfileEditorLibrary::cacheFileExists(%this, %dir, %face, %size)
+{
+ %path = makeFullPath(%dir, getMainDotCsDir());
+ if(!isDirectory(%path))
+ {
+ return false;
+ }
+ return %this.tabListContains(getFileList(%path), %face SPC %size SPC "(ansi).uft");
+}
+
+// Bake every face/size a root actually uses. Called from the dialog's Save,
+// where a short pause is expected, rather than from the edit path.
+function GuiProfileEditorLibrary::bakeFontsFor(%this, %root)
+{
+ if(!isObject(%root))
+ {
+ return;
+ }
+
+ if(%root.getClassName() $= "GuiProfileTheme")
+ {
+ // The theme's three faces at its base size, plus whatever its members
+ // ended up with -- a recipe may offset the size, and a member may have
+ // overridden the face outright.
+ %this.bakeFont(%root.fontTitle, %root.fontDirectory, %root.fontSize);
+ %this.bakeFont(%root.fontBody, %root.fontDirectory, %root.fontSize);
+ %this.bakeFont(%root.fontCode, %root.fontDirectory, %root.fontSize);
+
+ %names = %root.getCategoryNames();
+ for(%i = 0; %i < getWordCount(%names); %i++)
+ {
+ %profiles = %root.getProfiles(getWord(%names, %i));
+ for(%p = 0; %p < getWordCount(%profiles); %p++)
+ {
+ %this.bakeProfileFont(getWord(%profiles, %p));
+ }
+ }
+ }
+ else
+ {
+ // A standalone bundle: just the profile it wraps.
+ %this.bakeProfileFont(%this.bundleProfile(%root));
+ }
+
+ %this.bakeRequestedFonts();
+ %this.invalidateFontFaceList();
+}
+
+function GuiProfileEditorLibrary::bakeProfileFont(%this, %profile)
+{
+ if(isObject(%profile))
+ {
+ %this.bakeFont(%profile.fontType, %profile.fontDirectory, %profile.fontSize);
+ }
+}
+
+// Bake the sizes the walk above cannot see. A control's fontSizeAdjust multiplies
+// its profile's fontSize, so what actually got rendered is not what any field
+// says: a profile set to 16 worn by a control adjusting 1.2 asks GFont for 19,
+// and only a machine with the face installed would ever draw it. The engine
+// records each face/size it had to rasterize for want of a cache; this bakes the
+// ones belonging to this project's font folder and forgets the rest -- the editor
+// misses its own chrome fonts against its own folder, and those are not ours.
+function GuiProfileEditorLibrary::bakeRequestedFonts(%this)
+{
+ %dir = %this.getFontsPath();
+ %rows = getUncachedFonts();
+ %count = getRecordCount(%rows);
+
+ for(%i = 0; %i < %count; %i++)
+ {
+ %row = getRecord(%rows, %i);
+ %face = getField(%row, 0);
+ %size = getField(%row, 1);
+ %rowDir = getField(%row, 2);
+
+ if(%face $= "" || %size <= 0 || %rowDir $= "")
+ {
+ continue;
+ }
+
+ // Compared as full paths: a profile carries the folder relative to the game
+ // root, while getFontsPath is absolute.
+ if(makeFullPath(%rowDir, getMainDotCsDir()) !$= %dir)
+ {
+ continue;
+ }
+
+ %this.bakeFont(%face, %rowDir, %size);
+ }
+
+ clearUncachedFonts();
+}
diff --git a/editor/GuiEditor/scripts/GuiProfileEditorNameDialog.cs b/editor/GuiEditor/scripts/GuiProfileEditorNameDialog.cs
new file mode 100644
index 000000000..bb9bc7f3d
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiProfileEditorNameDialog.cs
@@ -0,0 +1,75 @@
+
+//-----------------------------------------------------------------------------
+// A small name prompt used by the Gui Profile Editor for new/rename
+// operations. The spawner sets dialogText, callbackTarget, callbackMethod,
+// and optionally defaultName; OK calls callbackTarget.callbackMethod(name).
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorNameDialog::init(%this, %width, %height)
+{
+ %window = %this.getObject(0);
+ %content = %window.getObject(0);
+
+ %form = new GuiGridCtrl()
+ {
+ class = "EditorForm";
+ extent = %width SPC %height;
+ cellSizeX = %width - 20;
+ cellSizeY = 50;
+ };
+ %form.addListener(%this);
+
+ %item = %form.addFormItem("Name", (%width - 20) SPC 30);
+ %this.nameBox = %form.createTextEditItem(%item);
+ %this.nameBox.text = %this.defaultName;
+ %this.nameBox.ReturnCommand = %this.getId() @ ".onDone();";
+ %this.nameBox.EscapeCommand = %this.getId() @ ".onClose();";
+ %content.add(%form);
+
+ %this.cancelButton = new GuiButtonCtrl()
+ {
+ HorizSizing = "right";
+ VertSizing = "bottom";
+ Position = (%width - 226) SPC (%height - 62);
+ Extent = "100 30";
+ Text = "Cancel";
+ Command = %this.getID() @ ".onClose();";
+ };
+ ThemeManager.setProfile(%this.cancelButton, "buttonProfile");
+ %content.add(%this.cancelButton);
+
+ %this.okButton = new GuiButtonCtrl()
+ {
+ HorizSizing = "right";
+ VertSizing = "bottom";
+ Position = (%width - 116) SPC (%height - 64);
+ Extent = "100 34";
+ Text = "OK";
+ Command = %this.getID() @ ".onDone();";
+ };
+ ThemeManager.setProfile(%this.okButton, "primaryButtonProfile");
+ %content.add(%this.okButton);
+}
+
+function GuiProfileEditorNameDialog::onDone(%this)
+{
+ %name = trim(%this.nameBox.getText());
+ if(%name $= "")
+ {
+ return;
+ }
+
+ %this.callbackTarget.call(%this.callbackMethod, %name);
+ %this.onClose();
+}
+
+// This dialog can sit on top of another EditorDialog, so it must not use the
+// shared EditorCore.dialog delete slot - closing both within the scheduled
+// delay would leak one of them. The parent object deletes it after a pause;
+// scheduling "delete" on the dialog itself would fire inside its own
+// script-callback guard and assert.
+function GuiProfileEditorNameDialog::onClose(%this)
+{
+ Canvas.popDialog(%this);
+ EditorCore.schedule(100, "deleteDialogObject", %this);
+}
diff --git a/editor/GuiEditor/scripts/GuiProfileEditorPreview.cs b/editor/GuiEditor/scripts/GuiProfileEditorPreview.cs
new file mode 100644
index 000000000..56324b78c
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiProfileEditorPreview.cs
@@ -0,0 +1,921 @@
+
+//-----------------------------------------------------------------------------
+// The live preview pane of the Gui Profile Editor. Shows a working sample
+// control for the selected profile category (or a border probe for border
+// categories) on a backdrop filled with the theme's background color, so
+// hovering and clicking exercises the highlight and selected states.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorPreview::onAdd(%this)
+{
+ // A scratch profile the preview owns. It is standalone, so editing it
+ // never touches the theme override machinery.
+ %this.borderProbeProfile = new GuiControlProfile();
+
+ // The same tiled grid the Gui Editor uses behind the edited gui.
+ %this.backdrop = new GuiSpriteCtrl()
+ {
+ HorizSizing = "width";
+ VertSizing = "height";
+ Position = "0 0";
+ Extent = %this.getExtent();
+ imageColor = "255 255 255 255";
+ Image = "EditorCore:editorGrid";
+ singleFrameBitmap = "1";
+ tileImage = "1";
+ positionOffset = "0 0";
+ imageSize = "128 128";
+ fullSize = "0";
+ constrainProportions = "1";
+ };
+ ThemeManager.setProfile(%this.backdrop, "emptyProfile");
+ %this.add(%this.backdrop);
+
+ // An invisible holder for the current samples, kept centered in the
+ // pane; layoutStage sizes it to the samples after each rebuild.
+ %this.stage = new GuiControl()
+ {
+ HorizSizing = "center";
+ VertSizing = "center";
+ Position = "0 0";
+ Extent = "200 200";
+ };
+ ThemeManager.setProfile(%this.stage, "emptyProfile");
+ %this.backdrop.add(%this.stage);
+
+ // A tiny object hierarchy for the TreeView sample to display.
+ %this.previewTreeGroup = new SimGroup();
+ %branch = new SimGroup();
+ %branch.add(new ScriptObject());
+ %branch.add(new ScriptObject());
+ %this.previewTreeGroup.add(%branch);
+ %this.previewTreeGroup.add(new ScriptObject());
+}
+
+function GuiProfileEditorPreview::onRemove(%this)
+{
+ // The sample controls must be gone before the scratch profile they may
+ // wear is deleted; the backdrop and stage die with the control tree.
+ %this.clearSamples();
+
+ if(isObject(%this.borderProbeProfile))
+ {
+ %this.borderProbeProfile.delete();
+ }
+ if(isObject(%this.previewTreeGroup))
+ {
+ %this.previewTreeGroup.delete();
+ }
+}
+
+function GuiProfileEditorPreview::clearSamples(%this)
+{
+ if(isObject(%this.stage))
+ {
+ %this.stage.deleteObjects();
+ }
+ %this.lastKind = "";
+ %this.lastTheme = "";
+ %this.lastCategory = "";
+ %this.lastMember = "";
+}
+
+// Re-run the last show after a field edit. Most changes render live anyway
+// since the samples reference the profile objects, but rebuilding also picks
+// up structural changes like border reassignments.
+function GuiProfileEditorPreview::refresh(%this)
+{
+ %kind = %this.lastKind;
+ %theme = %this.lastTheme;
+ %category = %this.lastCategory;
+ %member = %this.lastMember;
+
+ if(%kind $= "theme")
+ {
+ %this.showTheme(%theme);
+ }
+ else if(%kind $= "category")
+ {
+ %this.showCategory(%theme, %category, %member);
+ }
+ else if(%kind $= "border")
+ {
+ %this.showBorder(%theme, %member);
+ }
+ else if(%kind $= "cursor")
+ {
+ %this.showCursor(%theme, %member);
+ }
+}
+
+// Size the stage to the bounding box of the current samples and center it
+// in the pane, so every demonstration sits in the middle of the backdrop.
+function GuiProfileEditorPreview::layoutStage(%this)
+{
+ %width = 0;
+ %height = 0;
+ for(%i = 0; %i < %this.stage.getCount(); %i++)
+ {
+ %sample = %this.stage.getObject(%i);
+ %right = getWord(%sample.getPosition(), 0) + getWord(%sample.getExtent(), 0);
+ %bottom = getWord(%sample.getPosition(), 1) + getWord(%sample.getExtent(), 1);
+ if(%right > %width)
+ {
+ %width = %right;
+ }
+ if(%bottom > %height)
+ {
+ %height = %bottom;
+ }
+ }
+
+ %this.stage.setExtent(%width, %height);
+
+ %x = mFloor((getWord(%this.backdrop.getExtent(), 0) - %width) / 2);
+ %y = mFloor((getWord(%this.backdrop.getExtent(), 1) - %height) / 2);
+ %this.stage.setPosition(mGetMax(%x, 0), mGetMax(%y, 0));
+}
+
+// Point one of a sample's secondary slots at a theme category it is not the
+// subject of: the backdrop a dropdown pops over, the content a scroll bar
+// scrolls. A stand-alone profile has no theme and so no siblings to borrow,
+// and the slot is left as the control was born with rather than handed the
+// profile under edit -- which would put a scroll bar's skin on its own
+// contents and drown the thing being previewed.
+//
+// Must run before the sample is added to the stage, so the control reads the
+// slot when it wakes, exactly as it would from an inline field list.
+function GuiProfileEditorPreview::setThemeSlot(%this, %ctrl, %field, %theme, %category)
+{
+ if(!isObject(%theme))
+ {
+ return;
+ }
+
+ %profile = %theme.getProfile(%category);
+ if(isObject(%profile))
+ {
+ %ctrl.setFieldValue(%field, %profile);
+ }
+}
+
+// A profile slot for a sample: the selected member when it belongs to the
+// slot's category, otherwise the theme's default for that category. With no
+// theme -- a stand-alone profile -- every slot falls to the member, so the
+// sample shows the one profile there is in each part it draws.
+function GuiProfileEditorPreview::slotProfile(%this, %theme, %slotCategory, %selectedCategory, %member)
+{
+ if(%slotCategory $= %selectedCategory && isObject(%member))
+ {
+ return %member;
+ }
+ if(isObject(%theme))
+ {
+ %profile = %theme.getProfile(%slotCategory);
+ if(isObject(%profile))
+ {
+ return %profile;
+ }
+ }
+ return %member;
+}
+
+//-----------------------------------------------------------------------------
+// Show methods, one per tree selection kind.
+//-----------------------------------------------------------------------------
+
+// The theme root: load the saved sample gui (a real, hand-authored layout) and
+// re-skin every control with this theme's generated category profiles, so the
+// preview shows an actual dialog rather than a hand-built mock. The sample ships
+// wearing the AppCore default profiles; reskin swaps them for the theme's.
+function GuiProfileEditorPreview::showTheme(%this, %theme)
+{
+ %this.clearSamples();
+ if(!isObject(%theme))
+ {
+ return;
+ }
+ %this.lastKind = "theme";
+ %this.lastTheme = %theme;
+
+ // Absolute path from the repo root: a "./" path would resolve against this
+ // script's own directory (editor/GuiEditor/scripts), not the game root.
+ %file = makeFullPath("editor/GuiEditor/gui/theme_sample.gui.taml", getMainDotCsDir());
+ %sample = TamlRead(%file);
+ if(!isObject(%sample))
+ {
+ return;
+ }
+
+ // Anchor the sample top-left with a fixed size. Its saved sizing is
+ // "center", which would recenter it to negative coords in the (initially
+ // smaller) stage and make layoutStage compute a stage smaller than the
+ // sample -- clipping it.
+ %sample.HorizSizing = "right";
+ %sample.VertSizing = "bottom";
+ %sample.setPosition(0, 0);
+ %this.reskin(%sample, %theme);
+ %this.addSample(%sample);
+ %this.layoutStage();
+}
+
+// Walk the sample tree and point every profile slot at the theme's matching
+// category profile: the main Profile by control class, the secondary profiles
+// (window content/buttons, scroll parts, dropdown list/background) by slot.
+function GuiProfileEditorPreview::reskin(%this, %ctrl, %theme)
+{
+ if(!isObject(%ctrl))
+ {
+ return;
+ }
+
+ %category = %this.categoryForClass(%ctrl.getClassName());
+ if(%category !$= "" && isObject(%theme.getProfile(%category)))
+ {
+ %ctrl.setEditFieldValue("Profile", %theme.getProfile(%category));
+ }
+
+ %this.reskinSlot(%ctrl, %theme, "contentProfile", "WindowContent");
+ %this.reskinSlot(%ctrl, %theme, "closeButtonProfile", "WindowCloseButton");
+ %this.reskinSlot(%ctrl, %theme, "minButtonProfile", "WindowButton");
+ %this.reskinSlot(%ctrl, %theme, "maxButtonProfile", "WindowButton");
+ %this.reskinSlot(%ctrl, %theme, "thumbProfile", "ScrollThumb");
+ %this.reskinSlot(%ctrl, %theme, "trackProfile", "ScrollTrack");
+ %this.reskinSlot(%ctrl, %theme, "arrowProfile", "ScrollArrow");
+ %this.reskinSlot(%ctrl, %theme, "ScrollProfile", "Scroll");
+ %this.reskinSlot(%ctrl, %theme, "listBoxProfile", "DropDownItem");
+ %this.reskinSlot(%ctrl, %theme, "backgroundProfile", "Overlay");
+
+ // The four cursor slots the engine has, so hovering the sample's text box or
+ // a window edge shows this theme's own cursors rather than whichever set is
+ // installed globally.
+ %this.reskinCursorSlot(%ctrl, %theme, "editCursor", "Edit");
+ %this.reskinCursorSlot(%ctrl, %theme, "leftRightCursor", "LeftRight");
+ %this.reskinCursorSlot(%ctrl, %theme, "upDownCursor", "UpDown");
+ %this.reskinCursorSlot(%ctrl, %theme, "nWSECursor", "NWSE");
+
+ %this.fillSampleContent(%ctrl);
+
+ for(%i = 0; %i < %ctrl.getCount(); %i++)
+ {
+ %this.reskin(%ctrl.getObject(%i), %theme);
+ }
+}
+
+// Runtime-only content the saved TAML can't carry: dropdown items and a
+// progress value. Runs on every (re)load, which is a fresh sample, so items
+// never accumulate.
+function GuiProfileEditorPreview::fillSampleContent(%this, %ctrl)
+{
+ %class = %ctrl.getClassName();
+ if(%class $= "GuiDropDownCtrl")
+ {
+ %ctrl.clearItems();
+ %ctrl.addItem("Item1");
+ %ctrl.addItem("Item2");
+ %ctrl.addItem("Item3");
+ %ctrl.addItem("Item4");
+ %ctrl.setSelected(0);
+ }
+ else if(%class $= "GuiProgressCtrl")
+ {
+ %ctrl.setProgress(0.6);
+ }
+}
+
+// Point one secondary profile slot at a theme category, but only if the control
+// actually uses that slot (so we never graft stray fields onto controls).
+function GuiProfileEditorPreview::reskinSlot(%this, %ctrl, %theme, %field, %category)
+{
+ if(%ctrl.getFieldValue(%field) $= "")
+ {
+ return;
+ }
+ %profile = %theme.getProfile(%category);
+ if(isObject(%profile))
+ {
+ %ctrl.setEditFieldValue(%field, %profile);
+ }
+}
+
+// The cursor equivalent, and it cannot use the same test: an unset cursor field
+// reads back as "" exactly like a field the control does not have, and sample
+// controls never set one. So ask the type system whether the field exists
+// rather than asking whether it holds anything.
+function GuiProfileEditorPreview::reskinCursorSlot(%this, %ctrl, %theme, %field, %category)
+{
+ if(%ctrl.getFieldType(%field) !$= "GuiCursor")
+ {
+ return;
+ }
+ %cursor = %theme.getCursor(%category);
+ if(isObject(%cursor))
+ {
+ %ctrl.setEditFieldValue(%field, %cursor);
+ }
+}
+
+function GuiProfileEditorPreview::categoryForClass(%this, %class)
+{
+ switch$(%class)
+ {
+ case "GuiWindowCtrl": return "Window";
+ case "GuiButtonCtrl": return "Button";
+ case "GuiCheckBoxCtrl": return "CheckBox";
+ case "GuiRadioCtrl": return "Radio";
+ case "GuiProgressCtrl": return "Progress";
+ case "GuiScrollCtrl": return "Scroll";
+ case "GuiTextEditCtrl": return "TextEdit";
+ case "GuiDropDownCtrl": return "DropDown";
+ case "GuiListBoxCtrl": return "ListBox";
+ case "GuiTreeViewCtrl": return "TreeView";
+ case "GuiTabBookCtrl": return "TabBook";
+ case "GuiTabPageCtrl": return "TabPage";
+ case "GuiSliderCtrl": return "Slider";
+ case "GuiMenuBarCtrl": return "MenuBar";
+ case "GuiChainCtrl": return "Empty";
+ case "GuiControl": return "Label";
+ default: return "";
+ }
+}
+
+function GuiProfileEditorPreview::showCategory(%this, %theme, %category, %member)
+{
+ %this.clearSamples();
+ if(!isObject(%member))
+ {
+ return;
+ }
+ %this.lastKind = "category";
+ %this.lastTheme = %theme;
+ %this.lastCategory = %category;
+ %this.lastMember = %member;
+
+ if(%category $= "Button" || %category $= "CheckBox" || %category $= "Radio" || %category $= "TextEdit")
+ {
+ %this.addStateSamples(%category, %member);
+ }
+ else if(%category $= "Label")
+ {
+ %this.addSample(new GuiControl()
+ {
+ Position = "0 0";
+ Extent = "340 30";
+ Text = "The quick brown fox jumps over the lazy dog.";
+ Profile = %member;
+ });
+ }
+ else if(%category $= "Panel")
+ {
+ %this.addSample(new GuiPanelCtrl()
+ {
+ Position = "0 0";
+ Extent = "240 140";
+ Profile = %member;
+ });
+ }
+ else if(%category $= "Scroll" || %category $= "ScrollTrack" || %category $= "ScrollThumb" || %category $= "ScrollArrow")
+ {
+ %scroll = new GuiScrollCtrl()
+ {
+ Position = "0 0";
+ Extent = "240 160";
+ hScrollBar = "alwaysOff";
+ vScrollBar = "alwaysOn";
+ constantThumbHeight = "0";
+ showArrowButtons = "1";
+ scrollBarThickness = "14";
+ Profile = %this.slotProfile(%theme, "Scroll", %category, %member);
+ TrackProfile = %this.slotProfile(%theme, "ScrollTrack", %category, %member);
+ ThumbProfile = %this.slotProfile(%theme, "ScrollThumb", %category, %member);
+ ArrowProfile = %this.slotProfile(%theme, "ScrollArrow", %category, %member);
+ };
+ %this.addSample(%scroll);
+
+ %content = new GuiControl()
+ {
+ Position = "0 0";
+ Extent = "200 400";
+ };
+ %this.setThemeSlot(%content, "Profile", %theme, "Empty");
+ %scroll.add(%content);
+ }
+ else if(%category $= "TabBook" || %category $= "Tab" || %category $= "TabPage")
+ {
+ %book = new GuiTabBookCtrl()
+ {
+ Position = "0 0";
+ Extent = "260 160";
+ Profile = %this.slotProfile(%theme, "TabBook", %category, %member);
+ TabProfile = %this.slotProfile(%theme, "Tab", %category, %member);
+ };
+ %this.addSample(%book);
+ %book.add(new GuiTabPageCtrl()
+ {
+ Text = "One";
+ Profile = %this.slotProfile(%theme, "TabPage", %category, %member);
+ });
+ %book.add(new GuiTabPageCtrl()
+ {
+ Text = "Two";
+ Profile = %this.slotProfile(%theme, "TabPage", %category, %member);
+ });
+ }
+ else if(%category $= "ListBox")
+ {
+ %list = new GuiListBoxCtrl()
+ {
+ Position = "0 0";
+ Extent = "220 140";
+ Profile = %member;
+ };
+ %this.addSample(%list);
+ %list.addItem("First item");
+ %list.addItem("Second item");
+ %list.addItem("Third item");
+ %list.addItem("Fourth item");
+ }
+ else if(%category $= "DropDown" || %category $= "DropDownItem")
+ {
+ %dropDown = new GuiDropDownCtrl()
+ {
+ Position = "0 0";
+ Extent = "220 30";
+ Profile = %this.slotProfile(%theme, "DropDown", %category, %member);
+ listBoxProfile = %this.slotProfile(%theme, "DropDownItem", %category, %member);
+ };
+ %this.setThemeSlot(%dropDown, "backgroundProfile", %theme, "Overlay");
+ %this.setThemeSlot(%dropDown, "scrollProfile", %theme, "Scroll");
+ %this.setThemeSlot(%dropDown, "trackProfile", %theme, "ScrollTrack");
+ %this.setThemeSlot(%dropDown, "thumbProfile", %theme, "ScrollThumb");
+ %this.setThemeSlot(%dropDown, "arrowProfile", %theme, "ScrollArrow");
+ %this.addSample(%dropDown);
+ %dropDown.addItem("First choice");
+ %dropDown.addItem("Second choice");
+ %dropDown.addItem("Third choice");
+ %dropDown.setSelected(0);
+ }
+ else if(%category $= "Window" || %category $= "WindowContent" || %category $= "WindowButton" || %category $= "WindowCloseButton")
+ {
+ %this.addSample(new GuiWindowCtrl()
+ {
+ Position = "0 0";
+ Extent = "260 160";
+ Text = "Window";
+ canMove = false;
+ canClose = true;
+ canMinimize = true;
+ canMaximize = true;
+ titleHeight = 30;
+ Profile = %this.slotProfile(%theme, "Window", %category, %member);
+ contentProfile = %this.slotProfile(%theme, "WindowContent", %category, %member);
+ closeButtonProfile = %this.slotProfile(%theme, "WindowCloseButton", %category, %member);
+ minButtonProfile = %this.slotProfile(%theme, "WindowButton", %category, %member);
+ maxButtonProfile = %this.slotProfile(%theme, "WindowButton", %category, %member);
+ });
+ }
+ else if(%category $= "MenuBar" || %category $= "Menu" || %category $= "MenuItem" || %category $= "MenuContent")
+ {
+ %menuBar = new GuiMenuBarCtrl()
+ {
+ Position = "0 0";
+ Extent = "340 30";
+ Profile = %this.slotProfile(%theme, "MenuBar", %category, %member);
+ MenuProfile = %this.slotProfile(%theme, "Menu", %category, %member);
+ MenuItemProfile = %this.slotProfile(%theme, "MenuItem", %category, %member);
+ MenuContentProfile = %this.slotProfile(%theme, "MenuContent", %category, %member);
+
+ new GuiMenuItemCtrl()
+ {
+ Text = "File";
+
+ new GuiMenuItemCtrl() { Text = "New"; };
+ new GuiMenuItemCtrl() { Text = "Open"; };
+ new GuiMenuItemCtrl() { Text = "Open Recent"; };
+ new GuiMenuItemCtrl() { Text = "-"; };
+ new GuiMenuItemCtrl() { Text = "Save"; };
+ new GuiMenuItemCtrl() { Text = "Save As..."; };
+ new GuiMenuItemCtrl() { Text = "-"; };
+ new GuiMenuItemCtrl() { Text = "Exit"; };
+ };
+ new GuiMenuItemCtrl()
+ {
+ Text = "Edit";
+
+ new GuiMenuItemCtrl() { Text = "Undo"; };
+ new GuiMenuItemCtrl() { Text = "Redo"; };
+ new GuiMenuItemCtrl() { Text = "-"; };
+ new GuiMenuItemCtrl() { Text = "Cut"; };
+ new GuiMenuItemCtrl() { Text = "Copy"; };
+ new GuiMenuItemCtrl() { Text = "Paste"; };
+ };
+ new GuiMenuItemCtrl()
+ {
+ Text = "View";
+
+ new GuiMenuItemCtrl() { Text = "Zoom In"; };
+ new GuiMenuItemCtrl() { Text = "Zoom Out"; };
+ new GuiMenuItemCtrl() { Text = "Reset Zoom"; };
+ };
+ };
+ %this.setThemeSlot(%menuBar, "backgroundProfile", %theme, "Overlay");
+ %this.addSample(%menuBar);
+ }
+ else if(%category $= "Progress")
+ {
+ %progress = new GuiProgressCtrl()
+ {
+ Position = "0 0";
+ Extent = "220 24";
+ Profile = %member;
+ };
+ %this.addSample(%progress);
+ %progress.setProgress(0.65);
+ }
+ else if(%category $= "TreeView")
+ {
+ %tree = new GuiTreeViewCtrl()
+ {
+ Position = "0 0";
+ Extent = "220 140";
+ Profile = %member;
+ };
+ %this.addSample(%tree);
+ %tree.inspect(%this.previewTreeGroup);
+ }
+ else if(%category $= "FrameSet" || %category $= "FrameSetDropButton")
+ {
+ %frameSet = new GuiFrameSetCtrl()
+ {
+ Position = "0 0";
+ Extent = "260 160";
+ DividerThickness = 6;
+ Profile = %this.slotProfile(%theme, "FrameSet", %category, %member);
+ dropButtonProfile = %this.slotProfile(%theme, "FrameSetDropButton", %category, %member);
+ };
+ %this.addSample(%frameSet);
+ %frameSet.createHorizontalSplit(1);
+ }
+ else if(%category $= "ColorPicker" || %category $= "ColorSelector")
+ {
+ %this.addSample(new GuiColorPickerCtrl()
+ {
+ Position = "0 0";
+ Extent = "200 120";
+ Profile = %this.slotProfile(%theme, "ColorPicker", %category, %member);
+ SelectorProfile = %this.slotProfile(%theme, "ColorSelector", %category, %member);
+ });
+ }
+ else if(%category $= "ColorPopup")
+ {
+ %popup = new GuiColorPopupCtrl()
+ {
+ Position = "0 0";
+ Extent = "60 30";
+ Profile = %member;
+ };
+ %this.setThemeSlot(%popup, "backgroundProfile", %theme, "Overlay");
+ %this.setThemeSlot(%popup, "popupProfile", %theme, "Panel");
+ %this.setThemeSlot(%popup, "pickerProfile", %theme, "ColorPicker");
+ %this.setThemeSlot(%popup, "selectorProfile", %theme, "ColorSelector");
+ %this.addSample(%popup);
+ }
+ else if(%category $= "Slider" || %category $= "SliderThumb")
+ {
+ // A horizontal and a vertical slider, so both groove orientations show.
+ // The main profile styles the groove, thumbProfile styles the thumb.
+ %this.addSample(new GuiSliderCtrl()
+ {
+ Position = "0 0";
+ Extent = "220 30";
+ range = "0 100";
+ ticks = "10";
+ value = "60";
+ Profile = %this.slotProfile(%theme, "Slider", %category, %member);
+ thumbProfile = %this.slotProfile(%theme, "SliderThumb", %category, %member);
+ });
+
+ %this.addSample(new GuiSliderCtrl()
+ {
+ Position = "0 50";
+ Extent = "30 160";
+ range = "0 100";
+ ticks = "10";
+ value = "40";
+ Profile = %this.slotProfile(%theme, "Slider", %category, %member);
+ thumbProfile = %this.slotProfile(%theme, "SliderThumb", %category, %member);
+ });
+ }
+ else if(%category $= "Tooltip")
+ {
+ // Tooltips can't be hovered in the preview, so show the profile as a
+ // static swatch (no button) with text that reaches toward the edges so
+ // the fill, border, and padding all read.
+ %this.addSample(new GuiControl()
+ {
+ Position = "0 0";
+ Extent = "320 40";
+ Text = "A short hint that describes a control.";
+ Profile = %member;
+ });
+ }
+ else
+ {
+ // Empty, Overlay, DragAndDrop, and anything new.
+ %this.addGenericSample(%member);
+ }
+
+ %this.layoutStage();
+}
+
+function GuiProfileEditorPreview::showBorder(%this, %theme, %border)
+{
+ %this.clearSamples();
+ if(!isObject(%border))
+ {
+ return;
+ }
+ %this.lastKind = "border";
+ %this.lastTheme = %theme;
+ %this.lastMember = %border;
+
+ // A probe profile shows the border on a plain panel fill; the button
+ // shape exercises the border's highlight and selected states.
+ if(isObject(%theme))
+ {
+ %this.borderProbeProfile.fillColor = %theme.colorSurface;
+ %this.borderProbeProfile.fontColor = %theme.colorForeground;
+ }
+ %this.borderProbeProfile.borderDefault = %border;
+
+ %probe = new GuiButtonCtrl()
+ {
+ Position = "0 0";
+ Extent = "200 60";
+ Text = "Border sample";
+ Profile = %this.borderProbeProfile;
+ };
+ %this.addSample(%probe);
+
+ %disabled = new GuiButtonCtrl()
+ {
+ Position = "0 80";
+ Extent = "200 60";
+ Text = "Disabled";
+ Profile = %this.borderProbeProfile;
+ };
+ %this.addSample(%disabled);
+ %disabled.setActive(false);
+
+ %this.layoutStage();
+}
+
+//-----------------------------------------------------------------------------
+// Cursors. The only preview in this pane that is not something to look at: a
+// cursor is judged by using it, so this is a range to move the pointer through
+// with targets small enough that a hot spot a few pixels out is obvious.
+//
+// The canvas cursor is swapped on entry and put back on exit, which is why the
+// buttons inside show it too -- a button does not override getCursor, so what
+// the canvas holds is what it displays.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorPreview::showCursor(%this, %theme, %cursor)
+{
+ %this.clearSamples();
+ if(!isObject(%cursor))
+ {
+ return;
+ }
+ %this.lastKind = "cursor";
+ %this.lastTheme = %theme;
+ %this.lastMember = %cursor;
+
+ // Dressed in the theme being edited rather than the editor's own chrome, like
+ // every other sample in this pane. That is also what gives the targets a
+ // hover state: the editor's button profile has no highlight fill, so targets
+ // wearing it sat dead under the pointer -- and a target you cannot see
+ // yourself hit is no test of a hot spot.
+ %range = new GuiControl()
+ {
+ class = "GuiProfileEditorCursorRange";
+ Position = "0 0";
+ Extent = "280 220";
+ preview = %this;
+ cursor = %cursor;
+ };
+ %panel = isObject(%theme) ? %theme.getProfile("Panel") : 0;
+ if(isObject(%panel))
+ {
+ %range.setEditFieldValue("Profile", %panel);
+ }
+ else
+ {
+ ThemeManager.setProfile(%range, "displayBoxProfile");
+ }
+ %this.addSample(%range);
+
+ // Laid out by sizing flags rather than by numbers, because the range wears a
+ // profile from the theme being edited and a theme is free to give its panels
+ // however much padding it likes -- PlanetX does, and hard-coded positions
+ // slid out from under it. GuiControl::onChildAdded parentResizes each child
+ // against the parent's INNER rect, so "fill" and "center" resolve against
+ // the padded area the moment the child is added.
+ %hint = new GuiControl()
+ {
+ HorizSizing = "fill";
+ VertSizing = "anchorTop";
+ Position = "0 0";
+ Extent = "280 40";
+ Text = "Move in here to try the cursor. The target is small on purpose.";
+ align = "center";
+ vAlign = "top";
+ textWrap = true;
+ textExtend = true;
+ };
+ ThemeManager.setProfile(%hint, "labelProfile");
+ %range.add(%hint);
+
+ // One target, centred. Six scattered ones needed absolute coordinates to be
+ // scattered *within*, which is exactly what a padded panel takes away; a
+ // single centred button says the same thing and cannot drift.
+ %target = new GuiButtonCtrl()
+ {
+ HorizSizing = "center";
+ VertSizing = "center";
+ Position = "128 98";
+ Extent = "24 24";
+ Text = "+";
+ };
+ %button = isObject(%theme) ? %theme.getProfile("Button") : 0;
+ if(isObject(%button))
+ {
+ %target.setEditFieldValue("Profile", %button);
+ }
+ else
+ {
+ ThemeManager.setProfile(%target, "buttonProfile");
+ }
+ %range.add(%target);
+
+ %this.layoutStage();
+}
+
+// Entering swaps the canvas cursor for the one being edited; leaving puts back
+// the editor's own. Both are needed: the canvas cursor is global, so a preview
+// that only set it would leave the whole editor wearing a half-finished cursor.
+function GuiProfileEditorCursorRange::onMouseEnter(%this)
+{
+ if(isObject(%this.cursor))
+ {
+ Canvas.setCursor(%this.cursor);
+ }
+}
+
+function GuiProfileEditorCursorRange::onMouseLeave(%this)
+{
+ %this.restoreCursor();
+}
+
+function GuiProfileEditorCursorRange::onRemove(%this)
+{
+ // Deleted while the pointer is inside it -- selecting another node rebuilds
+ // the stage -- and onMouseLeave never arrives.
+ %this.restoreCursor();
+}
+
+function GuiProfileEditorCursorRange::restoreCursor(%this)
+{
+ if(isObject(ThemeManager.activeTheme.defaultCursor))
+ {
+ Canvas.setCursor(ThemeManager.activeTheme.defaultCursor);
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Sample builders.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorPreview::addSample(%this, %sample)
+{
+ %this.stage.add(%sample);
+ return %sample;
+}
+
+// An interactive control plus a disabled twin, for the four-state categories.
+function GuiProfileEditorPreview::addStateSamples(%this, %category, %member)
+{
+ if(%category $= "Button")
+ {
+ %this.addButtonSample(%member, 0);
+ }
+ else if(%category $= "CheckBox")
+ {
+ %active = new GuiCheckBoxCtrl()
+ {
+ Position = "0 0";
+ Extent = "160 30";
+ Text = "Check box";
+ Profile = %member;
+ };
+ %this.addSample(%active);
+ %active.setStateOn(true);
+
+ %disabled = new GuiCheckBoxCtrl()
+ {
+ Position = "0 40";
+ Extent = "160 30";
+ Text = "Disabled";
+ Profile = %member;
+ };
+ %this.addSample(%disabled);
+ %disabled.setActive(false);
+ }
+ else if(%category $= "Radio")
+ {
+ // Several radios sharing a groupNum toggle exclusively; plus a disabled one.
+ for(%i = 0; %i < 4; %i++)
+ {
+ %radio = new GuiRadioCtrl()
+ {
+ Position = "0" SPC (%i * 34);
+ Extent = "180 30";
+ Text = "Option" SPC (%i + 1);
+ groupNum = "700";
+ Profile = %member;
+ };
+ %this.addSample(%radio);
+ if(%i == 0)
+ {
+ %radio.setStateOn(true);
+ }
+ }
+
+ %disabled = new GuiRadioCtrl()
+ {
+ Position = "0" SPC (4 * 34);
+ Extent = "180 30";
+ Text = "Disabled";
+ groupNum = "700";
+ Profile = %member;
+ };
+ %this.addSample(%disabled);
+ %disabled.setActive(false);
+ }
+ else if(%category $= "TextEdit")
+ {
+ %this.addSample(new GuiTextEditCtrl()
+ {
+ Position = "0 0";
+ Extent = "200 30";
+ Text = "Edit me";
+ Profile = %member;
+ });
+
+ %disabled = new GuiTextEditCtrl()
+ {
+ Position = "0 40";
+ Extent = "200 30";
+ Text = "Disabled";
+ Profile = %member;
+ };
+ %this.addSample(%disabled);
+ %disabled.setActive(false);
+ }
+}
+
+function GuiProfileEditorPreview::addButtonSample(%this, %profile, %y)
+{
+ %this.addSample(new GuiButtonCtrl()
+ {
+ Position = "0" SPC %y;
+ Extent = "140 36";
+ Text = "Button";
+ Profile = %profile;
+ });
+
+ %disabled = new GuiButtonCtrl()
+ {
+ Position = "0" SPC (%y + 46);
+ Extent = "140 36";
+ Text = "Disabled";
+ Profile = %profile;
+ };
+ %this.addSample(%disabled);
+ %disabled.setActive(false);
+}
+
+// For profiles with no theme siblings: a plain container and a button, both
+// wearing the profile, so fill, font, and borders all show somewhere.
+function GuiProfileEditorPreview::addGenericSample(%this, %profile)
+{
+ %this.addSample(new GuiControl()
+ {
+ Position = "0 0";
+ Extent = "340 80";
+ Text = "The quick brown fox jumps over the lazy dog.";
+ Profile = %profile;
+ });
+
+ %this.addButtonSample(%profile, 100);
+}
diff --git a/editor/GuiEditor/scripts/GuiProfileEditorProfileForm.cs b/editor/GuiEditor/scripts/GuiProfileEditorProfileForm.cs
new file mode 100644
index 000000000..4897798d2
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiProfileEditorProfileForm.cs
@@ -0,0 +1,760 @@
+
+//-----------------------------------------------------------------------------
+// The custom profile-editing pane shown in place of the generic inspector when a
+// profile node is selected in the Gui Profile Editor -- the third and last of
+// the panes that replaced it, after ProfileThemeEditForm and
+// GuiProfileEditorBorderForm.
+//
+// A GuiControlProfile carries every field any control might want, but a given
+// profile only ever styles one kind of control, and most of them use a fraction
+// of it: seventeen of the engine's thirty-eight profile categories never draw
+// text at all. The pane asks GuiProfileEditorFieldSpec what the selected
+// profile's category actually reads and hides the rest, so a scroll-bar thumb
+// stops offering seven font fields. A Show All checkbox lifts the filter for
+// everything a profile can meaningfully carry; fields that are never meaningful
+// -- SimObject plumbing, theme bookkeeping, the border references the Borders
+// pane owns -- stay hidden either way.
+//
+// Layout is a vertical chain of blocks -- an identity header, an always-open
+// essentials block, then four collapsible GuiPanelCtrl sections -- and each
+// block lays its fields out in a GuiGridCtrl, the way the native inspector
+// does. That is what makes the pane use the width the Properties frame is given:
+// widen it and the cells reflow into two, three or four columns instead of
+// leaving dead space to the right.
+//
+// Filtering is done purely with setVisible. Both container types skip their
+// hidden children when they lay out, so a filtered field takes no space and
+// leaves no hole, and never rebuilding means a selection change can never free
+// a control the engine is mid-dispatch on.
+//
+// The pane owns every write to the profile; its rows only marshal values. The
+// creator sets formWidth and dialog inline, then calls build() once after
+// adding the pane to its scroller.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorProfileForm::onAdd(%this)
+{
+ ThemeManager.setProfile(%this, "emptyProfile");
+
+ // Every color row this form builds gets the popup that offers the selected
+ // theme's six colors. EditorFieldRow lives in EditorCore and cannot name a
+ // Gui Editor class, so the pane that wants one says so -- see its header.
+ %this.swatchClass = "GuiProfileEditorColorPopup";
+
+ %this.fieldSpec = new ScriptObject()
+ {
+ class = "GuiProfileEditorFieldSpec";
+ };
+}
+
+function GuiProfileEditorProfileForm::onRemove(%this)
+{
+ %this.unbind();
+
+ if(isObject(%this.fieldSpec))
+ {
+ %this.fieldSpec.delete();
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Construction.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorProfileForm::build(%this)
+{
+ %w = %this.formWidth;
+
+ // The minimum cell width. Cells are laid out by a GuiGridCtrl in Variable
+ // mode, so this is the narrowest a column may get: the grid fits as many
+ // columns as the pane can hold at this width, then shares the leftover
+ // evenly between them. Dragging the Properties frame wider therefore adds
+ // columns rather than leaving dead space, which is the whole point of using
+ // a grid here instead of a chain.
+ %this.rowWidth = 220;
+ %this.rowFields = "";
+ %this.panelList = "";
+
+ %this.buildHeader();
+ %this.buildEssentials();
+
+ // Sections in the order the work usually goes: the text block first because
+ // it is the one a text-drawing profile reaches for after the essentials.
+ %this.buildSection("TextLayout", "Text Layout",
+ "align vAlign textOffset fontCharset");
+ %this.buildSection("Interaction", "Interaction",
+ "tab canKeyFocus cursorColor fillColorTextSL fontColorTextSL");
+ %this.buildSection("RichText", "Rich Text Colors",
+ "fontColorLink fontColorLinkHL fontColors7 fontColors8 fontColors9");
+ %this.buildSection("Image", "Image",
+ "imageAsset bitmap");
+
+ // A GuiPanelCtrl learns its collapsed height only from parentResized -- its
+ // constructor defaults to 64x64 no matter what Extent the creator set, and a
+ // GuiChainCtrl positions its children without ever resizing them, so nothing
+ // else would tell the panels how tall their headers are. Nudging the chain's
+ // width by a pixel and back forces exactly one parentResized through every
+ // child, leaving the widths where they started. (The frame set in
+ // GuiProfileEditorDialog::init needs the same kind of forced layout pass.)
+ %h = getWord(%this.getExtent(), 1);
+ %this.resize(0, 0, %w + 1, %h);
+ %this.resize(0, 0, %w, %h);
+
+ // Text Layout starts open -- it is the section a text-drawing profile reaches
+ // for after the essentials. This has to run after the nudge above: a panel
+ // only records its collapsed height while it is collapsed, so expanding one
+ // first would leave it with the constructor's 64-pixel header forever.
+ %this.panel["TextLayout"].setExpanded(true);
+}
+
+// The header spans the whole pane rather than sitting in the cell grid: it
+// identifies what is being edited and sets the filter, so it stays put while
+// the cells below reflow into however many columns fit.
+function GuiProfileEditorProfileForm::buildHeader(%this)
+{
+ %w = %this.formWidth;
+
+ %this.header = new GuiControl()
+ {
+ HorizSizing = "width";
+ Position = "0 0";
+ Extent = %w SPC 78;
+ };
+ ThemeManager.setProfile(%this.header, "emptyProfile");
+ %this.add(%this.header);
+
+ %this.nameLabel = new GuiControl()
+ {
+ HorizSizing = "width";
+ Position = "6 2";
+ Extent = (%w - 12) SPC 22;
+ Text = "Profile:";
+ align = "left";
+ vAlign = "middle";
+ };
+ ThemeManager.setProfile(%this.nameLabel, "labelProfile");
+ %this.header.add(%this.nameLabel);
+
+ %forLabel = new GuiControl()
+ {
+ Position = "6 28";
+ Extent = "30 22";
+ Text = "For:";
+ align = "left";
+ vAlign = "middle";
+ };
+ ThemeManager.setProfile(%forLabel, "labelProfile");
+ %this.header.add(%forLabel);
+
+ // The category is what the whole pane filters on. It is fixed for a theme
+ // member (the theme names its slot) and free for a standalone profile, which
+ // starts with none -- setting it there also picks the preview's sample.
+ %this.categoryDrop = new GuiDropDownCtrl()
+ {
+ class = "EditorFieldRowDropDown";
+ Position = "38 28";
+ Extent = "150 22";
+ ConstantThumbHeight = false;
+ ScrollBarThickness = 12;
+ ShowArrowButtons = true;
+ owner = %this;
+ selectMethod = "onCategoryChanged";
+ };
+ ThemeManager.setProfile(%this.categoryDrop, "dropDownProfile");
+ ThemeManager.setProfile(%this.categoryDrop, "dropDownItemProfile", "listBoxProfile");
+ ThemeManager.setProfile(%this.categoryDrop, "emptyProfile", "backgroundProfile");
+ ThemeManager.setProfile(%this.categoryDrop, "scrollingPanelProfile", "ScrollProfile");
+ ThemeManager.setProfile(%this.categoryDrop, "scrollingPanelThumbProfile", "ThumbProfile");
+ ThemeManager.setProfile(%this.categoryDrop, "scrollingPanelTrackProfile", "TrackProfile");
+ ThemeManager.setProfile(%this.categoryDrop, "scrollingPanelArrowProfile", "ArrowProfile");
+ %this.header.add(%this.categoryDrop);
+
+ %this.categoryDrop.addItem(%this.anyCategoryLabel());
+ %names = %this.fieldSpec.categoryNames;
+ %count = getFieldCount(%names);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %this.categoryDrop.addItem(getField(%names, %i));
+ }
+
+ %showAllW = %w - 200;
+ %this.showAllBox = new GuiCheckBoxCtrl()
+ {
+ HorizSizing = "width";
+ Position = "194 28";
+ Extent = %showAllW SPC 22;
+ Text = "Show all fields";
+ boxOffset = "0 2";
+ boxExtent = "18 18";
+ textOffset = "24 2";
+ textExtent = (%showAllW - 24) SPC 18;
+ Command = %this.getID() @ ".onShowAllToggled();";
+ };
+ ThemeManager.setProfile(%this.showAllBox, "checkboxProfile");
+ %this.header.add(%this.showAllBox);
+
+ // Shown only for the categories whose control draws a circle or a ring:
+ // renderBorderedCircle and renderBorderedRing read the default border alone,
+ // so the Borders pane's four sides do nothing for them.
+ %this.circleHint = new GuiControl()
+ {
+ Position = "6 54";
+ Extent = (%w - 12) SPC 20;
+ Text = "Draws a circle - only the default border applies.";
+ align = "left";
+ vAlign = "middle";
+ Visible = false;
+ };
+ ThemeManager.setProfile(%this.circleHint, "labelProfile");
+ %this.header.add(%this.circleHint);
+}
+
+// A grid of field cells. Variable column mode reflows into as many columns as
+// the pane can hold at rowWidth and shares the leftover width between them;
+// variable row mode lets a taller cell (the state-color ones) set the height of
+// its own row; a dynamic extent grows the grid downward inside the scroller.
+// This is the same configuration the native GuiInspector gives its group grids.
+//
+// A hidden cell is skipped by the grid, so the cells after it close up rather
+// than flowing around a hole -- see GuiGridCtrl::resize.
+function GuiProfileEditorProfileForm::makeCellGrid(%this, %y)
+{
+ %grid = new GuiGridCtrl()
+ {
+ HorizSizing = "width";
+ Position = "0" SPC %y;
+ Extent = %this.formWidth SPC 4;
+ CellModeX = "variable";
+ CellModeY = "variable";
+ CellSizeX = %this.rowWidth;
+ CellSizeY = 48;
+ CellSpacingX = 4;
+ CellSpacingY = 4;
+ MaxColCount = 0;
+ MaxRowCount = 0;
+ OrderMode = "lrtb";
+ IsExtentDynamic = true;
+ };
+ ThemeManager.setProfile(%grid, "emptyProfile");
+ return %grid;
+}
+
+// The always-visible block: the two state-color cells and the typeface, which
+// together cover nearly every edit anyone makes to a profile.
+function GuiProfileEditorProfileForm::buildEssentials(%this)
+{
+ %grid = %this.makeCellGrid(0);
+ %this.add(%grid);
+ %this.essentialsGrid = %grid;
+
+ %this.fillRow = %this.addStateColorRow(%grid, "fillColor", "Fill Color");
+
+ %this.addFieldRow(%grid, "fontType", "Font Face", "dropdown", "", "");
+ %this.addFieldRow(%grid, "fontSize", "Font Size", "number", "", "");
+
+ %this.fontRow = %this.addStateColorRow(%grid, "fontColor", "Text Color");
+}
+
+// A collapsible section. Its cells live in an inner grid rather than directly on
+// the panel: GuiExpandCtrl::toggleHiddenChildren force-writes mVisible on every
+// direct child whenever it expands, collapses or is resized, which would undo
+// the filter. Grandchildren are untouched, and the grid skips the hidden ones.
+function GuiProfileEditorProfileForm::buildSection(%this, %key, %title, %fields)
+{
+ %w = %this.formWidth;
+ %headerH = 24;
+
+ %panel = new GuiPanelCtrl()
+ {
+ HorizSizing = "width";
+ Text = %title;
+ Position = "0 0";
+ Extent = %w SPC %headerH;
+ MinExtent = "80" SPC %headerH;
+ };
+ ThemeManager.setProfile(%panel, "panelProfile");
+ %this.add(%panel);
+
+ %grid = %this.makeCellGrid(%headerH);
+ %panel.add(%grid);
+
+ %this.panel[%key] = %panel;
+ %this.panelFields[%key] = %fields;
+ %this.panelList = (%this.panelList $= "") ? %key : (%this.panelList SPC %key);
+
+ %count = getWordCount(%fields);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %field = getWord(%fields, %i);
+ %this.addFieldRow(%grid, %field, %this.labelFor(%field), %this.kindFor(%field),
+ %this.enumItemsFor(%field), %this.arrayIndexFor(%field));
+ }
+}
+
+function GuiProfileEditorProfileForm::addFieldRow(%this, %container, %field, %label, %kind, %enumItems, %arrayIndex)
+{
+ %row = new GuiControl()
+ {
+ class = "EditorFieldRow";
+ Position = "0 0";
+ fieldName = %field;
+ labelText = %label;
+ kind = %kind;
+ enumItems = %enumItems;
+ arrayIndex = %arrayIndex;
+ owner = %this;
+ };
+ %container.add(%row);
+ %row.build();
+
+ %this.row[%field] = %row;
+ %this.rowFields = (%this.rowFields $= "") ? %field : (%this.rowFields SPC %field);
+ return %row;
+}
+
+function GuiProfileEditorProfileForm::addStateColorRow(%this, %container, %fieldBase, %label)
+{
+ %row = new GuiControl()
+ {
+ class = "GuiProfileEditorStateColorRow";
+ Position = "0 0";
+ fieldBase = %fieldBase;
+ labelText = %label;
+ owner = %this;
+ };
+ %container.add(%row);
+ %row.build();
+ return %row;
+}
+
+//-----------------------------------------------------------------------------
+// Field descriptions. Kept as lookups rather than a table so the section
+// definitions above read as plain field lists.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorProfileForm::labelFor(%this, %field)
+{
+ // Deliberately no backslash-c in any of these: that sequence is the engine's
+ // in-string color escape (see dglDrawTextN), so writing it in a caption would
+ // recolor the label instead of naming the field.
+ switch$(%field)
+ {
+ case "align": return "Horizontal Align";
+ case "vAlign": return "Vertical Align";
+ case "textOffset": return "Text Offset";
+ case "fontCharset": return "Font Charset";
+ case "tab": return "Tab Stop";
+ case "canKeyFocus": return "Keyboard Focus";
+ case "cursorColor": return "Caret Color";
+ case "fillColorTextSL": return "Selection Fill";
+ case "fontColorTextSL": return "Selection Text";
+ case "fontColorLink": return "Link Color";
+ case "fontColorLinkHL": return "Link Highlight";
+ case "fontColors7": return "Custom Color 1";
+ case "fontColors8": return "Custom Color 2";
+ case "fontColors9": return "Custom Color 3";
+ case "imageAsset": return "Image Asset";
+ case "bitmap": return "Bitmap";
+ }
+ return %field;
+}
+
+function GuiProfileEditorProfileForm::kindFor(%this, %field)
+{
+ switch$(%field)
+ {
+ case "align" or "vAlign" or "fontCharset": return "enum";
+ case "textOffset": return "point";
+ case "tab" or "canKeyFocus": return "bool";
+ case "cursorColor" or "fillColorTextSL" or "fontColorTextSL": return "color";
+ case "fontColorLink" or "fontColorLinkHL": return "color";
+ case "fontColors7" or "fontColors8" or "fontColors9": return "color";
+ // A path nobody should be asked to type: the row gets a Find button.
+ case "bitmap": return "file";
+ // Nor an asset id, which you would otherwise have to remember exactly.
+ case "imageAsset": return "asset";
+ }
+ return "text";
+}
+
+// The engine's own enum tables, from guiTypes.cc. Space-separated here and
+// converted to the tab list the row wants, because no value contains a space.
+function GuiProfileEditorProfileForm::enumItemsFor(%this, %field)
+{
+ %items = "";
+ switch$(%field)
+ {
+ case "align": %items = "left center right";
+ case "vAlign": %items = "top middle bottom";
+ case "fontCharset": %items = "ANSI SYMBOL SHIFTJIS HANGEUL HANGUL GB2312 CHINESEBIG5 OEM JOHAB HEBREW ARABIC GREEK TURKISH VIETNAMESE THAI EASTEUROPE RUSSIAN MAC BALTIC";
+ }
+ return %this.wordsToTabs(%items);
+}
+
+// The three user color slots live in the fontColors array rather than in named
+// fields, so their rows carry the index and the pane writes them through it.
+function GuiProfileEditorProfileForm::arrayIndexFor(%this, %field)
+{
+ switch$(%field)
+ {
+ case "fontColors7": return 7;
+ case "fontColors8": return 8;
+ case "fontColors9": return 9;
+ }
+ return "";
+}
+
+function GuiProfileEditorProfileForm::wordsToTabs(%this, %words)
+{
+ %out = "";
+ %count = getWordCount(%words);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %word = getWord(%words, %i);
+ %out = (%out $= "") ? %word : (%out TAB %word);
+ }
+ return %out;
+}
+
+function GuiProfileEditorProfileForm::anyCategoryLabel(%this)
+{
+ return "(any)";
+}
+
+//-----------------------------------------------------------------------------
+// Binding. %kind is the tree proxy's kind, which decides whether the category
+// is the user's to choose.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorProfileForm::bind(%this, %profile, %kind)
+{
+ if(!isObject(%profile))
+ {
+ %this.unbind();
+ return;
+ }
+
+ %this.target = %profile;
+ %this.proxyKind = %kind;
+ %this.isStandalone = (%kind $= "standalone");
+
+ %this.nameLabel.setText("Profile: " @ %profile.getName());
+
+ // A theme member's category names its slot and renaming it would orphan the
+ // member; only a standalone profile gets to choose.
+ %this.populating = true;
+ %category = %profile.category;
+ %this.selectCategory(%category);
+ %this.categoryDrop.setActive(%this.isStandalone);
+ %this.populating = false;
+
+ %this.applyFilter();
+ %this.refresh();
+}
+
+function GuiProfileEditorProfileForm::unbind(%this)
+{
+ %this.target = "";
+ %this.proxyKind = "";
+ %this.isStandalone = false;
+}
+
+// The theme the bound profile belongs to, or nothing for a standalone profile.
+// Overrides only exist against a theme.
+function GuiProfileEditorProfileForm::currentTheme(%this)
+{
+ %root = %this.dialog.currentRoot;
+ if(isObject(%root) && %root.getClassName() $= "GuiProfileTheme")
+ {
+ return %root;
+ }
+ return "";
+}
+
+function GuiProfileEditorProfileForm::currentCategory(%this)
+{
+ if(!isObject(%this.target))
+ {
+ return "";
+ }
+ return %this.target.category;
+}
+
+function GuiProfileEditorProfileForm::selectCategory(%this, %category)
+{
+ %label = (%category $= "") ? %this.anyCategoryLabel() : %category;
+ %index = %this.categoryDrop.findItemText(%label, false);
+ if(%index < 0)
+ {
+ // A category this build does not know: keep it rather than silently
+ // retagging the profile.
+ %this.categoryDrop.addItem(%label);
+ %index = %this.categoryDrop.findItemText(%label, false);
+ }
+ %this.categoryDrop.setSelected(%index);
+}
+
+//-----------------------------------------------------------------------------
+// Filtering. Nothing here creates or deletes a control -- it only decides what
+// is visible and what is inert.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorProfileForm::applyFilter(%this)
+{
+ %spec = %this.fieldSpec;
+ %category = %this.currentCategory();
+ %showAll = %this.showAllBox.getStateOn();
+
+ %count = getWordCount(%this.rowFields);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %field = getWord(%this.rowFields, %i);
+ %row = %this.row[%field];
+ %row.setVisible(%spec.isFieldVisible(%category, %field, %showAll));
+ }
+
+ // cursorColor paints the text caret in one control and the keyboard-focus
+ // rectangle in another; name it for whichever this profile is.
+ %this.row["cursorColor"].setLabelText(%spec.cursorLabelFor(%category));
+
+ // The fill row is universal; the text colors follow the category's text
+ // class exactly as their individual fields would.
+ %this.fillRow.setVisible(true);
+ %this.fontRow.setVisible(%spec.isFieldVisible(%category, "fontColor", %showAll));
+
+ %this.applyStateFilter(%this.fillRow, %category, %showAll);
+ %this.applyStateFilter(%this.fontRow, %category, %showAll);
+
+ // A section with nothing left to show gets out of the way entirely.
+ %panels = getWordCount(%this.panelList);
+ for(%i = 0; %i < %panels; %i++)
+ {
+ %key = getWord(%this.panelList, %i);
+ %this.panel[%key].setVisible(%spec.anyFieldVisible(%category, %this.panelFields[%key], %showAll));
+ }
+
+ %this.circleHint.setVisible(!%showAll && %spec.hasFlag(%category, "circle"));
+}
+
+// Grey the states the bound control never renders in rather than hiding them,
+// so a value set earlier (or by hand in the file) is never lost.
+function GuiProfileEditorProfileForm::applyStateFilter(%this, %row, %category, %showAll)
+{
+ %spec = %this.fieldSpec;
+ %reason = "This control never renders in this state.";
+ for(%i = 0; %i < 4; %i++)
+ {
+ %live = %showAll || %spec.isStateLive(%category, %i);
+ %row.setStateEnabled(%i, %live, %reason);
+ }
+}
+
+function GuiProfileEditorProfileForm::onShowAllToggled(%this)
+{
+ %this.applyFilter();
+ %this.refresh();
+}
+
+// The standalone category picker. Writing category on a standalone profile is
+// safe: GuiControlProfile::onStaticModified only records overrides for a
+// profile that belongs to a theme.
+function GuiProfileEditorProfileForm::onCategoryChanged(%this)
+{
+ if(%this.populating || !isObject(%this.target) || !%this.isStandalone)
+ {
+ return;
+ }
+
+ %choice = %this.categoryDrop.getText();
+ %this.target.category = (%choice $= %this.anyCategoryLabel()) ? "" : %choice;
+
+ %this.applyFilter();
+ %this.refresh();
+
+ // The preview picks its sample control from the category too.
+ %this.dialog.onProfileChanged(%this.target);
+ %this.dialog.updatePreview();
+}
+
+//-----------------------------------------------------------------------------
+// Loading values. The populating guard keeps every setText / setColorI /
+// setStateOn from echoing back through the commits and marking the profile as
+// having overridden fields it never touched.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorProfileForm::refresh(%this)
+{
+ if(!isObject(%this.target))
+ {
+ return;
+ }
+
+ %this.populating = true;
+
+ // Every font installed on this machine, plus any the project already has a
+ // cache for, plus this profile's own face if it is neither.
+ %this.row["fontType"].fillItems(
+ %this.dialog.library.getFontFaceList(%this.target.fontType));
+
+ %count = getWordCount(%this.rowFields);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %field = getWord(%this.rowFields, %i);
+ %this.row[%field].setValue(%this.readField(%field));
+ }
+
+ for(%i = 0; %i < 4; %i++)
+ {
+ %this.fillRow.setValue(%i, %this.target.getFieldValue(%this.fillRow.stateField(%i)));
+ %this.fontRow.setValue(%i, %this.target.getFieldValue(%this.fontRow.stateField(%i)));
+ }
+
+ %this.populating = false;
+
+ %this.refreshOverrides();
+}
+
+// The three user color slots are array elements, which getFieldValue cannot
+// reach (it always passes a null index), so they are read through slot access.
+function GuiProfileEditorProfileForm::readField(%this, %field)
+{
+ switch$(%field)
+ {
+ case "fontColors7": return %this.target.fontColors[7];
+ case "fontColors8": return %this.target.fontColors[8];
+ case "fontColors9": return %this.target.fontColors[9];
+ }
+ return %this.target.getFieldValue(%field);
+}
+
+function GuiProfileEditorProfileForm::writeField(%this, %field, %value)
+{
+ switch$(%field)
+ {
+ case "fontColors7": %this.target.fontColors[7] = %value; return;
+ case "fontColors8": %this.target.fontColors[8] = %value; return;
+ case "fontColors9": %this.target.fontColors[9] = %value; return;
+ }
+ %this.target.setFieldValue(%field, %value);
+}
+
+// Mark every field that has been pushed away from its theme's stamped value. A
+// standalone profile has no theme, so nothing is ever marked.
+function GuiProfileEditorProfileForm::refreshOverrides(%this)
+{
+ %theme = %this.currentTheme();
+ %hasTheme = isObject(%theme);
+
+ %count = getWordCount(%this.rowFields);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %field = getWord(%this.rowFields, %i);
+ %this.row[%field].setOverridden(%hasTheme && %theme.isFieldOverridden(%this.target, %field));
+ }
+
+ %this.refreshStateOverrides(%this.fillRow, %theme, %hasTheme);
+ %this.refreshStateOverrides(%this.fontRow, %theme, %hasTheme);
+}
+
+function GuiProfileEditorProfileForm::refreshStateOverrides(%this, %row, %theme, %hasTheme)
+{
+ for(%i = 0; %i < 4; %i++)
+ {
+ %field = %row.stateField(%i);
+ %row.setStateOverridden(%i, %hasTheme && %theme.isFieldOverridden(%this.target, %field));
+ }
+ %row.refreshResetButton();
+}
+
+//-----------------------------------------------------------------------------
+// Commits. Every write to the profile goes through here, and every one of them
+// ends at the dialog's commit sink, which marks the theme dirty and defers the
+// preview rebuild (see GuiProfileEditorDialog::schedulePreviewRefresh -- a
+// synchronous rebuild here can free a control the engine is mid-event on).
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorProfileForm::onFieldRowCommit(%this, %row)
+{
+ if(%this.populating || !isObject(%this.target))
+ {
+ return;
+ }
+
+ // A text box commits on blur, so most commits arrive from a field the user
+ // only tabbed through. Writing one would mark the field as overridden away
+ // from its theme -- a permanent change to the saved file -- for an edit that
+ // never happened, so a value that matches what was loaded is dropped here.
+ if(!%row.hasChanged())
+ {
+ return;
+ }
+
+ // No font cache is baked here, whatever face or size was just picked: the
+ // preview renders it straight from the platform font, and baking costs seconds
+ // -- it happens on Save instead.
+ %this.writeField(%row.fieldName, %row.getValue());
+ %row.markClean();
+ %this.afterCommit();
+}
+
+function GuiProfileEditorProfileForm::onProfileStateColorCommit(%this, %row, %index)
+{
+ if(%this.populating || !isObject(%this.target))
+ {
+ return;
+ }
+
+ // As in onFieldRowCommit: a swatch that came back holding what was loaded
+ // into it is not an edit, and must not record a theme override.
+ if(!%row.hasChanged(%index))
+ {
+ return;
+ }
+
+ %this.target.setFieldValue(%row.stateField(%index), %row.getValue(%index));
+ %row.markClean(%index);
+ %this.afterCommit();
+}
+
+function GuiProfileEditorProfileForm::onFieldRowReset(%this, %row)
+{
+ %theme = %this.currentTheme();
+ if(!isObject(%theme) || !isObject(%this.target))
+ {
+ return;
+ }
+
+ %theme.resetField(%this.target, %row.fieldName);
+ %this.refresh();
+ %this.afterCommit();
+}
+
+// Clears whichever of the row's four states are overridden. Resetting a state
+// that is not overridden would be a no-op, but skipping them avoids a restamp
+// of the whole theme per state.
+function GuiProfileEditorProfileForm::onProfileStateColorReset(%this, %row)
+{
+ %theme = %this.currentTheme();
+ if(!isObject(%theme) || !isObject(%this.target))
+ {
+ return;
+ }
+
+ for(%i = 0; %i < 4; %i++)
+ {
+ %field = %row.stateField(%i);
+ if(%theme.isFieldOverridden(%this.target, %field))
+ {
+ %theme.resetField(%this.target, %field);
+ }
+ }
+
+ %this.refresh();
+ %this.afterCommit();
+}
+
+function GuiProfileEditorProfileForm::afterCommit(%this)
+{
+ %this.refreshOverrides();
+ %this.dialog.onProfileChanged(%this.target);
+}
diff --git a/editor/GuiEditor/scripts/GuiProfileEditorStateColorRow.cs b/editor/GuiEditor/scripts/GuiProfileEditorStateColorRow.cs
new file mode 100644
index 000000000..bce4085d4
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiProfileEditorStateColorRow.cs
@@ -0,0 +1,218 @@
+
+//-----------------------------------------------------------------------------
+// A profile's fill and text colors are each one value per control state, so
+// each set gets a single grid cell holding four swatches with the state
+// captions beneath -- the same shape GuiProfileEditorBorderGrid gives the
+// border values, so the two panes read alike. Two of these replace eight
+// full-width inspector rows.
+//
+// Field names follow the engine's own convention: the base name is the normal
+// state and HL / SL / NA are suffixes, which holds for both fillColor* and
+// fontColor*, so one row class serves both.
+//
+// A state the bound control never renders in is greyed rather than dropped, so
+// its value survives; the pane's Show All puts every state back in reach.
+//
+// Overrides are per state, and the caption under each swatch turns the theme's
+// override color to show it. Resetting is per row rather than per state: a
+// caption is a label, and making it a button would give it a control's hover
+// behaviour. The single reset button clears whichever of the four states are
+// overridden, which is per-state in every case but the rare one where a user
+// overrode two states of the same row and wants only one back.
+//
+// The creator sets fieldBase, labelText and owner inline; call build() once
+// after adding the row to its container, which decides the cell width. It
+// records .rowHeight. The row never touches the profile -- commits go to
+// owner.onProfileStateColorCommit(%row, %stateIndex) and resets to
+// owner.onProfileStateColorReset(%row).
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorStateColorRow::onAdd(%this)
+{
+ ThemeManager.setProfile(%this, "emptyProfile");
+}
+
+// Suffix per control state; index 0 is the normal state, drawn with no caption.
+function GuiProfileEditorStateColorRow::stateSuffix(%this, %index)
+{
+ return getWord("_ HL SL NA", %index);
+}
+
+function GuiProfileEditorStateColorRow::stateField(%this, %index)
+{
+ return (%index == 0) ? %this.fieldBase : (%this.fieldBase @ %this.stateSuffix(%index));
+}
+
+function GuiProfileEditorStateColorRow::build(%this)
+{
+ // The grid has already sized this cell by the time build() runs, so lay out
+ // against the width we actually have. See EditorFieldRow::build.
+ %w = getWord(%this.getExtent(), 0);
+ %pad = 4;
+ %resetW = 24;
+ %gap = 4;
+ %swatchW = 42;
+ %labelH = 16;
+ %swatchY = %labelH + 4;
+ %captionY = %swatchY + 24;
+ %h = %captionY + 14 + 4;
+
+ %this.rowHeight = %h;
+ %this.setExtent(%w, %h);
+
+ // The caption sits above the swatches, matching the plain field cells, so a
+ // grid can flow the two kinds of cell together in one column.
+ %this.label = new GuiControl()
+ {
+ HorizSizing = "width";
+ Position = %pad SPC 2;
+ Extent = (%w - %pad * 2) SPC %labelH;
+ Text = %this.labelText;
+ align = "left";
+ vAlign = "middle";
+ };
+ ThemeManager.setProfile(%this.label, "labelProfile");
+ %this.add(%this.label);
+
+ for(%i = 0; %i < 4; %i++)
+ {
+ %x = %pad + %i * (%swatchW + %gap);
+
+ // Swatches keep their size as the cell widens -- four fixed chips read
+ // as a state row, where four stretched bars would not.
+ %swatch = new GuiColorPopupCtrl()
+ {
+ class = "GuiProfileEditorColorPopup";
+ Position = %x SPC %swatchY;
+ Extent = %swatchW SPC 22;
+ showColorValues = true;
+ };
+ ThemeManager.setProfile(%swatch, "colorPickerProfile");
+ ThemeManager.setProfile(%swatch, "emptyProfile", "backgroundProfile");
+ ThemeManager.setProfile(%swatch, "colorPopupProfile", "popupProfile");
+ ThemeManager.setProfile(%swatch, "emptyProfile", "pickerProfile");
+ ThemeManager.setProfile(%swatch, "colorPickerSelectorProfile", "selectorProfile");
+ ThemeManager.setProfile(%swatch, "textEditProfile", "valueProfile");
+ // Worn by the swatch's own greyed-state tip and handed on to the popup's
+ // R/G/B/A boxes, which name their channel the same way.
+ ThemeManager.setProfile(%swatch, "tipProfile", "TooltipProfile");
+ %swatch.Command = %this.getID() @ ".commitState(" @ %i @ ");";
+ %this.add(%swatch);
+ %this.swatch[%i] = %swatch;
+
+ %caption = new GuiControl()
+ {
+ Position = %x SPC %captionY;
+ Extent = %swatchW SPC 14;
+ Text = (%i == 0) ? "" : %this.stateSuffix(%i);
+ align = "center";
+ };
+ ThemeManager.setProfile(%caption, "labelProfile");
+ %this.add(%caption);
+ %this.caption[%i] = %caption;
+ }
+
+ // The circular revert arrow. "left" sizing pins the button to the cell's
+ // right edge as the grid widens.
+ %this.resetButton = new GuiButtonCtrl()
+ {
+ class = "EditorIconButton";
+ Frame = $EditorIcon::playback_reload;
+ HorizSizing = "left";
+ Position = (%w - %resetW - %pad) SPC (%swatchY - 1);
+ Tooltip = "Reset this row's overridden states to the theme's values";
+ Command = %this.getID() @ ".onResetClicked();";
+ Visible = false;
+ };
+ ThemeManager.setProfile(%this.resetButton, "iconButtonProfile");
+ %this.add(%this.resetButton);
+}
+
+//-----------------------------------------------------------------------------
+// Values.
+//-----------------------------------------------------------------------------
+
+// Loading a swatch also records what it ended up holding, so a later commit can
+// tell an actual edit from a no-op. The recorded form is what the swatch reads
+// back: a ColorI field holding "White" comes back as "255 255 255 255".
+function GuiProfileEditorStateColorRow::setValue(%this, %index, %value)
+{
+ // setColorI wants four integers, but a ColorI field holding a stock color
+ // comes back as a single name token; baseColor parses those.
+ if(getWordCount(%value) >= 4)
+ {
+ %this.swatch[%index].setColorI(%value);
+ }
+ else
+ {
+ %this.swatch[%index].baseColor = %value;
+ }
+ %this.lastValue[%index] = %this.getValue(%index);
+}
+
+function GuiProfileEditorStateColorRow::hasChanged(%this, %index)
+{
+ return %this.getValue(%index) !$= %this.lastValue[%index];
+}
+
+function GuiProfileEditorStateColorRow::markClean(%this, %index)
+{
+ %this.lastValue[%index] = %this.getValue(%index);
+}
+
+function GuiProfileEditorStateColorRow::getValue(%this, %index)
+{
+ return %this.swatch[%index].getColorI();
+}
+
+//-----------------------------------------------------------------------------
+// Filtering and the override markers.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorStateColorRow::setStateEnabled(%this, %index, %enabled, %reason)
+{
+ %this.swatch[%index].setActive(%enabled);
+ %this.swatch[%index].Tooltip = %enabled ? "" : %reason;
+}
+
+function GuiProfileEditorStateColorRow::setStateOverridden(%this, %index, %overridden)
+{
+ ThemeManager.setProfile(%this.caption[%index], %overridden ? "overrideLabelProfile" : "labelProfile");
+ %this.overridden[%index] = %overridden;
+}
+
+// Called after all four states have been marked, so the row's single reset
+// button appears exactly when there is something to reset.
+function GuiProfileEditorStateColorRow::refreshResetButton(%this)
+{
+ %any = false;
+ for(%i = 0; %i < 4; %i++)
+ {
+ if(%this.overridden[%i])
+ {
+ %any = true;
+ }
+ }
+ %this.resetButton.setVisible(%any);
+}
+
+//-----------------------------------------------------------------------------
+// Commit.
+//-----------------------------------------------------------------------------
+
+function GuiProfileEditorStateColorRow::commitState(%this, %index)
+{
+ if(!isObject(%this.owner) || %this.owner.populating)
+ {
+ return;
+ }
+ %this.owner.onProfileStateColorCommit(%this, %index);
+}
+
+function GuiProfileEditorStateColorRow::onResetClicked(%this)
+{
+ if(isObject(%this.owner))
+ {
+ %this.owner.onProfileStateColorReset(%this);
+ }
+}
diff --git a/editor/GuiEditor/scripts/GuiProfileEditorTree.cs b/editor/GuiEditor/scripts/GuiProfileEditorTree.cs
new file mode 100644
index 000000000..ffe30d5a5
--- /dev/null
+++ b/editor/GuiEditor/scripts/GuiProfileEditorTree.cs
@@ -0,0 +1,10 @@
+
+function GuiProfileEditorTree::onSelect(%this, %index, %text, %item)
+{
+ %this.dialog.onTreeSelect(%item);
+}
+
+function GuiProfileEditorTree::onGetObjectText(%this, %obj)
+{
+ return %obj.treeLabel;
+}
diff --git a/editor/GuiEditor/scripts/ProfileThemeEditForm.cs b/editor/GuiEditor/scripts/ProfileThemeEditForm.cs
new file mode 100644
index 000000000..0aea28a52
--- /dev/null
+++ b/editor/GuiEditor/scripts/ProfileThemeEditForm.cs
@@ -0,0 +1,280 @@
+
+//-----------------------------------------------------------------------------
+// The custom theme-editing form shown in place of the generic inspector when a
+// theme is selected in the Gui Profile Editor. It is a GuiGridCtrl with
+// superclass EditorForm (so all the addFormItem / create*Item row factories are
+// available) and is its own event listener. Every field edit writes straight to
+// the theme, which restamps its members, then marks the library dirty and
+// refreshes the preview -- the same path the inspector's onProfileChanged took.
+//
+// Text fields commit on blur via AltCommand (fired from onLoseFirstResponder),
+// matching how the native inspector applies its edits; Enter commits too.
+//-----------------------------------------------------------------------------
+
+function ProfileThemeEditForm::build(%this)
+{
+ %cw = 350;
+
+ // The six theme colors, in display order. Field names double as the
+ // dispatch keys the change handlers use; kept on the form (not a global)
+ // so bindTheme can walk them.
+ %this.colorFields = "colorBackground colorSurface colorForeground colorAccent colorHighlight colorWarning";
+ %colorLabels = "Background" TAB "Surface" TAB "Foreground" TAB "Accent" TAB "Highlight" TAB "Warning";
+
+ // Name (display only; rename lives on the toolbar).
+ %this.nameLabel = %this.addFormItem("Theme:", %cw SPC 30);
+
+ %item = %this.addFormItem("Border Size", %cw SPC 30);
+ %this.borderSizeBox = %this.createTextEditItem(%item);
+ %this.borderSizeBox.inputMode = "Number";
+ %this.borderSizeBox.AltCommand = %this.getID() @ ".commitBorderSize();";
+
+ // No font directory row: the project keeps its font caches in one folder
+ // (GuiProfileEditorLibrary::getFontsPath), which every theme is pointed at on
+ // creation and on load. The three drop-downs below offer the machine's
+ // installed fonts, so choosing a font is the only question left.
+ %item = %this.addFormItem("Font - Title", %cw SPC 30);
+ %this.fontTitleDrop = %this.createDropDownItem(%item);
+ %this.fontTitleDrop.themeField = "fontTitle";
+
+ %item = %this.addFormItem("Font - Body", %cw SPC 30);
+ %this.fontBodyDrop = %this.createDropDownItem(%item);
+ %this.fontBodyDrop.themeField = "fontBody";
+
+ %item = %this.addFormItem("Font - Code", %cw SPC 30);
+ %this.fontCodeDrop = %this.createDropDownItem(%item);
+ %this.fontCodeDrop.themeField = "fontCode";
+
+ %item = %this.addFormItem("Font Size", %cw SPC 30);
+ %this.fontSizeBox = %this.createTextEditItem(%item);
+ %this.fontSizeBox.inputMode = "Number";
+ %this.fontSizeBox.AltCommand = %this.getID() @ ".commitFontSize();";
+
+ %count = getFieldCount(%colorLabels);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %field = getWord(%this.colorFields, %i);
+ %label = getField(%colorLabels, %i);
+
+ %item = %this.addFormItem(%label, %cw SPC 76);
+ %item.vAlign = "top";
+ %item.align = "left";
+ // The form's own R/G/B/A boxes already cover exact values, so this popup
+ // takes the swatch row only -- handy for pulling one theme color level
+ // with another.
+ %swatch = %this.createColorItem(%item, "GuiProfileEditorColorPopup");
+ %swatch.themeField = %field;
+ %swatch.Command = %this.getID() @ ".onColorPopup(" @ %swatch.getID() @ ");";
+
+ %boxCmd = %this.getID() @ ".onColorTyped(" @ %swatch.getID() @ ");";
+ %swatch.redBox.AltCommand = %boxCmd;
+ %swatch.greenBox.AltCommand = %boxCmd;
+ %swatch.blueBox.AltCommand = %boxCmd;
+ %swatch.alphaBox.AltCommand = %boxCmd;
+
+ %this.swatch[%field] = %swatch;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Binding.
+//-----------------------------------------------------------------------------
+
+function ProfileThemeEditForm::bindTheme(%this, %theme)
+{
+ if(!isObject(%theme))
+ {
+ %this.unbind();
+ return;
+ }
+
+ %this.theme = %theme;
+
+ // Setting control values fires the same change events a user edit would;
+ // the flag keeps those from writing back into the theme mid-populate.
+ %this.populating = true;
+
+ %this.nameLabel.setText("Theme: " @ %theme.getName());
+ %this.borderSizeBox.setText(%theme.borderSize);
+ %this.fontSizeBox.setText(%theme.fontSize);
+
+ %this.rebuildFontDropdowns();
+
+ %count = getWordCount(%this.colorFields);
+ for(%i = 0; %i < %count; %i++)
+ {
+ %field = getWord(%this.colorFields, %i);
+ %swatch = %this.swatch[%field];
+ if(!isObject(%swatch))
+ {
+ continue;
+ }
+ %this.showColor(%swatch, %theme.getFieldValue(%field));
+ }
+
+ %this.populating = false;
+}
+
+function ProfileThemeEditForm::unbind(%this)
+{
+ %this.theme = "";
+}
+
+// The single write path: assign the field (which restamps the theme's
+// members), mark the theme dirty, and refresh the preview.
+function ProfileThemeEditForm::applyField(%this, %field, %value)
+{
+ if(%this.populating || !isObject(%this.theme))
+ {
+ return;
+ }
+ %this.theme.setFieldValue(%field, %value);
+ %this.dialog.library.markDirty(%this.theme);
+ // Defer the preview rebuild: this can fire on blur while another control is
+ // being clicked, and rebuilding synchronously would free that control mid
+ // event. See GuiProfileEditorDialog::schedulePreviewRefresh.
+ %this.dialog.schedulePreviewRefresh();
+}
+
+//-----------------------------------------------------------------------------
+// Text-field commits (fired on blur via AltCommand, and on Enter via the
+// EditorForm ReturnPressed event).
+//-----------------------------------------------------------------------------
+
+function ProfileThemeEditForm::commitBorderSize(%this)
+{
+ if(%this.populating || !isObject(%this.theme))
+ {
+ return;
+ }
+ %v = mFloor(%this.borderSizeBox.getText());
+ if(%v < 0)
+ {
+ %v = 0;
+ }
+ %this.borderSizeBox.setText(%v);
+ %this.applyField("borderSize", %v);
+}
+
+function ProfileThemeEditForm::commitFontSize(%this)
+{
+ if(%this.populating || !isObject(%this.theme))
+ {
+ return;
+ }
+ %v = mFloor(%this.fontSizeBox.getText());
+ if(%v < 1)
+ {
+ %v = 1;
+ }
+ %this.fontSizeBox.setText(%v);
+ %this.applyField("fontSize", %v);
+}
+
+function ProfileThemeEditForm::onReturnPressed(%this, %ctrl)
+{
+ if(%ctrl == %this.borderSizeBox)
+ {
+ %this.commitBorderSize();
+ }
+ else if(%ctrl == %this.fontSizeBox)
+ {
+ %this.commitFontSize();
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Color commits. The swatch and its four numeric boxes are kept in sync; the
+// swatch (not string math) is the single source of truth for the value.
+//-----------------------------------------------------------------------------
+
+function ProfileThemeEditForm::onColorPopup(%this, %swatch)
+{
+ if(%this.populating)
+ {
+ return;
+ }
+ %ci = %swatch.getColorI();
+ %this.setColorBoxes(%swatch, %ci);
+ %this.applyField(%swatch.themeField, %ci);
+}
+
+function ProfileThemeEditForm::onColorTyped(%this, %swatch)
+{
+ if(%this.populating)
+ {
+ return;
+ }
+ %r = mClamp(mRound(%swatch.redBox.getText()), 0, 255);
+ %g = mClamp(mRound(%swatch.greenBox.getText()), 0, 255);
+ %b = mClamp(mRound(%swatch.blueBox.getText()), 0, 255);
+ %a = mClamp(mRound(%swatch.alphaBox.getText()), 0, 255);
+ %ci = %r SPC %g SPC %b SPC %a;
+
+ %swatch.setColorI(%ci);
+ %this.setColorBoxes(%swatch, %ci);
+ %this.applyField(%swatch.themeField, %ci);
+}
+
+function ProfileThemeEditForm::setColorBoxes(%this, %swatch, %ci)
+{
+ %swatch.redBox.setText(getWord(%ci, 0));
+ %swatch.greenBox.setText(getWord(%ci, 1));
+ %swatch.blueBox.setText(getWord(%ci, 2));
+ %swatch.alphaBox.setText(getWord(%ci, 3));
+}
+
+// Display a color value on the swatch and its boxes. setColorI wants four
+// integers; a stock color name (e.g. "White") comes back from a ColorI field as
+// a single token, so route that through the baseColor field, which parses named
+// colors. (The C++ setField the native inspector uses is not a script method,
+// so we must handle both forms here - this is what makes the initial values
+// show correctly instead of the swatch's default grey.)
+function ProfileThemeEditForm::showColor(%this, %swatch, %value)
+{
+ if(getWordCount(%value) >= 4)
+ {
+ %swatch.setColorI(%value);
+ }
+ else
+ {
+ %swatch.baseColor = %value;
+ }
+ %this.setColorBoxes(%swatch, %swatch.getColorI());
+}
+
+//-----------------------------------------------------------------------------
+// Font selection. The enumeration, drop-down filling and cache baking all live
+// on the shared library (GuiProfileEditorLibrary) because the profile pane picks
+// a face exactly the way a theme does.
+//-----------------------------------------------------------------------------
+
+function ProfileThemeEditForm::onDropDownSelect(%this, %ctrl)
+{
+ if(%this.populating || %ctrl.themeField $= "")
+ {
+ return;
+ }
+ %face = %ctrl.getText();
+ %this.applyField(%ctrl.themeField, %face);
+}
+
+function ProfileThemeEditForm::rebuildFontDropdowns(%this)
+{
+ if(!isObject(%this.theme))
+ {
+ return;
+ }
+ // One list for all three roles: the machine's installed fonts plus anything
+ // the project already has a cache for. Each drop-down adds its own face if it
+ // is in neither -- a theme that arrived with its caches may name a font that
+ // isn't installed here, and it must stay selected.
+ %library = %this.dialog.library;
+ %library.fillFontDropdown(%this.fontTitleDrop, %library.getFontFaceList(%this.theme.fontTitle), %this.theme.fontTitle);
+ %library.fillFontDropdown(%this.fontBodyDrop, %library.getFontFaceList(%this.theme.fontBody), %this.theme.fontBody);
+ %library.fillFontDropdown(%this.fontCodeDrop, %library.getFontFaceList(%this.theme.fontCode), %this.theme.fontCode);
+}
+
+// Font caches are not baked while the theme is being edited: the preview
+// renders a newly chosen face straight from the platform font, and baking one
+// costs seconds. GuiProfileEditorLibrary::bakeFontsFor does it on Save.
diff --git a/editor/ProjectManager/scripts/NewModuleDialog.cs b/editor/ProjectManager/scripts/NewModuleDialog.cs
index 9133ff9ec..4d7e45cd8 100644
--- a/editor/ProjectManager/scripts/NewModuleDialog.cs
+++ b/editor/ProjectManager/scripts/NewModuleDialog.cs
@@ -61,17 +61,35 @@ class = "EditorForm";
%allModules = %manager.findModules(false);
+ // The dropdown shows a display name, and sortByText reorders it, so the id a
+ // name belongs to is remembered against the name rather than by position. The
+ // manager and its module definitions are gone by the time a choice is made.
for(%i = 0; %i < getWordCount(%allModules); %i++)
{
%mod = getWord(%allModules, %i);
- if(%mod.type $= "template")
+ if(%mod.Template)
{
- %this.templateDropDown.addItem(%mod.ModuleID);
+ %name = ModuleStamper.displayName(%mod);
+ %this.templateDropDown.addItem(%name);
+ %this.templateID[%name] = %mod.ModuleID;
}
}
%this.templateDropDown.sortByText();
%this.templateDropDown.insertItem(0, "none");
%this.templateDropDown.setSelected(0);
+
+ %manager.delete();
+}
+
+function NewModuleDialog::getSelectedTemplate(%this)
+{
+ %name = %this.templateDropDown.getText();
+ if(%name $= "none" || %this.templateID[%name] $= "")
+ {
+ return "none";
+ }
+
+ return %this.templateID[%name];
}
function NewModuleDialog::onDropDownClosed(%this, %dropDown)
@@ -93,7 +111,6 @@ class = "EditorForm";
{
%this.createButton.active = false;
- %module = %this.templateDropDown.getText();
%name = %this.moduleNameBox.getText();
%path = pathConcat(getMainDotCsDir(), ProjectManager.getProjectFolder(), %name);
@@ -112,7 +129,7 @@ class = "EditorForm";
{
if(%this.validate())
{
- %module = %this.templateDropDown.getText();
+ %module = %this.getSelectedTemplate();
%name = %this.moduleNameBox.getText();
%path = pathConcat(getMainDotCsDir(), ProjectManager.getProjectFolder(), %name);
diff --git a/editor/ProjectManager/scripts/ProjectGamePanel.cs b/editor/ProjectManager/scripts/ProjectGamePanel.cs
index 6df552123..f487ecaa2 100644
--- a/editor/ProjectManager/scripts/ProjectGamePanel.cs
+++ b/editor/ProjectManager/scripts/ProjectGamePanel.cs
@@ -3,8 +3,8 @@
{
%this.init("Project");
- %this.buttonBar.addButton("createNewModule", 11, "Create Module", "");
- %this.buttonBar.addButton("editModule", 49, "Edit Module", "editModuleAvailable");
+ %this.buttonBar.addButton("createNewModule", $EditorIcon::doc_plus, "Create Module", "");
+ %this.buttonBar.addButton("editModule", $EditorIcon::doc_edit, "Edit Module", "editModuleAvailable");
}
function ProjectGamePanel::onOpen(%this, %allModules)
@@ -98,10 +98,23 @@ class = "NewModuleDialog";
if(isDirectory(%templatePath))
{
pathCopy(%templatePath, %data.path);
+
+ // The id is in the template's script and asset ids too, not just in
+ // its module.taml, and the engine calls ::.
+ // Rewriting only the definition leaves a module that loads and then
+ // does nothing.
+ ModuleStamper.renameInPlace(%data.path, %data.template, %data.moduleName);
+
%obj = TamlRead(pathConcat(%data.path, "module.taml"));
%obj.ModuleID = %data.moduleName;
+
+ // A copy of a template is a module of its own: it is not something to
+ // stamp out again, and it is not a Game Core or an Art Pack either.
+ ModuleStamper.clearTemplateMarkers(%obj);
%obj.Type = "";
+
TamlWrite(%obj, pathConcat(%data.path, "module.taml"));
+ %obj.delete();
}
}
else
@@ -117,6 +130,7 @@ class = "NewModuleDialog";
};
createPath(%data.path);
TamlWrite(%obj, pathConcat(%data.path, "module.taml"));
+ %obj.delete();
}
ModuleDatabase.scanModules(%data.path);
%this.onOpen(ModuleDatabase.findModules(false));
@@ -177,6 +191,12 @@ class = "EditModuleDialog";
{
directoryDelete(%modulePath);
%modulePath = %newModulePath;
+
+ // The old id is written through the module's own scripts and asset
+ // ids as well, and the engine calls ::.
+ // Renaming the folder and the definition alone would leave a module
+ // that loads and then does nothing.
+ ModuleStamper.renameInPlace(%modulePath, %moduleID, %data.moduleID);
}
}
echo("Editing Module at " @ %modulePath);
@@ -188,6 +208,7 @@ class = "EditModuleDialog";
%file.type = %data.type;
%file.author = %data.author;
TamlWrite(%file, pathConcat(%modulePath, "module.taml"));
+ %file.delete();
ModuleDatabase.scanModules(%modulePath, true);
%this.card.moduleID = %data.moduleID;
%this.card.versionID = %data.versionID;
diff --git a/editor/ProjectManager/scripts/ProjectLibraryPanel.cs b/editor/ProjectManager/scripts/ProjectLibraryPanel.cs
index 393eab37a..3e9eef41c 100644
--- a/editor/ProjectManager/scripts/ProjectLibraryPanel.cs
+++ b/editor/ProjectManager/scripts/ProjectLibraryPanel.cs
@@ -24,7 +24,9 @@
function ProjectLibraryPanel::addModule(%this, %module)
{
- if(%module.type !$= "Template")
+ // Template modules are stamped out into a project by New Module or New
+ // Project, not installed alongside it, so they do not belong in this list.
+ if(!%module.Template)
{
%this.list.addItemWithID(%this.getModuleName(%module), %module);
}
diff --git a/editor/main.cs b/editor/main.cs
index 3f02a1945..b9aafb3b1 100644
--- a/editor/main.cs
+++ b/editor/main.cs
@@ -38,3 +38,27 @@
EditorCore.open();
EditorCore.showProjectSelector();
}
+
+//-----------------------------------------------------------------------------
+
+// The engine calls onPreExit ahead of onExit (see shutdownGame in
+// game/defaultGame.cc), while everything the editor owns is still alive.
+//
+// Unload exactly what was loaded above, in reverse. ModuleManager resolves each
+// module's dependencies and calls the DestroyFunctions in reverse order, so every
+// editor module gets to clean up after itself, and EditorCore -- which the four
+// above pull in as a shared dependency rather than loading themselves -- unloads
+// last, when its load count finally drops to zero.
+//
+// This lives beside the loads, in the editor's own boot script, so the two lists
+// stay in step and any project shipping its own main.cs inherits the teardown.
+function onPreExit()
+{
+ if(isObject(EditorManager))
+ {
+ EditorManager.unloadExplicit("GuiEditor");
+ EditorManager.unloadExplicit("AssetAdmin");
+ EditorManager.unloadExplicit("ProjectManager");
+ EditorManager.unloadExplicit("EditorConsole");
+ }
+}
diff --git a/engine/compilers/Make-32bit/Dockerfile b/engine/compilers/Make-32bit/Dockerfile
deleted file mode 100644
index a9141d406..000000000
--- a/engine/compilers/Make-32bit/Dockerfile
+++ /dev/null
@@ -1,14 +0,0 @@
-FROM ubuntu:20.04
-ARG DEBIAN_FRONTEND=noninteractive
-RUN dpkg --add-architecture i386 &&\
- apt-get update && \
- apt-get -y install \
- build-essential \
- gcc-multilib \
- g++-multilib \
- nasm \
- libsdl-dev:i386 \
- libxft-dev:i386 \
- libopenal-dev:i386 && \
- rm -rf /var/lib/{apt,dpkg,cache,log}/
-RUN mkdir /torque2d-engine-build/
diff --git a/engine/compilers/Make-32bit/Makefile b/engine/compilers/Make-32bit/Makefile
deleted file mode 100644
index 4154e3ba3..000000000
--- a/engine/compilers/Make-32bit/Makefile
+++ /dev/null
@@ -1,45 +0,0 @@
-DEPS :=
-LIB_TARGETS :=
-LIB_TARGETS_DEBUG :=
-SHARED_LIB_TARGETS :=
-SHARED_LIB_TARGETS_DEBUG :=
-APP_TARGETS :=
-APP_TARGETS_DEBUG :=
-
-build-in-docker: docker-buildenv
- docker run \
- --rm \
- --user $(shell id -u):$(shell id -g) \
- -v $(shell readlink -e ../../../ ):/torque2d-engine-build/ \
- -w /torque2d-engine-build/engine/compilers/Make-32bit/ \
- torque2d-linux32-build-env \
- make -j all
-
-all: debug release
-
-docker-buildenv: Dockerfile
- docker build -t torque2d-linux32-build-env .
-
-clean:
- rm -rf Debug
- rm -rf Release
- rm -rf lib
-
-.PHONY: all debug release clean
-
--include x Torque2D.mk
--include x zlib
--include x lpng
--include x ljpeg
--include x vorbis
--include x ogg
-
-release: $(LIB_TARGETS) $(SHARED_LIB_TARGETS) $(APP_TARGETS)
- @echo Built libraries: $(LIB_TARGETS)
- @echo Built shared libraries: $(SHARED_LIB_TARGETS)
- @echo Built apps: $(APP_TARGETS)
-
-debug: $(LIB_TARGETS_DEBUG) $(SHARED_LIB_TARGETS_DEBUG) $(APP_TARGETS_DEBUG)
- @echo Built libraries: $(LIB_TARGETS_DEBUG)
- @echo Built shared libraries: $(SHARED_LIB_TARGETS_DEBUG)
- @echo Built apps: $(APP_TARGETS_DEBUG)
diff --git a/engine/compilers/Make-32bit/Torque2D.mk b/engine/compilers/Make-32bit/Torque2D.mk
deleted file mode 100644
index f725ae61a..000000000
--- a/engine/compilers/Make-32bit/Torque2D.mk
+++ /dev/null
@@ -1,145 +0,0 @@
-APPNAME := ../../../Torque2D
-
-2D_SOURCES := $(shell find ../../source/2d/ -name "*.cc") + \
- $(shell find ../../source/2d/ -name "*.cpp")
-ALGORITHM_SOURCES := $(shell find ../../source/algorithm/ -name "*.cc") + \
- $(shell find ../../source/algorithm/ -name "*.c")
-ASSETS_SOURCES := $(shell find ../../source/assets/ -name "*.cc")
-AUDIO_SOURCES := $(shell find ../../source/audio/ -name "*.cc")
-BITMAPFONT_SOURCES := $(shell find ../../source/bitmapFont/ -name "*.cc")
-BOX2D_SOURCES := $(shell find ../../source/Box2D/ -name "*.cpp")
-COLLECTION_SOURCES := $(shell find ../../source/collection/ -name "*.cc")
-COMPONENT_SOURCES := $(shell find ../../source/component/ -name "*.cpp")
-CONSOLE_SOURCES := $(shell find ../../source/console/ -name "*.cc")
-DEBUG_SOURCES := $(shell find ../../source/debug/ -name "*.cc")
-DELEGATES_SOURCES := $(shell find ../../source/delegates/ -name "*.cc")
-GAME_SOURCES := $(shell find ../../source/game/ -name "*.cc")
-GRAPHICS_SOURCES := $(shell find ../../source/graphics/ -name "*.cc")
-GUI_SOURCES := $(shell find ../../source/gui/ -name "*.cc")
-INPUT_SOURCES := $(shell find ../../source/input/ -name "*.cc")
-IO_SOURCES := $(shell find ../../source/io/ -name "*.cc")
-MATH_SOURCES := $(shell find ../../source/math/ -name "*.cc") + \
- $(shell find ../../source/math/ -name "*.cpp") + \
- $(shell find ../../source/math/ -name "*.asm")
-MEMORY_SOURCES := $(shell find ../../source/memory/ -name "*.cc")
-MESSAGING_SOURCES := $(shell find ../../source/messaging/ -name "*.cc")
-MODULE_SOURCES := $(shell find ../../source/module/ -name "*.cc")
-NETWORK_SOURCES := $(shell find ../../source/network/ -name "*.cc")
-PERSISTENCE_SOURCES := $(shell find ../../source/persistence/ -name "*.cc") + \
- $(shell find ../../source/persistence/ -name "*.cpp")
-PLATFORM_SOURCES := $(shell find ../../source/platform/ -name "*.cc") + \
- $(shell find ../../source/platform/ -name "*.cpp") + \
- $(shell find ../../source/platform/ -name "*.asm")
-PLATFORM_UNIX_SOURCES := $(shell find ../../source/platformX86UNIX/ -name "*.cc")
-SIM_SOURCES := $(shell find ../../source/sim/ -name "*.cc") + \
- $(shell find ../../source/sim/ -name "*.cpp")
-STRING_SOURCES := $(shell find ../../source/string/ -name "*.cc") + \
- $(shell find ../../source/string/ -name "*.cpp")
-
-SOURCES := $(2D_SOURCES) + \
- $(ALGORITHM_SOURCES) + \
- $(ASSETS_SOURCES) + \
- $(AUDIO_SOURCES) + \
- $(BITMAPFONT_SOURCES) + \
- $(BOX2D_SOURCES) + \
- $(COLLECTION_SOURCES) + \
- $(COMPONENT_SOURCES) + \
- $(CONSOLE_SOURCES) + \
- $(DEBUG_SOURCES) + \
- $(DELEGATES_SOURCES) + \
- $(GAME_SOURCES) + \
- $(GRAPHICS_SOURCES) + \
- $(GUI_SOURCES) + \
- $(INPUT_SOURCES) + \
- $(IO_SOURCES) + \
- $(MATH_SOURCES) + \
- $(MEMORY_SOURCES) + \
- $(MESSAGING_SOURCES) + \
- $(MODULE_SOURCES) + \
- $(NETWORK_SOURCES) + \
- $(PERSISTENCE_SOURCES) + \
- $(PLATFORM_SOURCES) + \
- $(PLATFORM_UNIX_SOURCES) + \
- $(SIM_SOURCES) + \
- $(STRING_SOURCES)
-
-LDFLAGS := -g -m32
-LDLIBS := -lstdc++ -lm -ldl -lpthread -lrt -lX11 -lXft -lSDL -lopenal
-
-CFLAGS := -std=c++14 -MMD -I. -Wfatal-errors -Wunused -m32 -msse -march=i686 -pipe
-
-CFLAGS += -I/usr/include
-CFLAGS += -I/usr/include/freetype2
-CFLAGS += -I../../source
-CFLAGS += -I../../source/persistence/rapidjson/include
-CFLAGS += -I../../lib/ljpeg
-CFLAGS += -I../../lib/zlib
-CFLAGS += -I../../lib/lpng
-CFLAGS += -I../../lib/freetype
-CFLAGS += -I../../lib/libvorbis/include
-CFLAGS += -I../../lib/libogg/include
-CFLAGS += -I../../lib/openal/LINUX/
-
-CFLAGS += -DLINUX
-CFLAGS += -Di386
-
-
-CFLAGS_DEBUG := $(CFLAGS) -ggdb
-CFLAGS_DEBUG += -DTORQUE_DEBUG
-CFLAGS_DEBUG += -DTORQUE_DEBUG_GUARD
-CFLAGS_DEBUG += -DTORQUE_NET_STATS
-
-CFLAGS += -Og
-
-NASMFLAGS := -f elf -D LINUX
-
-CC := gcc
-LD := gcc
-
-APP_TARGETS += $(APPNAME)
-APP_TARGETS_DEBUG += $(APPNAME)_DEBUG
-
-OBJS := $(patsubst ../../source/%,Release/%.o,$(SOURCES))
-OBJS := $(filter %.o, $(OBJS))
-
-OBJS_DEBUG := $(patsubst ../../source/%,Debug/%.o,$(SOURCES))
-OBJS_DEBUG := $(filter %.o,$(OBJS_DEBUG))
-
-$(APP_TARGETS): $(OBJS) $(LIB_TARGETS)
- @echo Linking release
- $(LD) $(LDFLAGS) -o $@ $(OBJS) $(LIB_TARGETS) $(LDLIBS)
-
-$(APP_TARGETS_DEBUG): $(OBJS_DEBUG) $(LIB_TARGETS_DEBUG)
- @echo Linking debug
- $(LD) $(LDFLAGS) -o $@ $(OBJS_DEBUG) $(LIB_TARGETS_DEBUG) $(LDLIBS)
-
-Release/%.asm.o: ../../source/%.asm
- @echo Building release asm $@
- @mkdir -p $(dir $@)
- nasm $(NASMFLAGS) $< -o $@
-
-Release/%.o: ../../source/%
- @echo Building release object $@
- @mkdir -p $(dir $@)
- $(CC) -c $(CFLAGS) $< -o $@
-
-Debug/%.asm.o: ../../source/%.asm
- @echo Building debug asm $@
- @mkdir -p $(dir $@)
- nasm $(NASMFLAGS) $< -o $@
-
-Debug/%.o: ../../source/%
- @echo Building debug object $@
- @mkdir -p $(dir $@)
- $(CC) -c $(CFLAGS_DEBUG) $< -o $@
-
-release: $(APP_TARGETS)
-debug: $(APP_TARGETS_DEBUG)
-
-.PHONY: $(APP_TARGETS) $(APP_TARGETS_DEBUG)
-
-DEPS += $(patsubst %.o,%.d,$(OBJS))
-DEPS += $(patsubst %.o,%.d,$(OBJS_DEBUG))
-
-APPNAME :=
-SOURCES :=
diff --git a/engine/compilers/Make-32bit/ljpeg b/engine/compilers/Make-32bit/ljpeg
deleted file mode 100644
index b6965e5c9..000000000
--- a/engine/compilers/Make-32bit/ljpeg
+++ /dev/null
@@ -1,109 +0,0 @@
-# I release this sample under the MIT license: free for any use, provided
-# you hold me harmless from any such use you make, and you retain my
-# copyright on the actual sources.
-# Copyright 2005 Jon Watte.
-
-LIBNAME := ljpeg
-SOURCES := ../../lib/ljpeg/jidctint.c \
-../../lib/ljpeg/jcmainct.c \
-../../lib/ljpeg/jfdctfst.c \
-../../lib/ljpeg/jdatadst.c \
-../../lib/ljpeg/jdsample.c \
-../../lib/ljpeg/jmemmgr.c \
-../../lib/ljpeg/jidctred.c \
-../../lib/ljpeg/jcphuff.c \
-../../lib/ljpeg/jchuff.c \
-../../lib/ljpeg/jdapistd.c \
-../../lib/ljpeg/jdpostct.c \
-../../lib/ljpeg/jquant2.c \
-../../lib/ljpeg/jdmerge.c \
-../../lib/ljpeg/jfdctflt.c \
-../../lib/ljpeg/jcprepct.c \
-../../lib/ljpeg/jccolor.c \
-../../lib/ljpeg/jfdctint.c \
-../../lib/ljpeg/jdhuff.c \
-../../lib/ljpeg/jcomapi.c \
-../../lib/ljpeg/jcinit.c \
-../../lib/ljpeg/jccoefct.c \
-../../lib/ljpeg/jdinput.c \
-../../lib/ljpeg/jutils.c \
-../../lib/ljpeg/jcapimin.c \
-../../lib/ljpeg/jdcoefct.c \
-../../lib/ljpeg/jidctflt.c \
-../../lib/ljpeg/jcmaster.c \
-../../lib/ljpeg/jddctmgr.c \
-../../lib/ljpeg/jidctfst.c \
-../../lib/ljpeg/jcparam.c \
-../../lib/ljpeg/jcapistd.c \
-../../lib/ljpeg/jdmaster.c \
-../../lib/ljpeg/jcdctmgr.c \
-../../lib/ljpeg/jctrans.c \
-../../lib/ljpeg/jdmainct.c \
-../../lib/ljpeg/jdtrans.c \
-../../lib/ljpeg/jcsample.c \
-../../lib/ljpeg/jdmarker.c \
-../../lib/ljpeg/jdatasrc.c \
-../../lib/ljpeg/jerror.c \
-../../lib/ljpeg/jquant1.c \
-../../lib/ljpeg/jdphuff.c \
-../../lib/ljpeg/jcmarker.c \
-../../lib/ljpeg/jdapimin.c \
-../../lib/ljpeg/jdcolor.c \
-../../lib/ljpeg/jmemnobs.c \
-
-LDFLAGS_ljpeg := -g -m32
-
-CFLAGS_ljpeg := -MMD -I. -m32 -msse -mmmx -march=i686
-
-CFLAGS_ljpeg += -I../../lib/ljpeg
-
-CFLAGS_ljpeg += -DUNICODE
-CFLAGS_ljpeg += -DLINUX
-
-CFLAGS_DEBUG_ljpeg := $(CFLAGS_ljpeg) -ggdb
-CFLAGS_DEBUG_ljpeg += -DTORQUE_DEBUG
-CFLAGS_DEBUG_ljpeg += -DTORQUE_DEBUG_GUARD
-CFLAGS_DEBUG_ljpeg += -DTORQUE_NET_STATS
-
-CFLAGS_ljpeg += -O3
-
-CC := gcc
-LD := gcc
-
-TARGET_ljpeg := lib/ljpeg.a
-TARGET_ljpeg_DEBUG := lib/ljpeg_DEBUG.a
-
-LIB_TARGETS += $(TARGET_ljpeg)
-LIB_TARGETS_DEBUG += $(TARGET_ljpeg_DEBUG)
-
-OBJS_ljpeg := $(patsubst ../../lib/ljpeg/%,Release/ljpeg/%.o,$(SOURCES))
-OBJS_ljpeg_DEBUG := $(patsubst ../../lib/ljpeg/%,Debug/ljpeg/%.o,$(SOURCES))
-
-# Deriving the variable name from the target name is the secret sauce
-# of the build system.
-#
-$(TARGET_ljpeg): $(OBJS_ljpeg)
- @echo Linking library ljpng
- @mkdir -p $(dir $@)
- ar cr $@ $(OBJS_ljpeg)
-
-$(TARGET_ljpeg_DEBUG): $(OBJS_ljpeg_DEBUG)
- @echo Linking debug library ljpng
- @mkdir -p $(dir $@)
- ar cr $@ $(OBJS_ljpeg_DEBUG)
-
-Release/ljpeg/%.o: ../../lib/ljpeg/%
- @mkdir -p $(dir $@)
- $(CC) -c $(CFLAGS_ljpeg) $< -o $@
-
-Debug/ljpeg/%.o: ../../lib/ljpeg/%
- @mkdir -p $(dir $@)
- $(CC) -c $(CFLAGS_DEBUG_ljpeg) $< -o $@
-
-release_ljpeg: $(TARGET_ljpeg)
-debug_ljpeg: $(TARGET_ljpeg_DEBUG)
-
-.PHONY: debug_ljpeg release_ljpeg
-
-DEPS += $(patsubst %.o,%.d,$(OBJS_ljpeg))
-DEPS += $(patsubst %.o,%.d,$(OBJS_ljpeg_DEBUG))
diff --git a/engine/compilers/Make-32bit/lpng b/engine/compilers/Make-32bit/lpng
deleted file mode 100644
index d16afd9cd..000000000
--- a/engine/compilers/Make-32bit/lpng
+++ /dev/null
@@ -1,78 +0,0 @@
-# I release this sample under the MIT license: free for any use, provided
-# you hold me harmless from any such use you make, and you retain my
-# copyright on the actual sources.
-# Copyright 2005 Jon Watte.
-
-LIBNAME := lpng
-SOURCES := ../../lib/lpng/pngerror.c \
-../../lib/lpng/pngwrite.c \
-../../lib/lpng/pngread.c \
-../../lib/lpng/pngmem.c \
-../../lib/lpng/pngset.c \
-../../lib/lpng/pngwio.c \
-../../lib/lpng/pngrtran.c \
-../../lib/lpng/pngtrans.c \
-../../lib/lpng/pngrutil.c \
-../../lib/lpng/pngwtran.c \
-../../lib/lpng/png.c \
-../../lib/lpng/pngrio.c \
-../../lib/lpng/pngwutil.c \
-../../lib/lpng/pngget.c \
-../../lib/lpng/pngpread.c \
-
-LDFLAGS_lpng := -g -m32
-#LDLIBS_lpng := -lstdc++
-CFLAGS_lpng := -MMD -I. -m32 -msse -mmmx -march=i686
-
-CFLAGS_lpng += -I../../lib/zlib
-CFLAGS_lpng += -I../../lib/lpng
-
-CFLAGS_lpng += -DUNICODE
-CFLAGS_lpng += -DLINUX
-
-
-CFLAGS_DEBUG_lpng := $(CFLAGS_lpng) -ggdb
-CFLAGS_DEBUG_lpng += -DTORQUE_DEBUG
-CFLAGS_DEBUG_lpng += -DTORQUE_DEBUG_GUARD
-CFLAGS_DEBUG_lpng += -DTORQUE_NET_STATS
-
-CFLAGS_lpng += -O3
-
-CC := gcc
-LD := gcc
-
-TARGET_lpng := lib/lpng.a
-TARGET_lpng_DEBUG := lib/lpng_DEBUG.a
-
-LIB_TARGETS += $(TARGET_lpng)
-LIB_TARGETS_DEBUG += $(TARGET_lpng_DEBUG)
-
-OBJS_lpng := $(patsubst ../../lib/lpng/%,Release/lpng/%.o,$(SOURCES))
-OBJS_lpng_DEBUG := $(patsubst ../../lib/lpng/%,Debug/lpng/%.o,$(SOURCES))
-
-# Deriving the variable name from the target name is the secret sauce
-# of the build system.
-#
-$(TARGET_lpng): $(OBJS_lpng)
- @mkdir -p $(dir $@)
- ar cr $@ $(OBJS_lpng)
-
-$(TARGET_lpng_DEBUG): $(OBJS_lpng_DEBUG)
- @mkdir -p $(dir $@)
- ar cr $@ $(OBJS_lpng_DEBUG)
-
-Release/lpng/%.o: ../../lib/lpng/%
- @mkdir -p $(dir $@)
- $(CC) -c $(CFLAGS_lpng) $< -o $@
-
-Debug/lpng/%.o: ../../lib/lpng/%
- @mkdir -p $(dir $@)
- $(CC) -c $(CFLAGS_DEBUG_lpng) $< -o $@
-
-release_lpng: $(TARGET_lpng)
-debug_lpng: $(TARGET_lpng_DEBUG)
-
-.PHONY: debug_lpng release_lpng
-
-DEPS += $(patsubst %.o,%.d,$(OBJS_lpng))
-DEPS += $(patsubst %.o,%.d,$(OBJS_lpng_DEBUG))
diff --git a/engine/compilers/Make-32bit/ogg b/engine/compilers/Make-32bit/ogg
deleted file mode 100644
index a7b0449c0..000000000
--- a/engine/compilers/Make-32bit/ogg
+++ /dev/null
@@ -1,64 +0,0 @@
-# I release this sample under the MIT license: free for any use, provided
-# you hold me harmless from any such use you make, and you retain my
-# copyright on the actual sources.
-# Copyright 2005 Jon Watte.
-
-LIBNAME := ogg
-SOURCES := \
-../../lib/libogg/src/bitwise.c \
-../../lib/libogg/src/framing.c \
-
-LDFLAGS_ogg := -g -m32
-
-CFLAGS_ogg := -MMD -I. -m32 -msse -mmmx -march=i686
-
-CFLAGS_ogg += -I../../lib/libogg/include
-
-CFLAGS_ogg += -DUNICODE
-CFLAGS_ogg += -DLINUX
-
-CFLAGS_DEBUG_ogg := $(CFLAGS_ogg) -ggdb
-CFLAGS_DEBUG_ogg += -DTORQUE_DEBUG
-CFLAGS_DEBUG_ogg += -DTORQUE_DEBUG_GUARD
-CFLAGS_DEBUG_ogg += -DTORQUE_NET_STATS
-
-CFLAGS_ogg += -O3
-
-CC := gcc
-LD := gcc
-
-TARGET_ogg := lib/libogg.a
-TARGET_ogg_DEBUG := lib/libogg_DEBUG.a
-
-LIB_TARGETS += $(TARGET_ogg)
-LIB_TARGETS_DEBUG += $(TARGET_ogg_DEBUG)
-
-OBJS_ogg := $(patsubst ../../lib/libogg/%,Release/ogg/%.o,$(SOURCES))
-OBJS_ogg_DEBUG := $(patsubst ../../lib/libogg/%,Debug/ogg/%.o,$(SOURCES))
-
-# Deriving the variable name from the target name is the secret sauce
-# of the build system.
-#
-$(TARGET_ogg): $(OBJS_ogg)
- @mkdir -p $(dir $@)
- ar cr $@ $(OBJS_ogg)
-
-$(TARGET_ogg_DEBUG): $(OBJS_ogg_DEBUG)
- @mkdir -p $(dir $@)
- ar cr $@ $(OBJS_ogg_DEBUG)
-
-Release/ogg/%.o: ../../lib/libogg/%
- @mkdir -p $(dir $@)
- $(CC) -c $(CFLAGS_ogg) $< -o $@
-
-Debug/ogg/%.o: ../../lib/libogg/%
- @mkdir -p $(dir $@)
- $(CC) -c $(CFLAGS_DEBUG_ogg) $< -o $@
-
-release_ogg: $(TARGET_ogg)
-debug_ogg: $(TARGET_ogg_DEBUG)
-
-.PHONY: debug_ogg release_ogg
-
-DEPS += $(patsubst %.o,%.d,$(OBJS_ogg))
-DEPS += $(patsubst %.o,%.d,$(OBJS_ogg_DEBUG))
diff --git a/engine/compilers/Make-32bit/vorbis b/engine/compilers/Make-32bit/vorbis
deleted file mode 100644
index fc626d9f4..000000000
--- a/engine/compilers/Make-32bit/vorbis
+++ /dev/null
@@ -1,89 +0,0 @@
-# I release this sample under the MIT license: free for any use, provided
-# you hold me harmless from any such use you make, and you retain my
-# copyright on the actual sources.
-# Copyright 2005 Jon Watte.
-
-LIBNAME := vorbis
-SOURCES := \
-../../lib/libvorbis/analysis.c \
-../../lib/libvorbis/barkmel.c \
-../../lib/libvorbis/bitrate.c \
-../../lib/libvorbis/block.c \
-../../lib/libvorbis/codebook.c \
-../../lib/libvorbis/envelope.c \
-../../lib/libvorbis/floor0.c \
-../../lib/libvorbis/floor1.c \
-../../lib/libvorbis/info.c \
-../../lib/libvorbis/lookup.c \
-../../lib/libvorbis/lpc.c \
-../../lib/libvorbis/lsp.c \
-../../lib/libvorbis/mapping0.c \
-../../lib/libvorbis/mdct.c \
-../../lib/libvorbis/psy.c \
-../../lib/libvorbis/registry.c \
-../../lib/libvorbis/res0.c \
-../../lib/libvorbis/sharedbook.c \
-../../lib/libvorbis/smallft.c \
-../../lib/libvorbis/synthesis.c \
-../../lib/libvorbis/tone.c \
-../../lib/libvorbis/vorbisenc.c \
-../../lib/libvorbis/vorbisfile.c \
-../../lib/libvorbis/window.c \
-
-LDFLAGS_vorbis := -g -m32
-
-CFLAGS_vorbis := -MMD -I. -m32 -msse -mmmx -march=i686
-
-CFLAGS_vorbis += -I../../lib/libvorbis
-CFLAGS_vorbis += -I../../lib/libvorbis/lib
-CFLAGS_vorbis += -I../../lib/libvorbis/include
-CFLAGS_vorbis += -I../../lib/libogg/include
-
-CFLAGS_vorbis += -DUNICODE
-CFLAGS_vorbis += -DLINUX
-
-CFLAGS_DEBUG_vorbis := $(CFLAGS_vorbis) -ggdb
-CFLAGS_DEBUG_vorbis += -DTORQUE_DEBUG
-CFLAGS_DEBUG_vorbis += -DTORQUE_DEBUG_GUARD
-CFLAGS_DEBUG_vorbis += -DTORQUE_NET_STATS
-
-CFLAGS_vorbis += -O3
-
-CC := gcc
-LD := gcc
-
-TARGET_vorbis := lib/libvorbis.a
-TARGET_vorbis_DEBUG := lib/libvorbis_DEBUG.a
-
-LIB_TARGETS += $(TARGET_vorbis)
-LIB_TARGETS_DEBUG += $(TARGET_vorbis_DEBUG)
-
-OBJS_vorbis := $(patsubst ../../lib/libvorbis/%,Release/vorbis/%.o,$(SOURCES))
-OBJS_vorbis_DEBUG := $(patsubst ../../lib/libvorbis/%,Debug/vorbis/%.o,$(SOURCES))
-
-# Deriving the variable name from the target name is the secret sauce
-# of the build system.
-#
-$(TARGET_vorbis): $(OBJS_vorbis)
- @mkdir -p $(dir $@)
- ar cr $@ $(OBJS_vorbis)
-
-$(TARGET_vorbis_DEBUG): $(OBJS_vorbis_DEBUG)
- @mkdir -p $(dir $@)
- ar cr $@ $(OBJS_vorbis_DEBUG)
-
-Release/vorbis/%.o: ../../lib/libvorbis/%
- @mkdir -p $(dir $@)
- $(CC) -c $(CFLAGS_vorbis) $< -o $@
-
-Debug/vorbis/%.o: ../../lib/libvorbis/%
- @mkdir -p $(dir $@)
- $(CC) -c $(CFLAGS_DEBUG_vorbis) $< -o $@
-
-release_vorbis: $(TARGET_vorbis)
-debug_vorbis: $(TARGET_vorbis_DEBUG)
-
-.PHONY: debug_vorbis release_vorbis
-
-DEPS += $(patsubst %.o,%.d,$(OBJS_vorbis))
-DEPS += $(patsubst %.o,%.d,$(OBJS_vorbis_DEBUG))
diff --git a/engine/compilers/Make-32bit/zlib b/engine/compilers/Make-32bit/zlib
deleted file mode 100644
index 142fd5a8b..000000000
--- a/engine/compilers/Make-32bit/zlib
+++ /dev/null
@@ -1,76 +0,0 @@
-# I release this sample under the MIT license: free for any use, provided
-# you hold me harmless from any such use you make, and you retain my
-# copyright on the actual sources.
-# Copyright 2005 Jon Watte.
-
-LIBNAME := zlib
-SOURCES := ../../lib/zlib/adler32.c \
-../../lib/zlib/zutil.c \
-../../lib/zlib/crc32.c \
-../../lib/zlib/trees.c \
-../../lib/zlib/inflate.c \
-../../lib/zlib/inftrees.c \
-../../lib/zlib/gzclose.c \
-../../lib/zlib/gzread.c \
-../../lib/zlib/infback.c \
-../../lib/zlib/uncompr.c \
-../../lib/zlib/deflate.c \
-../../lib/zlib/inffast.c \
-../../lib/zlib/gzwrite.c \
-../../lib/zlib/compress.c \
-../../lib/zlib/gzlib.c \
-
-LDFLAGS_zlib := -g -m32
-
-CFLAGS_zlib := -MMD -I. -m32 -msse -mmmx -march=i686
-
-CFLAGS_zlib += -I../../lib/zlib
-
-CFLAGS_zlib += -DUNICODE
-CFLAGS_zlib += -DLINUX
-
-CFLAGS_DEBUG_zlib := $(CFLAGS_zlib) -ggdb
-CFLAGS_DEBUG_zlib += -DTORQUE_DEBUG
-CFLAGS_DEBUG_zlib += -DTORQUE_DEBUG_GUARD
-CFLAGS_DEBUG_zlib += -DTORQUE_NET_STATS
-
-CFLAGS_zlib += -O3
-
-CC := gcc
-LD := gcc
-
-TARGET_zlib := lib/zlib.a
-TARGET_zlib_DEBUG := lib/zlib_DEBUG.a
-
-LIB_TARGETS += $(TARGET_zlib)
-LIB_TARGETS_DEBUG += $(TARGET_zlib_DEBUG)
-
-OBJS_zlib := $(patsubst ../../lib/zlib/%,Release/zlib/%.o,$(SOURCES))
-OBJS_zlib_DEBUG := $(patsubst ../../lib/zlib/%,Debug/zlib/%.o,$(SOURCES))
-
-# Deriving the variable name from the target name is the secret sauce
-# of the build system.
-#
-$(TARGET_zlib): $(OBJS_zlib)
- @mkdir -p $(dir $@)
- ar cr $@ $(OBJS_zlib)
-
-$(TARGET_zlib_DEBUG): $(OBJS_zlib_DEBUG)
- @mkdir -p $(dir $@)
- ar cr $@ $(OBJS_zlib_DEBUG)
-
-Release/zlib/%.o: ../../lib/zlib/%
- @mkdir -p $(dir $@)
- $(CC) -c $(CFLAGS_zlib) $< -o $@
-
-Debug/zlib/%.o: ../../lib/zlib/%
- @mkdir -p $(dir $@)
- $(CC) -c $(CFLAGS_DEBUG_zlib) $< -o $@
-
-release_zlib: $(TARGET_zlib)
-debug_zlib: $(TARGET_zlib_DEBUG)
-
-.PHONY: debug_zlib release_zlib
-
-DEPS += $(patsubst %.o,%.d,$(OBJS_zlib))
-DEPS += $(patsubst %.o,%.d,$(OBJS_zlib_DEBUG))
diff --git a/engine/compilers/Make-64bit/Dockerfile b/engine/compilers/Make-64bit/Dockerfile
deleted file mode 100644
index 083421f60..000000000
--- a/engine/compilers/Make-64bit/Dockerfile
+++ /dev/null
@@ -1,13 +0,0 @@
-FROM ubuntu:20.04
-ARG DEBIAN_FRONTEND=noninteractive
-RUN apt-get update && \
- apt-get -y install \
- build-essential \
- gcc-multilib \
- g++-multilib \
- nasm \
- libsdl-dev \
- libxft-dev \
- libopenal-dev && \
- rm -rf /var/lib/{apt,dpkg,cache,log}/
-RUN mkdir /torque2d-engine-build/
diff --git a/engine/compilers/Make-64bit/Makefile b/engine/compilers/Make-64bit/Makefile
deleted file mode 100644
index 7d5240e40..000000000
--- a/engine/compilers/Make-64bit/Makefile
+++ /dev/null
@@ -1,45 +0,0 @@
-DEPS :=
-LIB_TARGETS :=
-LIB_TARGETS_DEBUG :=
-SHARED_LIB_TARGETS :=
-SHARED_LIB_TARGETS_DEBUG :=
-APP_TARGETS :=
-APP_TARGETS_DEBUG :=
-
-build-in-docker: docker-buildenv
- docker run \
- --rm \
- --user $(shell id -u):$(shell id -g) \
- -v $(shell readlink -e ../../../ ):/torque2d-engine-build/ \
- -w /torque2d-engine-build/engine/compilers/Make-64bit/ \
- torque2d-linux64-build-env \
- make -j all
-
-all: debug release
-
-docker-buildenv: Dockerfile
- docker build -t torque2d-linux64-build-env .
-
-clean:
- rm -rf Debug
- rm -rf Release
- rm -rf lib
-
-.PHONY: all debug release clean
-
--include x Torque2D.mk
--include x zlib
--include x lpng
--include x ljpeg
--include x vorbis
--include x ogg
-
-release: $(LIB_TARGETS) $(SHARED_LIB_TARGETS) $(APP_TARGETS)
- @echo Built libraries: $(LIB_TARGETS)
- @echo Built shared libraries: $(SHARED_LIB_TARGETS)
- @echo Built apps: $(APP_TARGETS)
-
-debug: $(LIB_TARGETS_DEBUG) $(SHARED_LIB_TARGETS_DEBUG) $(APP_TARGETS_DEBUG)
- @echo Built libraries: $(LIB_TARGETS_DEBUG)
- @echo Built shared libraries: $(SHARED_LIB_TARGETS_DEBUG)
- @echo Built apps: $(APP_TARGETS_DEBUG)
diff --git a/engine/compilers/Make-64bit/Torque2D.mk b/engine/compilers/Make-64bit/Torque2D.mk
deleted file mode 100644
index 09309a1ee..000000000
--- a/engine/compilers/Make-64bit/Torque2D.mk
+++ /dev/null
@@ -1,144 +0,0 @@
-APPNAME := ../../../Torque2D
-
-2D_SOURCES := $(shell find ../../source/2d/ -name "*.cc") + \
- $(shell find ../../source/2d/ -name "*.cpp")
-ALGORITHM_SOURCES := $(shell find ../../source/algorithm/ -name "*.cc") + \
- $(shell find ../../source/algorithm/ -name "*.c")
-ASSETS_SOURCES := $(shell find ../../source/assets/ -name "*.cc")
-AUDIO_SOURCES := $(shell find ../../source/audio/ -name "*.cc")
-BITMAPFONT_SOURCES := $(shell find ../../source/bitmapFont/ -name "*.cc")
-BOX2D_SOURCES := $(shell find ../../source/Box2D/ -name "*.cpp")
-COLLECTION_SOURCES := $(shell find ../../source/collection/ -name "*.cc")
-COMPONENT_SOURCES := $(shell find ../../source/component/ -name "*.cpp")
-CONSOLE_SOURCES := $(shell find ../../source/console/ -name "*.cc")
-DEBUG_SOURCES := $(shell find ../../source/debug/ -name "*.cc")
-DELEGATES_SOURCES := $(shell find ../../source/delegates/ -name "*.cc")
-GAME_SOURCES := $(shell find ../../source/game/ -name "*.cc")
-GRAPHICS_SOURCES := $(shell find ../../source/graphics/ -name "*.cc")
-GUI_SOURCES := $(shell find ../../source/gui/ -name "*.cc")
-INPUT_SOURCES := $(shell find ../../source/input/ -name "*.cc")
-IO_SOURCES := $(shell find ../../source/io/ -name "*.cc")
-MATH_SOURCES := $(shell find ../../source/math/ -name "*.cc") + \
- $(shell find ../../source/math/ -name "*.cpp")
-MEMORY_SOURCES := $(shell find ../../source/memory/ -name "*.cc")
-MESSAGING_SOURCES := $(shell find ../../source/messaging/ -name "*.cc")
-MODULE_SOURCES := $(shell find ../../source/module/ -name "*.cc")
-NETWORK_SOURCES := $(shell find ../../source/network/ -name "*.cc")
-PERSISTENCE_SOURCES := $(shell find ../../source/persistence/ -name "*.cc") + \
- $(shell find ../../source/persistence/ -name "*.cpp")
-PLATFORM_SOURCES := $(shell find ../../source/platform/ -name "*.cc") + \
- $(shell find ../../source/platform/ -name "*.cpp")
-PLATFORM_UNIX_SOURCES := $(shell find ../../source/platformX86UNIX/ -name "*.cc")
-SIM_SOURCES := $(shell find ../../source/sim/ -name "*.cc") + \
- $(shell find ../../source/sim/ -name "*.cpp")
-STRING_SOURCES := $(shell find ../../source/string/ -name "*.cc") + \
- $(shell find ../../source/string/ -name "*.cpp")
-
-SOURCES := $(2D_SOURCES) + \
- $(ALGORITHM_SOURCES) + \
- $(ASSETS_SOURCES) + \
- $(AUDIO_SOURCES) + \
- $(BITMAPFONT_SOURCES) + \
- $(BOX2D_SOURCES) + \
- $(COLLECTION_SOURCES) + \
- $(COMPONENT_SOURCES) + \
- $(CONSOLE_SOURCES) + \
- $(DEBUG_SOURCES) + \
- $(DELEGATES_SOURCES) + \
- $(GAME_SOURCES) + \
- $(GRAPHICS_SOURCES) + \
- $(GUI_SOURCES) + \
- $(INPUT_SOURCES) + \
- $(IO_SOURCES) + \
- $(MATH_SOURCES) + \
- $(MEMORY_SOURCES) + \
- $(MESSAGING_SOURCES) + \
- $(MODULE_SOURCES) + \
- $(NETWORK_SOURCES) + \
- $(PERSISTENCE_SOURCES) + \
- $(PLATFORM_SOURCES) + \
- $(PLATFORM_UNIX_SOURCES) + \
- $(SIM_SOURCES) + \
- $(STRING_SOURCES)
-
-LDFLAGS := -g -m64
-LDLIBS := -lstdc++ -lm -ldl -lpthread -lrt -lX11 -lXft -lSDL -lopenal
-
-CFLAGS := -std=c++17 -MMD -I. -Wfatal-errors -Wunused -m64 -msse -march=x86-64 -pipe
-
-CFLAGS += -I/usr/include
-CFLAGS += -I/usr/include/freetype2
-CFLAGS += -I../../source
-CFLAGS += -I../../source/persistence/rapidjson/include
-CFLAGS += -I../../lib/ljpeg
-CFLAGS += -I../../lib/zlib
-CFLAGS += -I../../lib/lpng
-CFLAGS += -I../../lib/freetype
-CFLAGS += -I../../lib/libvorbis/include
-CFLAGS += -I../../lib/libogg/include
-CFLAGS += -I../../lib/openal/LINUX/
-
-CFLAGS += -DLINUX
-CFLAGS += -D__amd64__
-CFLAGS += -DTORQUE_64
-
-
-CFLAGS_DEBUG := $(CFLAGS) -ggdb
-CFLAGS_DEBUG += -DTORQUE_DEBUG
-CFLAGS_DEBUG += -DTORQUE_DEBUG_GUARD
-CFLAGS_DEBUG += -DTORQUE_NET_STATS
-
-CFLAGS += -Og
-
-NASMFLAGS := -f elf64 -D LINUX
-
-CC := gcc
-LD := gcc
-
-APP_TARGETS += $(APPNAME)
-APP_TARGETS_DEBUG += $(APPNAME)_DEBUG
-
-OBJS := $(patsubst ../../source/%,Release/%.o,$(SOURCES))
-OBJS := $(filter %.o, $(OBJS))
-
-OBJS_DEBUG := $(patsubst ../../source/%,Debug/%.o,$(SOURCES))
-OBJS_DEBUG := $(filter %.o,$(OBJS_DEBUG))
-
-$(APP_TARGETS): $(OBJS) $(LIB_TARGETS)
- @echo Linking release
- $(LD) $(LDFLAGS) -o $@ $(OBJS) $(LIB_TARGETS) $(LDLIBS)
-
-$(APP_TARGETS_DEBUG): $(OBJS_DEBUG) $(LIB_TARGETS_DEBUG)
- @echo Linking debug
- $(LD) $(LDFLAGS) -o $@ $(OBJS_DEBUG) $(LIB_TARGETS_DEBUG) $(LDLIBS)
-
-Release/%.asm.o: ../../source/%.asm
- @echo Building release asm $@
- @mkdir -p $(dir $@)
- nasm $(NASMFLAGS) $< -o $@
-
-Release/%.o: ../../source/%
- @echo Building release object $@
- @mkdir -p $(dir $@)
- $(CC) -c $(CFLAGS) $< -o $@
-
-Debug/%.asm.o: ../../source/%.asm
- @echo Building debug asm $@
- @mkdir -p $(dir $@)
- nasm $(NASMFLAGS) $< -o $@
-
-Debug/%.o: ../../source/%
- @echo Building debug object $@
- @mkdir -p $(dir $@)
- $(CC) -c $(CFLAGS_DEBUG) $< -o $@
-
-release: $(APP_TARGETS)
-debug: $(APP_TARGETS_DEBUG)
-
-.PHONY: $(APP_TARGETS) $(APP_TARGETS_DEBUG)
-
-DEPS += $(patsubst %.o,%.d,$(OBJS))
-DEPS += $(patsubst %.o,%.d,$(OBJS_DEBUG))
-
-APPNAME :=
-SOURCES :=
diff --git a/engine/compilers/Make-64bit/ljpeg b/engine/compilers/Make-64bit/ljpeg
deleted file mode 100644
index 22a417ce5..000000000
--- a/engine/compilers/Make-64bit/ljpeg
+++ /dev/null
@@ -1,109 +0,0 @@
-# I release this sample under the MIT license: free for any use, provided
-# you hold me harmless from any such use you make, and you retain my
-# copyright on the actual sources.
-# Copyright 2005 Jon Watte.
-
-LIBNAME := ljpeg
-SOURCES := ../../lib/ljpeg/jidctint.c \
-../../lib/ljpeg/jcmainct.c \
-../../lib/ljpeg/jfdctfst.c \
-../../lib/ljpeg/jdatadst.c \
-../../lib/ljpeg/jdsample.c \
-../../lib/ljpeg/jmemmgr.c \
-../../lib/ljpeg/jidctred.c \
-../../lib/ljpeg/jcphuff.c \
-../../lib/ljpeg/jchuff.c \
-../../lib/ljpeg/jdapistd.c \
-../../lib/ljpeg/jdpostct.c \
-../../lib/ljpeg/jquant2.c \
-../../lib/ljpeg/jdmerge.c \
-../../lib/ljpeg/jfdctflt.c \
-../../lib/ljpeg/jcprepct.c \
-../../lib/ljpeg/jccolor.c \
-../../lib/ljpeg/jfdctint.c \
-../../lib/ljpeg/jdhuff.c \
-../../lib/ljpeg/jcomapi.c \
-../../lib/ljpeg/jcinit.c \
-../../lib/ljpeg/jccoefct.c \
-../../lib/ljpeg/jdinput.c \
-../../lib/ljpeg/jutils.c \
-../../lib/ljpeg/jcapimin.c \
-../../lib/ljpeg/jdcoefct.c \
-../../lib/ljpeg/jidctflt.c \
-../../lib/ljpeg/jcmaster.c \
-../../lib/ljpeg/jddctmgr.c \
-../../lib/ljpeg/jidctfst.c \
-../../lib/ljpeg/jcparam.c \
-../../lib/ljpeg/jcapistd.c \
-../../lib/ljpeg/jdmaster.c \
-../../lib/ljpeg/jcdctmgr.c \
-../../lib/ljpeg/jctrans.c \
-../../lib/ljpeg/jdmainct.c \
-../../lib/ljpeg/jdtrans.c \
-../../lib/ljpeg/jcsample.c \
-../../lib/ljpeg/jdmarker.c \
-../../lib/ljpeg/jdatasrc.c \
-../../lib/ljpeg/jerror.c \
-../../lib/ljpeg/jquant1.c \
-../../lib/ljpeg/jdphuff.c \
-../../lib/ljpeg/jcmarker.c \
-../../lib/ljpeg/jdapimin.c \
-../../lib/ljpeg/jdcolor.c \
-../../lib/ljpeg/jmemnobs.c \
-
-LDFLAGS_ljpeg := -g -m64
-
-CFLAGS_ljpeg := -MMD -I. -m64 -msse -mmmx -march=x86-64
-
-CFLAGS_ljpeg += -I../../lib/ljpeg
-
-CFLAGS_ljpeg += -DUNICODE
-CFLAGS_ljpeg += -DLINUX
-
-CFLAGS_DEBUG_ljpeg := $(CFLAGS_ljpeg) -ggdb
-CFLAGS_DEBUG_ljpeg += -DTORQUE_DEBUG
-CFLAGS_DEBUG_ljpeg += -DTORQUE_DEBUG_GUARD
-CFLAGS_DEBUG_ljpeg += -DTORQUE_NET_STATS
-
-CFLAGS_ljpeg += -O3
-
-CC := gcc
-LD := gcc
-
-TARGET_ljpeg := lib/ljpeg.a
-TARGET_ljpeg_DEBUG := lib/ljpeg_DEBUG.a
-
-LIB_TARGETS += $(TARGET_ljpeg)
-LIB_TARGETS_DEBUG += $(TARGET_ljpeg_DEBUG)
-
-OBJS_ljpeg := $(patsubst ../../lib/ljpeg/%,Release/ljpeg/%.o,$(SOURCES))
-OBJS_ljpeg_DEBUG := $(patsubst ../../lib/ljpeg/%,Debug/ljpeg/%.o,$(SOURCES))
-
-# Deriving the variable name from the target name is the secret sauce
-# of the build system.
-#
-$(TARGET_ljpeg): $(OBJS_ljpeg)
- @echo Linking library ljpng
- @mkdir -p $(dir $@)
- ar cr $@ $(OBJS_ljpeg)
-
-$(TARGET_ljpeg_DEBUG): $(OBJS_ljpeg_DEBUG)
- @echo Linking debug library ljpng
- @mkdir -p $(dir $@)
- ar cr $@ $(OBJS_ljpeg_DEBUG)
-
-Release/ljpeg/%.o: ../../lib/ljpeg/%
- @mkdir -p $(dir $@)
- $(CC) -c $(CFLAGS_ljpeg) $< -o $@
-
-Debug/ljpeg/%.o: ../../lib/ljpeg/%
- @mkdir -p $(dir $@)
- $(CC) -c $(CFLAGS_DEBUG_ljpeg) $< -o $@
-
-release_ljpeg: $(TARGET_ljpeg)
-debug_ljpeg: $(TARGET_ljpeg_DEBUG)
-
-.PHONY: debug_ljpeg release_ljpeg
-
-DEPS += $(patsubst %.o,%.d,$(OBJS_ljpeg))
-DEPS += $(patsubst %.o,%.d,$(OBJS_ljpeg_DEBUG))
diff --git a/engine/compilers/Make-64bit/lpng b/engine/compilers/Make-64bit/lpng
deleted file mode 100644
index 6ed227f75..000000000
--- a/engine/compilers/Make-64bit/lpng
+++ /dev/null
@@ -1,78 +0,0 @@
-# I release this sample under the MIT license: free for any use, provided
-# you hold me harmless from any such use you make, and you retain my
-# copyright on the actual sources.
-# Copyright 2005 Jon Watte.
-
-LIBNAME := lpng
-SOURCES := ../../lib/lpng/pngerror.c \
-../../lib/lpng/pngwrite.c \
-../../lib/lpng/pngread.c \
-../../lib/lpng/pngmem.c \
-../../lib/lpng/pngset.c \
-../../lib/lpng/pngwio.c \
-../../lib/lpng/pngrtran.c \
-../../lib/lpng/pngtrans.c \
-../../lib/lpng/pngrutil.c \
-../../lib/lpng/pngwtran.c \
-../../lib/lpng/png.c \
-../../lib/lpng/pngrio.c \
-../../lib/lpng/pngwutil.c \
-../../lib/lpng/pngget.c \
-../../lib/lpng/pngpread.c \
-
-LDFLAGS_lpng := -g -m64
-#LDLIBS_lpng := -lstdc++
-CFLAGS_lpng := -MMD -I. -m64 -msse -mmmx -march=x86-64
-
-CFLAGS_lpng += -I../../lib/zlib
-CFLAGS_lpng += -I../../lib/lpng
-
-CFLAGS_lpng += -DUNICODE
-CFLAGS_lpng += -DLINUX
-
-
-CFLAGS_DEBUG_lpng := $(CFLAGS_lpng) -ggdb
-CFLAGS_DEBUG_lpng += -DTORQUE_DEBUG
-CFLAGS_DEBUG_lpng += -DTORQUE_DEBUG_GUARD
-CFLAGS_DEBUG_lpng += -DTORQUE_NET_STATS
-
-CFLAGS_lpng += -O3
-
-CC := gcc
-LD := gcc
-
-TARGET_lpng := lib/lpng.a
-TARGET_lpng_DEBUG := lib/lpng_DEBUG.a
-
-LIB_TARGETS += $(TARGET_lpng)
-LIB_TARGETS_DEBUG += $(TARGET_lpng_DEBUG)
-
-OBJS_lpng := $(patsubst ../../lib/lpng/%,Release/lpng/%.o,$(SOURCES))
-OBJS_lpng_DEBUG := $(patsubst ../../lib/lpng/%,Debug/lpng/%.o,$(SOURCES))
-
-# Deriving the variable name from the target name is the secret sauce
-# of the build system.
-#
-$(TARGET_lpng): $(OBJS_lpng)
- @mkdir -p $(dir $@)
- ar cr $@ $(OBJS_lpng)
-
-$(TARGET_lpng_DEBUG): $(OBJS_lpng_DEBUG)
- @mkdir -p $(dir $@)
- ar cr $@ $(OBJS_lpng_DEBUG)
-
-Release/lpng/%.o: ../../lib/lpng/%
- @mkdir -p $(dir $@)
- $(CC) -c $(CFLAGS_lpng) $< -o $@
-
-Debug/lpng/%.o: ../../lib/lpng/%
- @mkdir -p $(dir $@)
- $(CC) -c $(CFLAGS_DEBUG_lpng) $< -o $@
-
-release_lpng: $(TARGET_lpng)
-debug_lpng: $(TARGET_lpng_DEBUG)
-
-.PHONY: debug_lpng release_lpng
-
-DEPS += $(patsubst %.o,%.d,$(OBJS_lpng))
-DEPS += $(patsubst %.o,%.d,$(OBJS_lpng_DEBUG))
diff --git a/engine/compilers/Make-64bit/ogg b/engine/compilers/Make-64bit/ogg
deleted file mode 100644
index 3dec1fcec..000000000
--- a/engine/compilers/Make-64bit/ogg
+++ /dev/null
@@ -1,64 +0,0 @@
-# I release this sample under the MIT license: free for any use, provided
-# you hold me harmless from any such use you make, and you retain my
-# copyright on the actual sources.
-# Copyright 2005 Jon Watte.
-
-LIBNAME := ogg
-SOURCES := \
-../../lib/libogg/src/bitwise.c \
-../../lib/libogg/src/framing.c \
-
-LDFLAGS_ogg := -g -m64
-
-CFLAGS_ogg := -MMD -I. -m64 -msse -mmmx -march=x86-64
-
-CFLAGS_ogg += -I../../lib/libogg/include
-
-CFLAGS_ogg += -DUNICODE
-CFLAGS_ogg += -DLINUX
-
-CFLAGS_DEBUG_ogg := $(CFLAGS_ogg) -ggdb
-CFLAGS_DEBUG_ogg += -DTORQUE_DEBUG
-CFLAGS_DEBUG_ogg += -DTORQUE_DEBUG_GUARD
-CFLAGS_DEBUG_ogg += -DTORQUE_NET_STATS
-
-CFLAGS_ogg += -O3
-
-CC := gcc
-LD := gcc
-
-TARGET_ogg := lib/libogg.a
-TARGET_ogg_DEBUG := lib/libogg_DEBUG.a
-
-LIB_TARGETS += $(TARGET_ogg)
-LIB_TARGETS_DEBUG += $(TARGET_ogg_DEBUG)
-
-OBJS_ogg := $(patsubst ../../lib/libogg/%,Release/ogg/%.o,$(SOURCES))
-OBJS_ogg_DEBUG := $(patsubst ../../lib/libogg/%,Debug/ogg/%.o,$(SOURCES))
-
-# Deriving the variable name from the target name is the secret sauce
-# of the build system.
-#
-$(TARGET_ogg): $(OBJS_ogg)
- @mkdir -p $(dir $@)
- ar cr $@ $(OBJS_ogg)
-
-$(TARGET_ogg_DEBUG): $(OBJS_ogg_DEBUG)
- @mkdir -p $(dir $@)
- ar cr $@ $(OBJS_ogg_DEBUG)
-
-Release/ogg/%.o: ../../lib/libogg/%
- @mkdir -p $(dir $@)
- $(CC) -c $(CFLAGS_ogg) $< -o $@
-
-Debug/ogg/%.o: ../../lib/libogg/%
- @mkdir -p $(dir $@)
- $(CC) -c $(CFLAGS_DEBUG_ogg) $< -o $@
-
-release_ogg: $(TARGET_ogg)
-debug_ogg: $(TARGET_ogg_DEBUG)
-
-.PHONY: debug_ogg release_ogg
-
-DEPS += $(patsubst %.o,%.d,$(OBJS_ogg))
-DEPS += $(patsubst %.o,%.d,$(OBJS_ogg_DEBUG))
diff --git a/engine/compilers/Make-64bit/vorbis b/engine/compilers/Make-64bit/vorbis
deleted file mode 100644
index 01ad8f1ac..000000000
--- a/engine/compilers/Make-64bit/vorbis
+++ /dev/null
@@ -1,89 +0,0 @@
-# I release this sample under the MIT license: free for any use, provided
-# you hold me harmless from any such use you make, and you retain my
-# copyright on the actual sources.
-# Copyright 2005 Jon Watte.
-
-LIBNAME := vorbis
-SOURCES := \
-../../lib/libvorbis/analysis.c \
-../../lib/libvorbis/barkmel.c \
-../../lib/libvorbis/bitrate.c \
-../../lib/libvorbis/block.c \
-../../lib/libvorbis/codebook.c \
-../../lib/libvorbis/envelope.c \
-../../lib/libvorbis/floor0.c \
-../../lib/libvorbis/floor1.c \
-../../lib/libvorbis/info.c \
-../../lib/libvorbis/lookup.c \
-../../lib/libvorbis/lpc.c \
-../../lib/libvorbis/lsp.c \
-../../lib/libvorbis/mapping0.c \
-../../lib/libvorbis/mdct.c \
-../../lib/libvorbis/psy.c \
-../../lib/libvorbis/registry.c \
-../../lib/libvorbis/res0.c \
-../../lib/libvorbis/sharedbook.c \
-../../lib/libvorbis/smallft.c \
-../../lib/libvorbis/synthesis.c \
-../../lib/libvorbis/tone.c \
-../../lib/libvorbis/vorbisenc.c \
-../../lib/libvorbis/vorbisfile.c \
-../../lib/libvorbis/window.c \
-
-LDFLAGS_vorbis := -g -m64
-
-CFLAGS_vorbis := -MMD -I. -m64 -msse -mmmx -march=x86-64
-
-CFLAGS_vorbis += -I../../lib/libvorbis
-CFLAGS_vorbis += -I../../lib/libvorbis/lib
-CFLAGS_vorbis += -I../../lib/libvorbis/include
-CFLAGS_vorbis += -I../../lib/libogg/include
-
-CFLAGS_vorbis += -DUNICODE
-CFLAGS_vorbis += -DLINUX
-
-CFLAGS_DEBUG_vorbis := $(CFLAGS_vorbis) -ggdb
-CFLAGS_DEBUG_vorbis += -DTORQUE_DEBUG
-CFLAGS_DEBUG_vorbis += -DTORQUE_DEBUG_GUARD
-CFLAGS_DEBUG_vorbis += -DTORQUE_NET_STATS
-
-CFLAGS_vorbis += -O3
-
-CC := gcc
-LD := gcc
-
-TARGET_vorbis := lib/libvorbis.a
-TARGET_vorbis_DEBUG := lib/libvorbis_DEBUG.a
-
-LIB_TARGETS += $(TARGET_vorbis)
-LIB_TARGETS_DEBUG += $(TARGET_vorbis_DEBUG)
-
-OBJS_vorbis := $(patsubst ../../lib/libvorbis/%,Release/vorbis/%.o,$(SOURCES))
-OBJS_vorbis_DEBUG := $(patsubst ../../lib/libvorbis/%,Debug/vorbis/%.o,$(SOURCES))
-
-# Deriving the variable name from the target name is the secret sauce
-# of the build system.
-#
-$(TARGET_vorbis): $(OBJS_vorbis)
- @mkdir -p $(dir $@)
- ar cr $@ $(OBJS_vorbis)
-
-$(TARGET_vorbis_DEBUG): $(OBJS_vorbis_DEBUG)
- @mkdir -p $(dir $@)
- ar cr $@ $(OBJS_vorbis_DEBUG)
-
-Release/vorbis/%.o: ../../lib/libvorbis/%
- @mkdir -p $(dir $@)
- $(CC) -c $(CFLAGS_vorbis) $< -o $@
-
-Debug/vorbis/%.o: ../../lib/libvorbis/%
- @mkdir -p $(dir $@)
- $(CC) -c $(CFLAGS_DEBUG_vorbis) $< -o $@
-
-release_vorbis: $(TARGET_vorbis)
-debug_vorbis: $(TARGET_vorbis_DEBUG)
-
-.PHONY: debug_vorbis release_vorbis
-
-DEPS += $(patsubst %.o,%.d,$(OBJS_vorbis))
-DEPS += $(patsubst %.o,%.d,$(OBJS_vorbis_DEBUG))
diff --git a/engine/compilers/Make-64bit/zlib b/engine/compilers/Make-64bit/zlib
deleted file mode 100644
index 6c2d1942f..000000000
--- a/engine/compilers/Make-64bit/zlib
+++ /dev/null
@@ -1,76 +0,0 @@
-# I release this sample under the MIT license: free for any use, provided
-# you hold me harmless from any such use you make, and you retain my
-# copyright on the actual sources.
-# Copyright 2005 Jon Watte.
-
-LIBNAME := zlib
-SOURCES := ../../lib/zlib/adler32.c \
-../../lib/zlib/zutil.c \
-../../lib/zlib/crc32.c \
-../../lib/zlib/trees.c \
-../../lib/zlib/inflate.c \
-../../lib/zlib/inftrees.c \
-../../lib/zlib/gzclose.c \
-../../lib/zlib/gzread.c \
-../../lib/zlib/infback.c \
-../../lib/zlib/uncompr.c \
-../../lib/zlib/deflate.c \
-../../lib/zlib/inffast.c \
-../../lib/zlib/gzwrite.c \
-../../lib/zlib/compress.c \
-../../lib/zlib/gzlib.c \
-
-LDFLAGS_zlib := -g -m
-
-CFLAGS_zlib := -MMD -I. -m64 -msse -mmmx -march=x86-64
-
-CFLAGS_zlib += -I../../lib/zlib
-
-CFLAGS_zlib += -DUNICODE
-CFLAGS_zlib += -DLINUX
-
-CFLAGS_DEBUG_zlib := $(CFLAGS_zlib) -ggdb
-CFLAGS_DEBUG_zlib += -DTORQUE_DEBUG
-CFLAGS_DEBUG_zlib += -DTORQUE_DEBUG_GUARD
-CFLAGS_DEBUG_zlib += -DTORQUE_NET_STATS
-
-CFLAGS_zlib += -O3
-
-CC := gcc
-LD := gcc
-
-TARGET_zlib := lib/zlib.a
-TARGET_zlib_DEBUG := lib/zlib_DEBUG.a
-
-LIB_TARGETS += $(TARGET_zlib)
-LIB_TARGETS_DEBUG += $(TARGET_zlib_DEBUG)
-
-OBJS_zlib := $(patsubst ../../lib/zlib/%,Release/zlib/%.o,$(SOURCES))
-OBJS_zlib_DEBUG := $(patsubst ../../lib/zlib/%,Debug/zlib/%.o,$(SOURCES))
-
-# Deriving the variable name from the target name is the secret sauce
-# of the build system.
-#
-$(TARGET_zlib): $(OBJS_zlib)
- @mkdir -p $(dir $@)
- ar cr $@ $(OBJS_zlib)
-
-$(TARGET_zlib_DEBUG): $(OBJS_zlib_DEBUG)
- @mkdir -p $(dir $@)
- ar cr $@ $(OBJS_zlib_DEBUG)
-
-Release/zlib/%.o: ../../lib/zlib/%
- @mkdir -p $(dir $@)
- $(CC) -c $(CFLAGS_zlib) $< -o $@
-
-Debug/zlib/%.o: ../../lib/zlib/%
- @mkdir -p $(dir $@)
- $(CC) -c $(CFLAGS_DEBUG_zlib) $< -o $@
-
-release_zlib: $(TARGET_zlib)
-debug_zlib: $(TARGET_zlib_DEBUG)
-
-.PHONY: debug_zlib release_zlib
-
-DEPS += $(patsubst %.o,%.d,$(OBJS_zlib))
-DEPS += $(patsubst %.o,%.d,$(OBJS_zlib_DEBUG))
diff --git a/engine/compilers/VisualStudio 2019/Torque 2D.aps b/engine/compilers/VisualStudio 2019/Torque 2D.aps
deleted file mode 100644
index 8b5584168..000000000
Binary files a/engine/compilers/VisualStudio 2019/Torque 2D.aps and /dev/null differ
diff --git a/engine/compilers/VisualStudio 2019/Torque 2D.ico b/engine/compilers/VisualStudio 2019/Torque 2D.ico
deleted file mode 100644
index 7e9d7feef..000000000
Binary files a/engine/compilers/VisualStudio 2019/Torque 2D.ico and /dev/null differ
diff --git a/engine/compilers/VisualStudio 2019/Torque 2D.rc b/engine/compilers/VisualStudio 2019/Torque 2D.rc
deleted file mode 100644
index 4c842aa90..000000000
--- a/engine/compilers/VisualStudio 2019/Torque 2D.rc
+++ /dev/null
@@ -1,110 +0,0 @@
-// Microsoft Visual C++ generated resource script.
-//
-#include "resource.h"
-
-#define APSTUDIO_READONLY_SYMBOLS
-/////////////////////////////////////////////////////////////////////////////
-//
-// Generated from the TEXTINCLUDE 2 resource.
-//
-#include "windows.h"
-
-/////////////////////////////////////////////////////////////////////////////
-#undef APSTUDIO_READONLY_SYMBOLS
-
-/////////////////////////////////////////////////////////////////////////////
-// English (United States) resources
-
-#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
-LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
-#pragma code_page(1252)
-
-/////////////////////////////////////////////////////////////////////////////
-//
-// Icon
-//
-
-// Icon with lowest ID value placed first to ensure application icon
-// remains consistent on all systems.
-IDI_TORQUE2D ICON "Torque 2D.ico"
-
-
-#ifdef APSTUDIO_INVOKED
-/////////////////////////////////////////////////////////////////////////////
-//
-// TEXTINCLUDE
-//
-
-1 TEXTINCLUDE
-BEGIN
- "resource.h\0"
-END
-
-2 TEXTINCLUDE
-BEGIN
- "#include ""afxres.h""\r\n"
- "\0"
-END
-
-3 TEXTINCLUDE
-BEGIN
- "\r\n"
- "\0"
-END
-
-#endif // APSTUDIO_INVOKED
-
-
-/////////////////////////////////////////////////////////////////////////////
-//
-// Version
-//
-
-VS_VERSION_INFO VERSIONINFO
- FILEVERSION 4,0,0,0
- PRODUCTVERSION 4,0,0,0
- FILEFLAGSMASK 0x17L
-#ifdef _DEBUG
- FILEFLAGS 0x1L
-#else
- FILEFLAGS 0x0L
-#endif
- FILEOS 0x4L
- FILETYPE 0x1L
- FILESUBTYPE 0x0L
-BEGIN
- BLOCK "StringFileInfo"
- BEGIN
- BLOCK "040904b0"
- BEGIN
- VALUE "CompanyName", "Torque Game Engines"
- VALUE "FileDescription", "Torque 2D: Rocket Edition"
- VALUE "FileVersion", "4, 0, 0, 0"
- VALUE "InternalName", "Torque 2D"
- VALUE "LegalCopyright", "Copyright (c) 2021 Torque Game Engines"
- VALUE "OriginalFilename", "Torque2D.exe"
- VALUE "ProductName", "Torque 2D"
- VALUE "ProductVersion", "4, 0, 0, 0"
- END
- END
- BLOCK "VarFileInfo"
- BEGIN
- VALUE "Translation", 0x409, 1200
- END
-END
-
-#endif // English (United States) resources
-/////////////////////////////////////////////////////////////////////////////
-
-
-
-#ifndef APSTUDIO_INVOKED
-/////////////////////////////////////////////////////////////////////////////
-//
-// Generated from the TEXTINCLUDE 3 resource.
-//
-
-
-/////////////////////////////////////////////////////////////////////////////
-#endif // not APSTUDIO_INVOKED
-
diff --git a/engine/compilers/VisualStudio 2019/Torque 2D.sln b/engine/compilers/VisualStudio 2019/Torque 2D.sln
deleted file mode 100644
index 9ef72593f..000000000
--- a/engine/compilers/VisualStudio 2019/Torque 2D.sln
+++ /dev/null
@@ -1,113 +0,0 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.31729.503
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Torque2D", "Torque 2D.vcxproj", "{1564A07D-230E-4C90-AEE6-52AC9A58D6C9}"
- ProjectSection(ProjectDependencies) = postProject
- {3A214E06-B95E-4D61-A291-1F8DF2EC10FD} = {3A214E06-B95E-4D61-A291-1F8DF2EC10FD}
- {15CBFEFF-7965-41F5-B4E2-21E8795C9159} = {15CBFEFF-7965-41F5-B4E2-21E8795C9159}
- EndProjectSection
-EndProject
-Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ljpeg", "ljpeg.vcxproj", "{0B07BA94-AA53-4FD4-ADB4-79EC2DA53B36}"
-EndProject
-Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "lpng", "lpng.vcxproj", "{AF1179E3-A838-46A3-A427-1E62AA4C52F4}"
-EndProject
-Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "zlib", "zlib.vcxproj", "{86CB2525-0CF3-40D3-BF42-A0A95035EE8C}"
-EndProject
-Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libogg", "libogg.vcxproj", "{15CBFEFF-7965-41F5-B4E2-21E8795C9159}"
-EndProject
-Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libvorbis", "libvorbis.vcxproj", "{3A214E06-B95E-4D61-A291-1F8DF2EC10FD}"
- ProjectSection(ProjectDependencies) = postProject
- {15CBFEFF-7965-41F5-B4E2-21E8795C9159} = {15CBFEFF-7965-41F5-B4E2-21E8795C9159}
- EndProjectSection
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Win32 = Debug|Win32
- Debug|x64 = Debug|x64
- Release|Win32 = Release|Win32
- Release|x64 = Release|x64
- Shipping|Win32 = Shipping|Win32
- Shipping|x64 = Shipping|x64
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {1564A07D-230E-4C90-AEE6-52AC9A58D6C9}.Debug|Win32.ActiveCfg = Debug|Win32
- {1564A07D-230E-4C90-AEE6-52AC9A58D6C9}.Debug|Win32.Build.0 = Debug|Win32
- {1564A07D-230E-4C90-AEE6-52AC9A58D6C9}.Debug|x64.ActiveCfg = Debug|x64
- {1564A07D-230E-4C90-AEE6-52AC9A58D6C9}.Debug|x64.Build.0 = Debug|x64
- {1564A07D-230E-4C90-AEE6-52AC9A58D6C9}.Release|Win32.ActiveCfg = Release|Win32
- {1564A07D-230E-4C90-AEE6-52AC9A58D6C9}.Release|Win32.Build.0 = Release|Win32
- {1564A07D-230E-4C90-AEE6-52AC9A58D6C9}.Release|x64.ActiveCfg = Release|x64
- {1564A07D-230E-4C90-AEE6-52AC9A58D6C9}.Release|x64.Build.0 = Release|x64
- {1564A07D-230E-4C90-AEE6-52AC9A58D6C9}.Shipping|Win32.ActiveCfg = Shipping|Win32
- {1564A07D-230E-4C90-AEE6-52AC9A58D6C9}.Shipping|Win32.Build.0 = Shipping|Win32
- {1564A07D-230E-4C90-AEE6-52AC9A58D6C9}.Shipping|x64.ActiveCfg = Shipping|x64
- {1564A07D-230E-4C90-AEE6-52AC9A58D6C9}.Shipping|x64.Build.0 = Shipping|x64
- {0B07BA94-AA53-4FD4-ADB4-79EC2DA53B36}.Debug|Win32.ActiveCfg = Debug|Win32
- {0B07BA94-AA53-4FD4-ADB4-79EC2DA53B36}.Debug|Win32.Build.0 = Debug|Win32
- {0B07BA94-AA53-4FD4-ADB4-79EC2DA53B36}.Debug|x64.ActiveCfg = Debug|x64
- {0B07BA94-AA53-4FD4-ADB4-79EC2DA53B36}.Debug|x64.Build.0 = Debug|x64
- {0B07BA94-AA53-4FD4-ADB4-79EC2DA53B36}.Release|Win32.ActiveCfg = Release|Win32
- {0B07BA94-AA53-4FD4-ADB4-79EC2DA53B36}.Release|Win32.Build.0 = Release|Win32
- {0B07BA94-AA53-4FD4-ADB4-79EC2DA53B36}.Release|x64.ActiveCfg = Release|x64
- {0B07BA94-AA53-4FD4-ADB4-79EC2DA53B36}.Release|x64.Build.0 = Release|x64
- {0B07BA94-AA53-4FD4-ADB4-79EC2DA53B36}.Shipping|Win32.ActiveCfg = Shipping|Win32
- {0B07BA94-AA53-4FD4-ADB4-79EC2DA53B36}.Shipping|Win32.Build.0 = Shipping|Win32
- {0B07BA94-AA53-4FD4-ADB4-79EC2DA53B36}.Shipping|x64.ActiveCfg = Shipping|x64
- {0B07BA94-AA53-4FD4-ADB4-79EC2DA53B36}.Shipping|x64.Build.0 = Shipping|x64
- {AF1179E3-A838-46A3-A427-1E62AA4C52F4}.Debug|Win32.ActiveCfg = Debug|Win32
- {AF1179E3-A838-46A3-A427-1E62AA4C52F4}.Debug|Win32.Build.0 = Debug|Win32
- {AF1179E3-A838-46A3-A427-1E62AA4C52F4}.Debug|x64.ActiveCfg = Debug|x64
- {AF1179E3-A838-46A3-A427-1E62AA4C52F4}.Debug|x64.Build.0 = Debug|x64
- {AF1179E3-A838-46A3-A427-1E62AA4C52F4}.Release|Win32.ActiveCfg = Release|Win32
- {AF1179E3-A838-46A3-A427-1E62AA4C52F4}.Release|Win32.Build.0 = Release|Win32
- {AF1179E3-A838-46A3-A427-1E62AA4C52F4}.Release|x64.ActiveCfg = Release|x64
- {AF1179E3-A838-46A3-A427-1E62AA4C52F4}.Release|x64.Build.0 = Release|x64
- {AF1179E3-A838-46A3-A427-1E62AA4C52F4}.Shipping|Win32.ActiveCfg = Shipping|Win32
- {AF1179E3-A838-46A3-A427-1E62AA4C52F4}.Shipping|Win32.Build.0 = Shipping|Win32
- {AF1179E3-A838-46A3-A427-1E62AA4C52F4}.Shipping|x64.ActiveCfg = Shipping|x64
- {AF1179E3-A838-46A3-A427-1E62AA4C52F4}.Shipping|x64.Build.0 = Shipping|x64
- {86CB2525-0CF3-40D3-BF42-A0A95035EE8C}.Debug|Win32.ActiveCfg = Debug|Win32
- {86CB2525-0CF3-40D3-BF42-A0A95035EE8C}.Debug|Win32.Build.0 = Debug|Win32
- {86CB2525-0CF3-40D3-BF42-A0A95035EE8C}.Debug|x64.ActiveCfg = Debug|x64
- {86CB2525-0CF3-40D3-BF42-A0A95035EE8C}.Debug|x64.Build.0 = Debug|x64
- {86CB2525-0CF3-40D3-BF42-A0A95035EE8C}.Release|Win32.ActiveCfg = Release|Win32
- {86CB2525-0CF3-40D3-BF42-A0A95035EE8C}.Release|Win32.Build.0 = Release|Win32
- {86CB2525-0CF3-40D3-BF42-A0A95035EE8C}.Release|x64.ActiveCfg = Release|x64
- {86CB2525-0CF3-40D3-BF42-A0A95035EE8C}.Release|x64.Build.0 = Release|x64
- {86CB2525-0CF3-40D3-BF42-A0A95035EE8C}.Shipping|Win32.ActiveCfg = Shipping|Win32
- {86CB2525-0CF3-40D3-BF42-A0A95035EE8C}.Shipping|Win32.Build.0 = Shipping|Win32
- {86CB2525-0CF3-40D3-BF42-A0A95035EE8C}.Shipping|x64.ActiveCfg = Shipping|x64
- {86CB2525-0CF3-40D3-BF42-A0A95035EE8C}.Shipping|x64.Build.0 = Shipping|x64
- {15CBFEFF-7965-41F5-B4E2-21E8795C9159}.Debug|Win32.ActiveCfg = Debug|Win32
- {15CBFEFF-7965-41F5-B4E2-21E8795C9159}.Debug|Win32.Build.0 = Debug|Win32
- {15CBFEFF-7965-41F5-B4E2-21E8795C9159}.Debug|x64.ActiveCfg = Debug|x64
- {15CBFEFF-7965-41F5-B4E2-21E8795C9159}.Debug|x64.Build.0 = Debug|x64
- {15CBFEFF-7965-41F5-B4E2-21E8795C9159}.Release|Win32.ActiveCfg = Release|Win32
- {15CBFEFF-7965-41F5-B4E2-21E8795C9159}.Release|Win32.Build.0 = Release|Win32
- {15CBFEFF-7965-41F5-B4E2-21E8795C9159}.Release|x64.ActiveCfg = Release|x64
- {15CBFEFF-7965-41F5-B4E2-21E8795C9159}.Release|x64.Build.0 = Release|x64
- {15CBFEFF-7965-41F5-B4E2-21E8795C9159}.Shipping|Win32.ActiveCfg = Release|Win32
- {15CBFEFF-7965-41F5-B4E2-21E8795C9159}.Shipping|Win32.Build.0 = Release|Win32
- {15CBFEFF-7965-41F5-B4E2-21E8795C9159}.Shipping|x64.ActiveCfg = Release|x64
- {15CBFEFF-7965-41F5-B4E2-21E8795C9159}.Shipping|x64.Build.0 = Release|x64
- {3A214E06-B95E-4D61-A291-1F8DF2EC10FD}.Debug|Win32.ActiveCfg = Debug|Win32
- {3A214E06-B95E-4D61-A291-1F8DF2EC10FD}.Debug|Win32.Build.0 = Debug|Win32
- {3A214E06-B95E-4D61-A291-1F8DF2EC10FD}.Debug|x64.ActiveCfg = Debug|x64
- {3A214E06-B95E-4D61-A291-1F8DF2EC10FD}.Debug|x64.Build.0 = Debug|x64
- {3A214E06-B95E-4D61-A291-1F8DF2EC10FD}.Release|Win32.ActiveCfg = Release|Win32
- {3A214E06-B95E-4D61-A291-1F8DF2EC10FD}.Release|Win32.Build.0 = Release|Win32
- {3A214E06-B95E-4D61-A291-1F8DF2EC10FD}.Release|x64.ActiveCfg = Release|x64
- {3A214E06-B95E-4D61-A291-1F8DF2EC10FD}.Release|x64.Build.0 = Release|x64
- {3A214E06-B95E-4D61-A291-1F8DF2EC10FD}.Shipping|Win32.ActiveCfg = Release|Win32
- {3A214E06-B95E-4D61-A291-1F8DF2EC10FD}.Shipping|Win32.Build.0 = Release|Win32
- {3A214E06-B95E-4D61-A291-1F8DF2EC10FD}.Shipping|x64.ActiveCfg = Release|x64
- {3A214E06-B95E-4D61-A291-1F8DF2EC10FD}.Shipping|x64.Build.0 = Release|x64
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {A22C1FEC-DB0E-418F-8851-2DCB8230E6D6}
- EndGlobalSection
-EndGlobal
diff --git a/engine/compilers/VisualStudio 2019/Torque 2D.vcxproj b/engine/compilers/VisualStudio 2019/Torque 2D.vcxproj
deleted file mode 100644
index d67248635..000000000
--- a/engine/compilers/VisualStudio 2019/Torque 2D.vcxproj
+++ /dev/null
@@ -1,1482 +0,0 @@
-
-
-
-
- Debug
- Win32
-
-
- Debug
- x64
-
-
- Release
- Win32
-
-
- Release
- x64
-
-
- Shipping
- Win32
-
-
- Shipping
- x64
-
-
-
- {1564A07D-230E-4C90-AEE6-52AC9A58D6C9}
- TorqueGame
- Torque2D
- 10.0
-
-
-
- Application
- false
- v142
-
-
- Application
- false
- v142
-
-
- Application
- false
- v142
-
-
- Application
- false
- v142
-
-
- Application
- false
- v142
-
-
- Application
- false
- v142
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- <_ProjectFileVersion>10.0.30319.1
- ../../../
- ../../Link/VC2012.$(Configuration).$(PlatformName)/$(ProjectName)/
-
-
- false
- ../../../
- ../../../
- ../../Link/VC2012.$(Configuration).$(PlatformName)/$(ProjectName)/
- ../../Link/VC2012.$(Configuration).$(PlatformName)/$(ProjectName)/
- false
- false
- false
- false
- Torque2D_DEBUG
- Torque2D_DEBUG
- Torque2D
- Torque2D
- Torque2D
- Torque2D
-
-
- ../../../
- ../../Link/VC2012.$(Configuration).$(PlatformName)/$(ProjectName)/
-
-
- ../../../
- ../../Link/VC2012.$(Configuration).$(PlatformName)/$(ProjectName)/
-
-
-
- $(OutDir)Torque2D.tlb
-
-
-
-
- Disabled
- ../../Lib/libogg/include;../../Lib/libvorbis/include;../../Lib/LeapSDK/include;../../Lib/zlib;../../Lib/lpng;../../Lib/ljpeg;../../Lib/openal/win32;../../source;../../source/persistence/rapidjson/include;../../source/persistence/libjson;../../source/testing/googleTest;../../source/testing/googleTest/include;../../source/spine;%(AdditionalIncludeDirectories)
- TORQUE_DEBUG;TORQUE_DEBUG_GUARD;_CRT_SECURE_NO_DEPRECATE;UNICODE;%(PreprocessorDefinitions)
- EnableFastChecks
- MultiThreaded
- false
- true
-
-
- $(OutDir)
- $(IntDir)$(ProjectName).pdb
- false
- true
- EditAndContinue
- CompileAsCpp
- true
- Level3
- 4800;4100;4127;4512
- false
- true
-
-
- _DEBUG;%(PreprocessorDefinitions)
- 0x0409
- ../../Lib/MSPlatformSDK/Include;%(AdditionalIncludeDirectories)
-
-
- COMCTL32.LIB;COMDLG32.LIB;USER32.LIB;ADVAPI32.LIB;GDI32.LIB;RPCRT4.LIB;WINMM.LIB;WS2_32.LIB;vfw32.lib;Imm32.lib;shell32.lib;shlwapi.lib;ole32.lib;%(AdditionalDependencies)
- ../../../Torque2D_DEBUG.exe
- true
- ../../Lib/unicode;../../lib/MSPlatformSDK/Lib;%(AdditionalLibraryDirectories)
- LIBC;LIBCD;LIBCMTD;%(IgnoreSpecificDefaultLibraries)
- true
- $(IntDir)$(ProjectName).pdb
- Windows
- false
-
-
- MachineX86
- false
-
-
-
-
- $(OutDir)Torque2D.tlb
-
-
-
-
- Disabled
- ../../Lib/libogg/include;../../Lib/libvorbis/include;../../Lib/LeapSDK/include;../../Lib/zlib;../../Lib/lpng;../../Lib/ljpeg;../../Lib/openal/win32;../../source;../../source/persistence/rapidjson/include;../../source/persistence/libjson;../../source/testing/googleTest;../../source/testing/googleTest/include;../../source/spine;%(AdditionalIncludeDirectories)
- TORQUE_DEBUG;TORQUE_DEBUG_GUARD;_CRT_SECURE_NO_DEPRECATE;UNICODE;%(PreprocessorDefinitions)
- EnableFastChecks
- MultiThreaded
- false
- true
-
-
- $(OutDir)
- $(IntDir)$(ProjectName).pdb
- false
- true
- ProgramDatabase
- CompileAsCpp
- true
- Level3
- 4800;4100;4127;4512
- false
- true
- true
-
-
- _DEBUG;%(PreprocessorDefinitions)
- 0x0409
- ../../Lib/MSPlatformSDK/Include;%(AdditionalIncludeDirectories)
-
-
- COMCTL32.LIB;COMDLG32.LIB;USER32.LIB;ADVAPI32.LIB;GDI32.LIB;RPCRT4.LIB;WINMM.LIB;WS2_32.LIB;vfw32.lib;Imm32.lib;shell32.lib;shlwapi.lib;ole32.lib;%(AdditionalDependencies)
- ../../../Torque2D_DEBUG.exe
- false
- ../../Lib/unicode;../../lib/MSPlatformSDK/Lib;%(AdditionalLibraryDirectories)
- LIBC;%(IgnoreSpecificDefaultLibraries)
- true
- $(IntDir)$(ProjectName).pdb
- Windows
- false
-
-
- false
- /NODEFAULTLIB:libcmtd.lib /NODEFAULTLIB:msvcrtd.lib %(AdditionalOptions)
-
-
-
-
- $(OutDir)Torque2D.tlb
-
-
-
-
- MinSpace
- OnlyExplicitInline
- ../../Lib/libogg/include;../../Lib/libvorbis/include;../../Lib/LeapSDK/include;../../Lib/zlib;../../Lib/lpng;../../Lib/ljpeg;../../Lib/openal/win32;../../source;../../source/persistence/rapidjson/include;../../source/persistence/libjson;../../source/testing/googleTest;../../source/testing/googleTest/include;../../source/spine;%(AdditionalIncludeDirectories)
- UNICODE;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- $(OutDir)
- $(IntDir)$(ProjectName).pdb
- false
- Level3
- true
- ProgramDatabase
- CompileAsCpp
- true
- 4800;4100;4127;4512
- true
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
- ../../Lib/MSPlatformSDK/Include;%(AdditionalIncludeDirectories)
-
-
- COMCTL32.LIB;COMDLG32.LIB;USER32.LIB;ADVAPI32.LIB;GDI32.LIB;WINMM.LIB;WS2_32.LIB;vfw32.lib;Imm32.lib;shell32.lib;shlwapi.lib;ole32.lib;RPCRT4.LIB;%(AdditionalDependencies)
- ../../../Torque2D.exe
- false
- ../../Lib/unicode;../../Lib/MSPlatformSDK/Lib;%(AdditionalLibraryDirectories)
- LIBC;%(IgnoreSpecificDefaultLibraries)
- true
- $(IntDir)$(ProjectName).pdb
- Windows
- false
-
-
- MachineX86
- false
- HighestAvailable
- false
-
-
-
-
-
-
-
-
-
-
-
-
-
- $(OutDir)Torque2D.tlb
-
-
-
-
- MinSpace
- OnlyExplicitInline
- ../../Lib/libogg/include;../../Lib/libvorbis/include;../../Lib/LeapSDK/include;../../Lib/zlib;../../Lib/lpng;../../Lib/ljpeg;../../Lib/openal/win32;../../source;../../source/persistence/rapidjson/include;../../source/persistence/libjson;../../source/testing/googleTest;../../source/testing/googleTest/include;../../source/spine;%(AdditionalIncludeDirectories)
- UNICODE;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- $(OutDir)
- $(IntDir)$(ProjectName).pdb
- false
- Level3
- true
- ProgramDatabase
- CompileAsCpp
- true
- 4800;4100;4127;4512
- true
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
- ../../Lib/MSPlatformSDK/Include;%(AdditionalIncludeDirectories)
-
-
- COMCTL32.LIB;COMDLG32.LIB;USER32.LIB;ADVAPI32.LIB;GDI32.LIB;WINMM.LIB;WS2_32.LIB;vfw32.lib;Imm32.lib;shell32.lib;shlwapi.lib;ole32.lib;RPCRT4.LIB;%(AdditionalDependencies)
- ../../../Torque2D.exe
- false
- ../../Lib/unicode;../../Lib/MSPlatformSDK/Lib;%(AdditionalLibraryDirectories)
- LIBC;%(IgnoreSpecificDefaultLibraries)
- true
- $(IntDir)$(ProjectName).pdb
- Windows
- false
-
-
- false
- HighestAvailable
- false
-
-
-
-
-
-
-
-
-
-
-
-
-
- $(OutDir)Torque2D.tlb
-
-
-
-
- MinSpace
- OnlyExplicitInline
- ../../Lib/LeapSDK/include;../../Lib/zlib;../../Lib/lpng;../../Lib/ljpeg;../../Lib/openal/win32;../../Source;../../Source/persistence/rapidjson/include;../../Source/persistence/libjson;%(AdditionalIncludeDirectories)
- TORQUE_SHIPPING;UNICODE;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- $(OutDir)
- $(IntDir)$(ProjectName).pdb
- false
- Level3
- true
- ProgramDatabase
- CompileAsCpp
- true
- 4800;4100;4127;4512
- true
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
- ../../Lib/MSPlatformSDK/Include;%(AdditionalIncludeDirectories)
-
-
- COMCTL32.LIB;COMDLG32.LIB;USER32.LIB;ADVAPI32.LIB;GDI32.LIB;WINMM.LIB;WS2_32.LIB;vfw32.lib;Imm32.lib;shell32.lib;shlwapi.lib;ole32.lib;RPCRT4.LIB;%(AdditionalDependencies)
- ../../../Torque2D.exe
- false
- ../../Lib/unicode;../../Lib/MSPlatformSDK/Lib;%(AdditionalLibraryDirectories)
- LIBC;%(IgnoreSpecificDefaultLibraries)
- false
- $(IntDir)$(ProjectName).pdb
- Windows
- false
-
-
- MachineX86
- false
- HighestAvailable
- false
-
-
-
-
-
-
-
-
-
-
-
-
-
- $(OutDir)Torque2D.tlb
-
-
-
-
- MinSpace
- OnlyExplicitInline
- ../../Lib/LeapSDK/include;../../Lib/zlib;../../Lib/lpng;../../Lib/ljpeg;../../Lib/openal/win32;../../Source;../../Source/persistence/rapidjson/include;../../Source/persistence/libjson;%(AdditionalIncludeDirectories)
- TORQUE_SHIPPING;UNICODE;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- $(OutDir)
- $(IntDir)$(ProjectName).pdb
- false
- Level3
- true
- ProgramDatabase
- CompileAsCpp
- true
- 4800;4100;4127;4512
- true
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
- ../../Lib/MSPlatformSDK/Include;%(AdditionalIncludeDirectories)
-
-
- COMCTL32.LIB;COMDLG32.LIB;USER32.LIB;ADVAPI32.LIB;GDI32.LIB;WINMM.LIB;WS2_32.LIB;vfw32.lib;Imm32.lib;shell32.lib;shlwapi.lib;ole32.lib;RPCRT4.LIB;%(AdditionalDependencies)
- ../../../Torque2D.exe
- false
- ../../Lib/unicode;../../Lib/MSPlatformSDK/Lib;%(AdditionalLibraryDirectories)
- LIBC;%(IgnoreSpecificDefaultLibraries)
- false
- $(IntDir)$(ProjectName).pdb
- Windows
- false
-
-
- false
- HighestAvailable
- false
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {15cbfeff-7965-41f5-b4e2-21e8795c9159}
- false
- false
- false
- true
- false
-
-
- {3a214e06-b95e-4d61-a291-1f8df2ec10fd}
- false
- false
- false
- true
- false
-
-
- {0b07ba94-aa53-4fd4-adb4-79ec2da53b36}
- false
-
-
- {af1179e3-a838-46a3-a427-1e62aa4c52f4}
- false
-
-
- {86cb2525-0cf3-40d3-bf42-a0a95035ee8c}
- false
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/engine/compilers/VisualStudio 2019/Torque 2D.vcxproj.filters b/engine/compilers/VisualStudio 2019/Torque 2D.vcxproj.filters
deleted file mode 100644
index b6c4abc3b..000000000
--- a/engine/compilers/VisualStudio 2019/Torque 2D.vcxproj.filters
+++ /dev/null
@@ -1,3109 +0,0 @@
-
-
-
-
- {b90d3c2c-9694-4051-8b22-325c00d37951}
-
-
- {57099cfd-2f9b-468f-88a5-df769a6235b0}
-
-
- {8b011278-0d15-4ba9-ba57-24056b918233}
-
-
- {b0a55e16-3a1f-4eb5-8f5b-3396d456be74}
-
-
- {71933100-a4a2-464e-bb3f-bb04d9f078af}
-
-
- {7824869e-4359-4413-a615-38c8534d641b}
-
-
- {baaa5934-0805-46eb-be6e-ae5643535b4b}
-
-
- {6a14bfc6-1f7c-4527-91d3-ccf897ff4002}
-
-
- {bfd4849e-66c6-450b-9a01-0873aa28e21a}
-
-
- {1f640da6-77a4-4176-a018-9bfad5becf95}
-
-
- {4faf4110-842d-4178-81ca-0375f3804cac}
-
-
- {2f939a2b-3cd9-4e67-8311-bf751093f2a5}
-
-
- {84907fc2-76e8-4a3b-9df4-76451387d3f9}
-
-
- {e2574a74-ff0d-401d-818d-96e6c155a4b4}
-
-
- {18c9fe85-da18-45d5-9573-a71ce9373961}
-
-
- {4f9157b1-a024-450f-a352-1849cf2bddcc}
-
-
- {14385fbb-f8af-4849-bdd9-8c8f67d3928e}
-
-
- {9ec04d21-b122-45be-bd70-2869fa9cdd8e}
-
-
- {09cd4b77-b9b4-41f4-b17e-3ac25c27beee}
-
-
- {47a0d755-122c-490b-b11c-d06a74fe2ab2}
-
-
- {ec2d3f1a-e3d1-49d5-98d6-a3084e37d077}
-
-
- {b3b96a6a-462f-4c7d-92a5-009cabbe94a3}
-
-
- {d55ba677-1863-4206-92b7-1df94c48df84}
-
-
- {d016b107-aefd-41ac-a04c-6b75caaf114d}
-
-
- {d48f2bd5-39e5-4fdb-8472-79839fcd7e21}
-
-
- {187e7671-960f-49a7-898b-a10cf5900751}
-
-
- {c17330c5-953e-4e71-9acb-1b7806b3c932}
-
-
- {92e20c7c-410f-4df4-a9b9-7662dac44699}
-
-
- {513dc4d3-ba16-414a-8576-16b1dba4f6e8}
-
-
- {c48781cd-5e6a-4ffe-b866-e64582f00d6d}
-
-
- {eee14c00-6a21-4fa8-9339-572a11819062}
-
-
- {fcfa64cc-dd46-41c1-9ada-8b4a15051285}
-
-
- {fa588b4f-a8dd-4615-beee-dab4db372118}
-
-
- {27eea104-6e41-4cfb-bc50-56f9157081ce}
-
-
- {0688e2d7-d313-4a35-b90d-1df191825361}
-
-
- {04e382a2-a0a9-40c9-b227-a46e345fb032}
-
-
- {5c7d23f1-7194-4a6f-8bd3-fd5b6a0beb4f}
-
-
- {66757e8b-c204-4b92-92ab-19278019d17d}
-
-
- {23024162-0d6f-4259-8dac-a3a621d55065}
-
-
- {c1b891be-db08-48e9-b52a-f0cf3bac185e}
-
-
- {87703b23-784c-4769-b085-57c72b1b536e}
-
-
- {a2568a68-f396-4b3e-9ed6-bf73656752d3}
-
-
- {d13f86bc-f4dd-4329-8337-dd41e2db04a1}
-
-
- {f5858e14-ed7e-4887-a22e-4123ab41a6f1}
-
-
- {a9dbe516-9ed6-43a6-987e-6f2bc6f59928}
-
-
- {d98ec67b-b10a-4c7b-bb2f-a6394a861b67}
-
-
- {04d21b23-41a0-44a7-810e-f31c81fd5c81}
-
-
- {3f7e4a0c-c3da-4972-a9bc-9cb0e0ce622e}
-
-
- {d77ee12f-a922-4d39-9e6c-2ad87a5b4a4e}
-
-
- {9da3da8f-0660-4a53-b5cf-3994c2dca7e3}
-
-
- {78695f15-84c6-4505-a999-716fe79bdabe}
-
-
- {e706ea06-aa20-4487-a010-e0d00adc658f}
-
-
- {fa7e2f20-cd6d-4118-8a74-9f9c95119064}
-
-
- {7b04617f-42ef-4238-9a98-9d8309b64c93}
-
-
- {57e1271d-4358-4180-b168-4b9c2cbac907}
-
-
- {a9e97335-bed5-4f6a-9959-12f5f41dbdcb}
-
-
- {e11e344e-6418-4ed0-980a-77d66cd64d65}
-
-
- {1eb9e730-583b-4aa4-ac25-b83960799ba4}
-
-
- {30e1ec13-118b-4d50-8e04-76e76fcfdc01}
-
-
- {4d0b6ff3-58d2-4952-bd14-915a50a3b568}
-
-
- {9cbec746-dd4c-4b4c-b11f-37a126ea2c38}
-
-
- {427672e0-f4a2-45a9-b44c-92d190e961aa}
-
-
- {e1ff3412-7343-4dfb-bc99-bce90655557b}
-
-
- {cc1c1416-376b-4686-a4ac-21d1a35c9390}
-
-
- {447ecd65-a7a2-4e18-9c55-b53356c6f7a9}
-
-
- {b2903a96-6c49-4961-82a8-f1832989d4a4}
-
-
- {598766e4-7dc1-45b8-8acf-f133f4fced82}
-
-
- {1a2a7ebc-eda6-4a67-b9ab-bc9f437b5d5c}
-
-
-
-
- audio
-
-
- audio
-
-
- audio
-
-
- audio
-
-
- audio
-
-
- component
-
-
- component
-
-
- component\behaviors
-
-
- component\behaviors
-
-
- component\behaviors
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- game
-
-
- game
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- persistence
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform\menus
-
-
- platform\nativeDialogs
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32\menus
-
-
- platformWin32\nativeDialogs
-
-
- platformWin32\nativeDialogs
-
-
- platformWin32\threads
-
-
- platformWin32\threads
-
-
- gui\buttons
-
-
- gui\buttons
-
-
- gui\buttons
-
-
- gui\containers
-
-
- gui\containers
-
-
- gui\containers
-
-
- gui\containers
-
-
- gui\editor
-
-
- gui\editor
-
-
- gui\editor
-
-
- gui\editor
-
-
- gui\editor
-
-
- Box2D\Collision
-
-
- Box2D\Collision
-
-
- Box2D\Collision
-
-
- Box2D\Collision
-
-
- Box2D\Collision
-
-
- Box2D\Collision
-
-
- Box2D\Collision
-
-
- Box2D\Collision
-
-
- Box2D\Collision\Shapes
-
-
- Box2D\Collision\Shapes
-
-
- Box2D\Collision\Shapes
-
-
- Box2D\Collision\Shapes
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Dynamics
-
-
- Box2D\Dynamics
-
-
- Box2D\Dynamics
-
-
- Box2D\Dynamics
-
-
- Box2D\Dynamics
-
-
- Box2D\Dynamics
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Rope
-
-
- persistence\taml
-
-
- module
-
-
- module
-
-
- persistence\tinyXML
-
-
- persistence\tinyXML
-
-
- persistence\tinyXML
-
-
- persistence\tinyXML
-
-
- module
-
-
- assets
-
-
- assets
-
-
- assets
-
-
- assets
-
-
- assets
-
-
- gui\language
-
-
- graphics
-
-
- graphics
-
-
- graphics
-
-
- graphics
-
-
- graphics
-
-
- graphics
-
-
- graphics
-
-
- graphics
-
-
- messaging
-
-
- messaging
-
-
- messaging
-
-
- messaging
-
-
- messaging
-
-
- game
-
-
- debug
-
-
- math
-
-
- input
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- graphics
-
-
- collection
-
-
- collection
-
-
- collection
-
-
- io
-
-
- io
-
-
- io
-
-
- io
-
-
- io
-
-
- io
-
-
- io
-
-
- io
-
-
- io
-
-
- io
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- memory
-
-
- algorithm
-
-
- algorithm
-
-
- game
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- network
-
-
- network
-
-
- network
-
-
- network
-
-
- network
-
-
- network
-
-
- network
-
-
- network
-
-
- network
-
-
- network
-
-
- network
-
-
- network
-
-
- string
-
-
- string
-
-
- string
-
-
- string
-
-
- string
-
-
- io\resource
-
-
- io\resource
-
-
- collection
-
-
- collection
-
-
- platform
-
-
- network
-
-
- debug
-
-
- string
-
-
- network
-
-
- network
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- audio
-
-
- graphics
-
-
- graphics
-
-
- graphics
-
-
- graphics
-
-
- persistence\taml
-
-
- delegates
-
-
- 2d\assets
-
-
- 2d\assets
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- 2d\gui
-
-
- 2d\gui
-
-
- 2d\core
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- 2d\gui
-
-
- debug\remote
-
-
- debug\remote
-
-
- debug\remote
-
-
- graphics
-
-
- platform
-
-
- network
-
-
- testing\tests
-
-
- testing
-
-
- testing\tests
-
-
- testing\tests
-
-
- platform\nativeDialogs
-
-
- platformWin32\nativeDialogs
-
-
- 2d\assets
-
-
- 2d\assets
-
-
- 2d\assets
-
-
- 2d\assets
-
-
- 2d\sceneobject
-
-
- 2d\core
-
-
- assets
-
-
- assets
-
-
- persistence\taml
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\controllers
-
-
- 2d\controllers
-
-
- 2d\controllers\core
-
-
- 2d\controllers\core
-
-
- 2d\controllers
-
-
- 2d\core
-
-
- 2d\experimental\composites
-
-
- 2d\core
-
-
- 2d\core
-
-
- persistence\taml\binary
-
-
- persistence\taml\binary
-
-
- persistence\taml\json
-
-
- persistence\taml\json
-
-
- persistence\taml\xml
-
-
- persistence\taml\xml
-
-
- persistence\taml\xml
-
-
- persistence\taml\json
-
-
- gui\containers
-
-
- console
-
-
- audio
-
-
- io
-
-
- math
-
-
- memory
-
-
- console
-
-
- math
-
-
- audio
-
-
- 2d\assets
-
-
- 2d\sceneobject
-
-
- bitmapFont
-
-
- bitmapFont
-
-
- audio
-
-
-
-
-
-
- gui\containers
-
-
- Box2D\Particle
-
-
- Box2D\Particle
-
-
- Box2D\Particle
-
-
- Box2D\Particle
-
-
- Box2D\Particle
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- graphics
-
-
- console
-
-
- math
-
-
- gui\containers
-
-
- gui\containers
-
-
- gui\containers
-
-
- 2d\editorToy
-
-
- 2d\editorToy
-
-
- gui\buttons
-
-
- gui\editor
-
-
- gui\containers
-
-
- gui\editor
-
-
- algorithm
-
-
- math\noise
-
-
- algorithm
-
-
- math\noise
-
-
- gui
-
-
- gui
-
-
- gui\containers
-
-
-
-
- audio
-
-
- audio
-
-
- audio
-
-
- audio
-
-
- audio
-
-
- audio
-
-
- component
-
-
- component
-
-
- component\behaviors
-
-
- component\behaviors
-
-
- component\behaviors
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- game
-
-
- game
-
-
- game
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- persistence
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform\menus
-
-
- platform\nativeDialogs
-
-
- platform\nativeDialogs
-
-
- platform\threads
-
-
- platform\threads
-
-
- platform\threads
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32\nativeDialogs
-
-
- gui\buttons
-
-
- gui\buttons
-
-
- gui\buttons
-
-
- gui\containers
-
-
- gui\containers
-
-
- gui\containers
-
-
- gui\containers
-
-
- gui\editor
-
-
- gui\editor
-
-
- gui\editor
-
-
- gui\editor
-
-
- gui\editor
-
-
- Box2D
-
-
- Box2D\Collision
-
-
- Box2D\Collision
-
-
- Box2D\Collision
-
-
- Box2D\Collision
-
-
- Box2D\Collision
-
-
- Box2D\Collision\Shapes
-
-
- Box2D\Collision\Shapes
-
-
- Box2D\Collision\Shapes
-
-
- Box2D\Collision\Shapes
-
-
- Box2D\Collision\Shapes
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Dynamics
-
-
- Box2D\Dynamics
-
-
- Box2D\Dynamics
-
-
- Box2D\Dynamics
-
-
- Box2D\Dynamics
-
-
- Box2D\Dynamics
-
-
- Box2D\Dynamics
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Rope
-
-
- component\behaviors
-
-
- component\behaviors
-
-
- component\behaviors
-
-
- component\behaviors
-
-
- persistence\taml
-
-
- persistence\taml
-
-
- persistence\taml
-
-
- module
-
-
- module
-
-
- module
-
-
- persistence\tinyXML
-
-
- persistence\tinyXML
-
-
- module
-
-
- module
-
-
- assets
-
-
- assets
-
-
- assets
-
-
- assets
-
-
- assets
-
-
- assets
-
-
- assets
-
-
- assets
-
-
- assets
-
-
- assets
-
-
- assets
-
-
- assets
-
-
- assets
-
-
- module
-
-
- module
-
-
- assets
-
-
- assets
-
-
- gui\language
-
-
- graphics
-
-
- graphics
-
-
- graphics
-
-
- graphics
-
-
- messaging
-
-
- messaging
-
-
- messaging
-
-
- messaging
-
-
- messaging
-
-
- game
-
-
- debug
-
-
- math
-
-
- input
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- graphics
-
-
- collection
-
-
- collection
-
-
- collection
-
-
- collection
-
-
- collection
-
-
- collection
-
-
- collection
-
-
- collection
-
-
- collection
-
-
- collection
-
-
- collection
-
-
- io
-
-
- io
-
-
- io
-
-
- io
-
-
- io
-
-
- io
-
-
- io
-
-
- io
-
-
- io
-
-
- io
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- memory
-
-
- memory
-
-
- collection
-
-
- collection
-
-
- algorithm
-
-
- algorithm
-
-
- algorithm
-
-
- collection
-
-
- game
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- network
-
-
- network
-
-
- network
-
-
- network
-
-
- network
-
-
- network
-
-
- network
-
-
- network
-
-
- string
-
-
- string
-
-
- string
-
-
- string
-
-
- string
-
-
- memory
-
-
- io\resource
-
-
- memory
-
-
- collection
-
-
- collection
-
-
- collection
-
-
- platform
-
-
- network
-
-
- debug
-
-
- string
-
-
- network
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- audio
-
-
- graphics
-
-
- graphics
-
-
- graphics
-
-
- graphics
-
-
- graphics
-
-
- persistence\taml
-
-
- delegates
-
-
- delegates
-
-
- delegates
-
-
- 2d\assets
-
-
- 2d\assets
-
-
- 2d\assets
-
-
- 2d\assets
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- 2d\gui
-
-
- 2d\gui
-
-
- 2d\gui
-
-
- 2d\core
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- algorithm
-
-
- 2d\gui
-
-
- 2d\gui
-
-
- debug\remote
-
-
- debug\remote
-
-
- debug\remote
-
-
- debug\remote
-
-
- debug\remote
-
-
- debug\remote
-
-
- graphics
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- game
-
-
- network
-
-
- testing
-
-
- platform
-
-
- platform
-
-
- platformWin32
-
-
- persistence\taml
-
-
- 2d\assets
-
-
- 2d\assets
-
-
- 2d\assets
-
-
- 2d\assets
-
-
- 2d\assets
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\assets
-
-
- 2d\core
-
-
- assets
-
-
- assets
-
-
- persistence\taml
-
-
- sim
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\controllers
-
-
- 2d\controllers
-
-
- 2d\controllers
-
-
- 2d\controllers
-
-
- 2d\controllers\core
-
-
- 2d\controllers\core
-
-
- 2d\controllers\core
-
-
- 2d\controllers\core
-
-
- 2d\controllers\core
-
-
- 2d\controllers
-
-
- 2d\controllers
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\experimental\composites
-
-
- 2d\experimental\composites
-
-
- 2d\core
-
-
- 2d\core
-
-
- persistence\rapidjson
-
-
- persistence\rapidjson
-
-
- persistence\rapidjson
-
-
- persistence\rapidjson
-
-
- persistence\rapidjson
-
-
- persistence\rapidjson
-
-
- persistence\rapidjson
-
-
- persistence\rapidjson
-
-
- persistence\rapidjson
-
-
- persistence\rapidjson
-
-
- persistence\rapidjson
-
-
- persistence\rapidjson
-
-
- persistence\rapidjson\internal
-
-
- persistence\rapidjson\internal
-
-
- persistence\rapidjson\internal
-
-
- persistence\taml\binary
-
-
- persistence\taml\binary
-
-
- persistence\taml\json
-
-
- persistence\taml\json
-
-
- persistence\taml\xml
-
-
- persistence\taml\xml
-
-
- persistence\taml\xml
-
-
- persistence\taml
-
-
- persistence\taml
-
-
- persistence\taml\json
-
-
- gui\containers
-
-
- sim
-
-
- input
-
-
- gui
-
-
- platform
-
-
- string
-
-
- console
-
-
- platform
-
-
- game
-
-
- console
-
-
- sim
-
-
- sim
-
-
- persistence
-
-
- messaging
-
-
- io
-
-
- platform\nativeDialogs
-
-
- console
-
-
- network
-
-
- game
-
-
- platform\menus
-
-
- io
-
-
- collection
-
-
- console
-
-
- messaging
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- component
-
-
- component
-
-
- io
-
-
- network
-
-
- messaging
-
-
- network
-
-
- network
-
-
- sim
-
-
- graphics
-
-
- console
-
-
- graphics
-
-
- network
-
-
- platform
-
-
- platform
-
-
- debug
-
-
- debug
-
-
- network
-
-
- network
-
-
- io\resource
-
-
- graphics
-
-
-
- platformWin32
-
-
- network
-
-
- console
-
-
- console
-
-
- console
-
-
- platform
-
-
- graphics
-
-
- graphics
-
-
- platformWin32
-
-
- platformWin32
-
-
- 2d\core
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- network
-
-
- network
-
-
- network
-
-
- testing
-
-
- platform\nativeDialogs
-
-
- sim
-
-
- platformWin32
-
-
- string
-
-
- io\zip
-
-
- gui
-
-
- console
-
-
- audio
-
-
- platformWin32
-
-
- 2d\assets
-
-
- 2d\assets
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- bitmapFont
-
-
- bitmapFont
-
-
- bitmapFont
-
-
- bitmapFont
-
-
- audio
-
-
-
-
-
-
-
-
-
-
- gui\buttons
-
-
- gui\containers
-
-
-
- gui\containers
-
-
- gui\containers
-
-
- gui\containers
-
-
- Box2D\Particle
-
-
- Box2D\Particle
-
-
- Box2D\Particle
-
-
- Box2D\Particle
-
-
- Box2D\Particle
-
-
- Box2D\Particle
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- graphics
-
-
- graphics
-
-
- console
-
-
- console
-
-
- math
-
-
- gui\containers
-
-
- gui\containers
-
-
- gui\containers
-
-
- gui\containers
-
-
- gui\buttons
-
-
- gui\containers
-
-
- gui\containers
-
-
- gui\containers
-
-
- 2d\editorToy
-
-
- 2d\editorToy
-
-
- 2d\editorToy
-
-
- gui
-
-
- gui\containers
-
-
- gui
-
-
- gui\buttons
-
-
- gui\buttons
-
-
- gui\editor
-
-
- gui\editor
-
-
- gui
-
-
- gui\containers
-
-
- gui\editor
-
-
- gui\editor
-
-
- algorithm
-
-
- math\noise
-
-
- math\noise
-
-
- algorithm
-
-
- math\noise
-
-
- math\noise
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui\containers
-
-
- gui\containers
-
-
- graphics
-
-
-
-
-
-
-
-
-
-
- Box2D\Particle
-
-
-
\ No newline at end of file
diff --git a/engine/compilers/VisualStudio 2019/libogg.vcxproj b/engine/compilers/VisualStudio 2019/libogg.vcxproj
deleted file mode 100644
index dd440c4c9..000000000
--- a/engine/compilers/VisualStudio 2019/libogg.vcxproj
+++ /dev/null
@@ -1,185 +0,0 @@
-
-
-
-
- Debug
- Win32
-
-
- Debug
- x64
-
-
- Release
- Win32
-
-
- Release
- x64
-
-
-
-
-
-
-
-
-
-
-
- {15CBFEFF-7965-41F5-B4E2-21E8795C9159}
- libogg
- Win32Proj
- 10.0
-
-
-
- StaticLibrary
- Unicode
- false
- v142
-
-
- StaticLibrary
- Unicode
- v142
-
-
- StaticLibrary
- Unicode
- true
- v142
-
-
- StaticLibrary
- Unicode
- v142
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- <_ProjectFileVersion>10.0.30319.1
- .\../../Link/Debug\
- .\../../Link/Debug/libogg\
- .\../../Link/Debug\
- .\../../Link/Debug/libogg\
- .\../../Link/Release\
- .\../../Link/Release/libogg\
- $(SolutionDir)$(Platform)\$(Configuration)\
- $(Platform)\$(Configuration)\
-
-
- $(ProjectName)_DEBUG
-
-
-
- Disabled
- ..\..\Lib\libogg\include;%(AdditionalIncludeDirectories)
- WIN32;_DEBUG;_WINDOWS;_USRDLL;LIBOGG_EXPORTS;%(PreprocessorDefinitions)
- false
- EnableFastChecks
- MultiThreadedDebug
-
-
- Level3
- EditAndContinue
- CompileAsC
- Cdecl
- .\../../Link/Debug/libogg/
- .\../../Link/Debug/libogg/
- .\../../Link/Debug/libogg/
- .\../../Link/Debug/lpng/libogg.pch
-
-
-
-
- X64
-
-
- Disabled
- ..\..\Lib\libogg\include;%(AdditionalIncludeDirectories)
- WIN32;_DEBUG;_WINDOWS;_USRDLL;LIBOGG_EXPORTS;%(PreprocessorDefinitions)
- true
- EnableFastChecks
- MultiThreadedDebug
-
-
- Level3
- ProgramDatabase
- CompileAsC
- Cdecl
- .\../../Link/Debug/libogg/
- .\../../Link/Debug/libogg/
- .\../../Link/Debug/libogg/
-
-
-
-
- MaxSpeed
- AnySuitable
- true
- Speed
- ..\..\Lib\libogg\include;%(AdditionalIncludeDirectories)
- WIN32;NDEBUG;_WINDOWS;_USRDLL;LIBOGG_EXPORTS;%(PreprocessorDefinitions)
- true
-
-
- MultiThreaded
- false
-
-
- Level3
-
-
- CompileAsC
- 4244;%(DisableSpecificWarnings)
- Cdecl
- .\../../Link/Release/libogg/
- .\../../Link/Release/libogg/
- .\../../Link/Release/libogg/
-
-
-
-
- X64
-
-
- MaxSpeed
- AnySuitable
- true
- Speed
- ..\..\Lib\libogg\include;%(AdditionalIncludeDirectories)
- WIN32;NDEBUG;_WINDOWS;_USRDLL;LIBOGG_EXPORTS;%(PreprocessorDefinitions)
- true
-
-
- MultiThreaded
- false
-
-
- Level4
-
-
- CompileAsC
- 4244;%(DisableSpecificWarnings)
- Cdecl
-
-
-
-
-
-
\ No newline at end of file
diff --git a/engine/compilers/VisualStudio 2019/libvorbis.vcxproj b/engine/compilers/VisualStudio 2019/libvorbis.vcxproj
deleted file mode 100644
index a6ecf4fd5..000000000
--- a/engine/compilers/VisualStudio 2019/libvorbis.vcxproj
+++ /dev/null
@@ -1,254 +0,0 @@
-
-
-
-
- Debug
- Win32
-
-
- Debug
- x64
-
-
- Release
- Win32
-
-
- Release
- x64
-
-
-
- {3A214E06-B95E-4D61-A291-1F8DF2EC10FD}
- libvorbis
- Win32Proj
- 10.0
-
-
-
- StaticLibrary
- Unicode
- false
- v142
-
-
- StaticLibrary
- Unicode
- v142
-
-
- StaticLibrary
- Unicode
- true
- v142
-
-
- StaticLibrary
- Unicode
- v142
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- <_ProjectFileVersion>10.0.30319.1
- .\../../Link/Debug\
- .\../../Link/Debug/libvorbis\
- .\../../Link/Debug\
- .\../../Link/Debug/libvorbis\
- .\../../Link/Release\
- .\../../Link/Release/libvorbis\
- .\../../Link/Release/
- .\../../Link/Release/libvorbis
-
-
- $(ProjectName)_DEBUG
-
-
-
- Disabled
- ..\..\Lib\libvorbis\lib;..\..\Lib\libvorbis\include;..\..\Lib\libogg\include;%(AdditionalIncludeDirectories)
- WIN32;_DEBUG;_WINDOWS;_USRDLL;LIBVORBIS_EXPORTS;%(PreprocessorDefinitions)
- false
- EnableFastChecks
- MultiThreadedDebug
-
-
- Level3
- EditAndContinue
- CompileAsC
- Cdecl
- .\../../Link/Debug/libvorbis/
- .\../../Link/Debug/libvorbis/
- .\../../Link/Debug/libvorbis/
- .\../../Link/Debug/lpng/libvorbis.pch
-
-
-
-
- X64
-
-
- Disabled
- ..\..\Lib\libvorbis\lib;..\..\Lib\libvorbis\include;..\..\Lib\libogg\include;%(AdditionalIncludeDirectories)
- WIN32;_DEBUG;_WINDOWS;_USRDLL;LIBVORBIS_EXPORTS;%(PreprocessorDefinitions)
- false
- EnableFastChecks
- MultiThreadedDebug
-
-
- Level3
- ProgramDatabase
- CompileAsC
- Cdecl
- .\../../Link/Debug/libvorbis/
- .\../../Link/Debug/libvorbis/
- 4244;4100;4267;4189;4305;4127;4706;%(DisableSpecificWarnings)
- AnySuitable
- true
- Speed
- false
- true
- false
- .\../../Link/Debug/libvorbis/
-
-
-
-
- Full
- AnySuitable
- true
- Speed
- ..\..\Lib\libvorbis\lib;..\..\Lib\libvorbis\include;..\..\Lib\libogg\include;%(AdditionalIncludeDirectories)
- WIN32;NDEBUG;_WINDOWS;_USRDLL;LIBVORBIS_EXPORTS;%(PreprocessorDefinitions)
- true
-
-
- MultiThreaded
- false
-
-
- Level3
- ProgramDatabase
- CompileAsC
- 4244;4100;4267;4189;4305;4127;4706;%(DisableSpecificWarnings)
- Cdecl
- .\../../Link/Release/libvorbis/
- .\../../Link/Release/libvorbis/
- .\../../Link/Release/libvorbis/
-
-
-
-
- X64
-
-
- Full
- AnySuitable
- true
- Speed
- ..\..\Lib\libvorbis\lib;..\..\Lib\libvorbis\include;..\..\Lib\libogg\include;%(AdditionalIncludeDirectories)
- WIN32;NDEBUG;_WINDOWS;_USRDLL;LIBVORBIS_EXPORTS;%(PreprocessorDefinitions)
- true
-
-
- MultiThreadedDLL
- false
-
-
- Level4
- ProgramDatabase
- CompileAsC
- 4244;4100;4267;4189;4305;4127;4706;%(DisableSpecificWarnings)
- Cdecl
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/engine/compilers/VisualStudio 2019/ljpeg.vcxproj b/engine/compilers/VisualStudio 2019/ljpeg.vcxproj
deleted file mode 100644
index ac315fb96..000000000
--- a/engine/compilers/VisualStudio 2019/ljpeg.vcxproj
+++ /dev/null
@@ -1,1128 +0,0 @@
-
-
-
-
- Debug
- Win32
-
-
- Debug
- x64
-
-
- Release
- Win32
-
-
- Release
- x64
-
-
- Shipping
- Win32
-
-
- Shipping
- x64
-
-
-
- {0B07BA94-AA53-4FD4-ADB4-79EC2DA53B36}
- 10.0
-
-
-
- StaticLibrary
- false
- v142
-
-
- StaticLibrary
- false
- v142
-
-
- StaticLibrary
- false
- v142
-
-
- StaticLibrary
- false
- v142
-
-
- StaticLibrary
- false
- v142
-
-
- StaticLibrary
- false
- v142
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- <_ProjectFileVersion>10.0.30319.1
- .\../../Link/Release\
- .\../../Link/Release\
- .\../../Link/Release/ljpeg\
- .\../../Link/Release/ljpeg\
- .\../../Link/Debug\
- .\../../Link/Debug/ljpeg\
- $(ProjectName)_DEBUG
- $(ProjectName)_DEBUG
-
-
-
- MaxSpeed
- OnlyExplicitInline
- ljpeg;%(AdditionalIncludeDirectories)
- _CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- .\../../Link/Release/ljpeg/ljpeg.pch
- .\../../Link/Release/ljpeg/
- .\../../Link/Release/ljpeg/
- .\../../Link/Release/ljpeg/
- Level3
- true
- Default
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- .\../../Link/Release\ljpeg.lib
- true
-
-
-
-
- MaxSpeed
- OnlyExplicitInline
- ljpeg;%(AdditionalIncludeDirectories)
- _CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- .\../../Link/Release/ljpeg/ljpeg.pch
- .\../../Link/Release/ljpeg/
- .\../../Link/Release/ljpeg/
- .\../../Link/Release/ljpeg/
- Level3
- true
- Default
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- .\../../Link/Release\ljpeg.lib
- true
-
-
-
-
- MaxSpeed
- OnlyExplicitInline
- ljpeg;%(AdditionalIncludeDirectories)
- _CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- .\../../Link/Release/ljpeg/ljpeg.pch
- .\../../Link/Release/ljpeg/
- .\../../Link/Release/ljpeg/
- .\../../Link/Release/ljpeg/
- Level3
- true
- Default
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- .\../../Link/Release\ljpeg.lib
- true
-
-
-
-
- MaxSpeed
- OnlyExplicitInline
- ljpeg;%(AdditionalIncludeDirectories)
- _CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- .\../../Link/Release/ljpeg/ljpeg.pch
- .\../../Link/Release/ljpeg/
- .\../../Link/Release/ljpeg/
- .\../../Link/Release/ljpeg/
- Level3
- true
- Default
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- .\../../Link/Release\ljpeg.lib
- true
-
-
-
-
- Disabled
- ljpeg;%(AdditionalIncludeDirectories)
- TORQUE_DEBUG;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- EnableFastChecks
- MultiThreadedDebug
- false
- true
-
-
- .\../../Link/Debug/ljpeg/ljpeg.pch
- .\../../Link/Debug/ljpeg/
- .\../../Link/Debug/ljpeg/
- .\../../Link/Debug/ljpeg/
- Level3
- true
- EditAndContinue
- Default
-
-
- _DEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- $(OutDir)$(TargetName)$(TargetExt)
- true
-
-
-
-
- Disabled
- ljpeg;%(AdditionalIncludeDirectories)
- TORQUE_DEBUG;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- EnableFastChecks
- MultiThreadedDebug
- false
- true
-
-
- .\../../Link/Debug/ljpeg/ljpeg.pch
- .\../../Link/Debug/ljpeg/
- .\../../Link/Debug/ljpeg/
- .\../../Link/Debug/ljpeg/
- Level3
- true
- ProgramDatabase
- Default
-
-
- _DEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- .\../../Link/Debug\ljpeg.lib
- true
-
-
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/engine/compilers/VisualStudio 2019/ljpeg.vcxproj.filters b/engine/compilers/VisualStudio 2019/ljpeg.vcxproj.filters
deleted file mode 100644
index e7df8bc60..000000000
--- a/engine/compilers/VisualStudio 2019/ljpeg.vcxproj.filters
+++ /dev/null
@@ -1,65 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/engine/compilers/VisualStudio 2019/lpng.vcxproj b/engine/compilers/VisualStudio 2019/lpng.vcxproj
deleted file mode 100644
index c8c4be1c2..000000000
--- a/engine/compilers/VisualStudio 2019/lpng.vcxproj
+++ /dev/null
@@ -1,316 +0,0 @@
-
-
-
-
- Debug
- Win32
-
-
- Debug
- x64
-
-
- Release
- Win32
-
-
- Release
- x64
-
-
- Shipping
- Win32
-
-
- Shipping
- x64
-
-
-
- {AF1179E3-A838-46A3-A427-1E62AA4C52F4}
- 10.0
-
-
-
- StaticLibrary
- false
- v142
-
-
- StaticLibrary
- false
- v142
-
-
- StaticLibrary
- false
- v142
-
-
- StaticLibrary
- false
- v142
-
-
- StaticLibrary
- false
- v142
-
-
- StaticLibrary
- false
- v142
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- <_ProjectFileVersion>10.0.30319.1
- .\../../Link/Debug\
- .\../../Link/Debug/lpng\
- .\../../Link/Release\
- .\../../Link/Release\
- .\../../Link/Release/lpng\
- .\../../Link/Release/lpng\
- $(ProjectName)_DEBUG
- $(ProjectName)_DEBUG
-
-
-
- Disabled
- ..\..\Lib\lpng;..\..\Lib\zlib;%(AdditionalIncludeDirectories)
- TORQUE_DEBUG;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- EnableFastChecks
- MultiThreadedDebug
- false
- true
-
-
- .\../../Link/Debug/lpng/lpng.pch
- .\../../Link/Debug/lpng/
- .\../../Link/Debug/lpng/
- .\../../Link/Debug/lpng/
- Level3
- true
- EditAndContinue
- Default
-
-
- _DEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- $(OutDir)$(TargetName)$(TargetExt)
- true
-
-
-
-
- Disabled
- ..\..\Lib\lpng;..\..\Lib\zlib;%(AdditionalIncludeDirectories)
- TORQUE_DEBUG;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- EnableFastChecks
- MultiThreadedDebug
- false
- true
-
-
- .\../../Link/Debug/lpng/lpng.pch
- .\../../Link/Debug/lpng/
- .\../../Link/Debug/lpng/
- .\../../Link/Debug/lpng/
- Level3
- true
- ProgramDatabase
- Default
-
-
- _DEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- .\../../Link/Debug\lpng.lib
- true
-
-
-
-
- MaxSpeed
- OnlyExplicitInline
- ..\..\Lib\lpng;..\..\Lib\zlib;%(AdditionalIncludeDirectories)
- _CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- .\../../Link/Release/lpng/lpng.pch
- .\../../Link/Release/lpng/
- .\../../Link/Release/lpng/
- .\../../Link/Release/lpng/
- Level3
- true
- Default
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- .\../../Link/Release\lpng.lib
- true
-
-
-
-
- MaxSpeed
- OnlyExplicitInline
- ..\..\Lib\lpng;..\..\Lib\zlib;%(AdditionalIncludeDirectories)
- _CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- .\../../Link/Release/lpng/lpng.pch
- .\../../Link/Release/lpng/
- .\../../Link/Release/lpng/
- .\../../Link/Release/lpng/
- Level3
- true
- Default
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- .\../../Link/Release\lpng.lib
- true
-
-
-
-
- MaxSpeed
- OnlyExplicitInline
- ..\..\Lib\lpng;..\..\Lib\zlib;%(AdditionalIncludeDirectories)
- _CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- .\../../Link/Release/lpng/lpng.pch
- .\../../Link/Release/lpng/
- .\../../Link/Release/lpng/
- .\../../Link/Release/lpng/
- Level3
- true
- Default
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- .\../../Link/Release\lpng.lib
- true
-
-
-
-
- MaxSpeed
- OnlyExplicitInline
- ..\..\Lib\lpng;..\..\Lib\zlib;%(AdditionalIncludeDirectories)
- _CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- .\../../Link/Release/lpng/lpng.pch
- .\../../Link/Release/lpng/
- .\../../Link/Release/lpng/
- .\../../Link/Release/lpng/
- Level3
- true
- Default
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- .\../../Link/Release\lpng.lib
- true
-
-
-
-
- {86cb2525-0cf3-40d3-bf42-a0a95035ee8c}
- false
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/engine/compilers/VisualStudio 2019/lpng.vcxproj.filters b/engine/compilers/VisualStudio 2019/lpng.vcxproj.filters
deleted file mode 100644
index dd7259716..000000000
--- a/engine/compilers/VisualStudio 2019/lpng.vcxproj.filters
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/engine/compilers/VisualStudio 2019/zlib.vcxproj b/engine/compilers/VisualStudio 2019/zlib.vcxproj
deleted file mode 100644
index 251a0c699..000000000
--- a/engine/compilers/VisualStudio 2019/zlib.vcxproj
+++ /dev/null
@@ -1,314 +0,0 @@
-
-
-
-
- Debug
- Win32
-
-
- Debug
- x64
-
-
- Release
- Win32
-
-
- Release
- x64
-
-
- Shipping
- Win32
-
-
- Shipping
- x64
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {86CB2525-0CF3-40D3-BF42-A0A95035EE8C}
- 10.0
-
-
-
- StaticLibrary
- false
- v142
-
-
- StaticLibrary
- false
- v142
-
-
- StaticLibrary
- false
- v142
-
-
- StaticLibrary
- false
- v142
-
-
- StaticLibrary
- false
- v142
-
-
- StaticLibrary
- false
- v142
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- <_ProjectFileVersion>10.0.30319.1
- .\../../Link/Debug\
- .\../../Link/Debug/zlib\
- .\../../Link/Release\
- .\../../Link/Release\
- .\../../Link/Release/zlib\
- .\../../Link/Release/zlib\
- $(ProjectName)_DEBUG
- $(ProjectName)_DEBUG
-
-
-
- Disabled
- zlib;%(AdditionalIncludeDirectories)
- TORQUE_DEBUG;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- EnableFastChecks
- MultiThreadedDebug
- false
- true
-
-
- .\../../Link/Debug/zlib/zlib.pch
- .\../../Link/Debug/zlib/
- .\../../Link/Debug/zlib/
- .\../../Link/Debug/zlib/
- Level3
- true
- EditAndContinue
- Default
-
-
- _DEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- $(OutDir)$(TargetName)$(TargetExt)
- true
-
-
-
-
- Disabled
- zlib;%(AdditionalIncludeDirectories)
- TORQUE_DEBUG;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- EnableFastChecks
- MultiThreadedDebug
- false
- true
-
-
- .\../../Link/Debug/zlib/zlib.pch
- .\../../Link/Debug/zlib/
- .\../../Link/Debug/zlib/
- .\../../Link/Debug/zlib/
- Level3
- true
- ProgramDatabase
- Default
-
-
- _DEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- .\../../Link/Debug\zlib.lib
- true
-
-
-
-
- MaxSpeed
- OnlyExplicitInline
- zlib;%(AdditionalIncludeDirectories)
- _CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- .\../../Link/Release/zlib/zlib.pch
- .\../../Link/Release/zlib/
- .\../../Link/Release/zlib/
- .\../../Link/Release/zlib/
- Level3
- true
- Default
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- .\../../Link/Release\zlib.lib
- true
-
-
-
-
- MaxSpeed
- OnlyExplicitInline
- zlib;%(AdditionalIncludeDirectories)
- _CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- .\../../Link/Release/zlib/zlib.pch
- .\../../Link/Release/zlib/
- .\../../Link/Release/zlib/
- .\../../Link/Release/zlib/
- Level3
- true
- Default
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- .\../../Link/Release\zlib.lib
- true
-
-
-
-
- MaxSpeed
- OnlyExplicitInline
- zlib;%(AdditionalIncludeDirectories)
- _CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- .\../../Link/Release/zlib/zlib.pch
- .\../../Link/Release/zlib/
- .\../../Link/Release/zlib/
- .\../../Link/Release/zlib/
- Level3
- true
- Default
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- .\../../Link/Release\zlib.lib
- true
-
-
-
-
- MaxSpeed
- OnlyExplicitInline
- zlib;%(AdditionalIncludeDirectories)
- _CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- .\../../Link/Release/zlib/zlib.pch
- .\../../Link/Release/zlib/
- .\../../Link/Release/zlib/
- .\../../Link/Release/zlib/
- Level3
- true
- Default
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- .\../../Link/Release\zlib.lib
- true
-
-
-
-
-
-
\ No newline at end of file
diff --git a/engine/compilers/VisualStudio 2019/zlib.vcxproj.filters b/engine/compilers/VisualStudio 2019/zlib.vcxproj.filters
deleted file mode 100644
index 5795e3953..000000000
--- a/engine/compilers/VisualStudio 2019/zlib.vcxproj.filters
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/engine/compilers/VisualStudio 2022/Torque 2D.aps b/engine/compilers/VisualStudio 2022/Torque 2D.aps
deleted file mode 100644
index 8b5584168..000000000
Binary files a/engine/compilers/VisualStudio 2022/Torque 2D.aps and /dev/null differ
diff --git a/engine/compilers/VisualStudio 2022/Torque 2D.ico b/engine/compilers/VisualStudio 2022/Torque 2D.ico
deleted file mode 100644
index 7e9d7feef..000000000
Binary files a/engine/compilers/VisualStudio 2022/Torque 2D.ico and /dev/null differ
diff --git a/engine/compilers/VisualStudio 2022/Torque 2D.rc b/engine/compilers/VisualStudio 2022/Torque 2D.rc
deleted file mode 100644
index 4c842aa90..000000000
--- a/engine/compilers/VisualStudio 2022/Torque 2D.rc
+++ /dev/null
@@ -1,110 +0,0 @@
-// Microsoft Visual C++ generated resource script.
-//
-#include "resource.h"
-
-#define APSTUDIO_READONLY_SYMBOLS
-/////////////////////////////////////////////////////////////////////////////
-//
-// Generated from the TEXTINCLUDE 2 resource.
-//
-#include "windows.h"
-
-/////////////////////////////////////////////////////////////////////////////
-#undef APSTUDIO_READONLY_SYMBOLS
-
-/////////////////////////////////////////////////////////////////////////////
-// English (United States) resources
-
-#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
-LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
-#pragma code_page(1252)
-
-/////////////////////////////////////////////////////////////////////////////
-//
-// Icon
-//
-
-// Icon with lowest ID value placed first to ensure application icon
-// remains consistent on all systems.
-IDI_TORQUE2D ICON "Torque 2D.ico"
-
-
-#ifdef APSTUDIO_INVOKED
-/////////////////////////////////////////////////////////////////////////////
-//
-// TEXTINCLUDE
-//
-
-1 TEXTINCLUDE
-BEGIN
- "resource.h\0"
-END
-
-2 TEXTINCLUDE
-BEGIN
- "#include ""afxres.h""\r\n"
- "\0"
-END
-
-3 TEXTINCLUDE
-BEGIN
- "\r\n"
- "\0"
-END
-
-#endif // APSTUDIO_INVOKED
-
-
-/////////////////////////////////////////////////////////////////////////////
-//
-// Version
-//
-
-VS_VERSION_INFO VERSIONINFO
- FILEVERSION 4,0,0,0
- PRODUCTVERSION 4,0,0,0
- FILEFLAGSMASK 0x17L
-#ifdef _DEBUG
- FILEFLAGS 0x1L
-#else
- FILEFLAGS 0x0L
-#endif
- FILEOS 0x4L
- FILETYPE 0x1L
- FILESUBTYPE 0x0L
-BEGIN
- BLOCK "StringFileInfo"
- BEGIN
- BLOCK "040904b0"
- BEGIN
- VALUE "CompanyName", "Torque Game Engines"
- VALUE "FileDescription", "Torque 2D: Rocket Edition"
- VALUE "FileVersion", "4, 0, 0, 0"
- VALUE "InternalName", "Torque 2D"
- VALUE "LegalCopyright", "Copyright (c) 2021 Torque Game Engines"
- VALUE "OriginalFilename", "Torque2D.exe"
- VALUE "ProductName", "Torque 2D"
- VALUE "ProductVersion", "4, 0, 0, 0"
- END
- END
- BLOCK "VarFileInfo"
- BEGIN
- VALUE "Translation", 0x409, 1200
- END
-END
-
-#endif // English (United States) resources
-/////////////////////////////////////////////////////////////////////////////
-
-
-
-#ifndef APSTUDIO_INVOKED
-/////////////////////////////////////////////////////////////////////////////
-//
-// Generated from the TEXTINCLUDE 3 resource.
-//
-
-
-/////////////////////////////////////////////////////////////////////////////
-#endif // not APSTUDIO_INVOKED
-
diff --git a/engine/compilers/VisualStudio 2022/Torque 2D.sln b/engine/compilers/VisualStudio 2022/Torque 2D.sln
deleted file mode 100644
index 3f425135f..000000000
--- a/engine/compilers/VisualStudio 2022/Torque 2D.sln
+++ /dev/null
@@ -1,110 +0,0 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.5.33530.505
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Torque2D", "Torque 2D.vcxproj", "{1564A07D-230E-4C90-AEE6-52AC9A58D6C9}"
- ProjectSection(ProjectDependencies) = postProject
- {15CBFEFF-7965-41F5-B4E2-21E8795C9159} = {15CBFEFF-7965-41F5-B4E2-21E8795C9159}
- {3A214E06-B95E-4D61-A291-1F8DF2EC10FD} = {3A214E06-B95E-4D61-A291-1F8DF2EC10FD}
- EndProjectSection
-EndProject
-Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ljpeg", "ljpeg.vcxproj", "{0B07BA94-AA53-4FD4-ADB4-79EC2DA53B36}"
-EndProject
-Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "lpng", "lpng.vcxproj", "{AF1179E3-A838-46A3-A427-1E62AA4C52F4}"
-EndProject
-Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "zlib", "zlib.vcxproj", "{86CB2525-0CF3-40D3-BF42-A0A95035EE8C}"
-EndProject
-Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libogg", "libogg.vcxproj", "{15CBFEFF-7965-41F5-B4E2-21E8795C9159}"
-EndProject
-Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libvorbis", "libvorbis.vcxproj", "{3A214E06-B95E-4D61-A291-1F8DF2EC10FD}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Win32 = Debug|Win32
- Debug|x64 = Debug|x64
- Release|Win32 = Release|Win32
- Release|x64 = Release|x64
- Shipping|Win32 = Shipping|Win32
- Shipping|x64 = Shipping|x64
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {1564A07D-230E-4C90-AEE6-52AC9A58D6C9}.Debug|Win32.ActiveCfg = Debug|Win32
- {1564A07D-230E-4C90-AEE6-52AC9A58D6C9}.Debug|Win32.Build.0 = Debug|Win32
- {1564A07D-230E-4C90-AEE6-52AC9A58D6C9}.Debug|x64.ActiveCfg = Debug|x64
- {1564A07D-230E-4C90-AEE6-52AC9A58D6C9}.Debug|x64.Build.0 = Debug|x64
- {1564A07D-230E-4C90-AEE6-52AC9A58D6C9}.Release|Win32.ActiveCfg = Release|Win32
- {1564A07D-230E-4C90-AEE6-52AC9A58D6C9}.Release|Win32.Build.0 = Release|Win32
- {1564A07D-230E-4C90-AEE6-52AC9A58D6C9}.Release|x64.ActiveCfg = Release|x64
- {1564A07D-230E-4C90-AEE6-52AC9A58D6C9}.Release|x64.Build.0 = Release|x64
- {1564A07D-230E-4C90-AEE6-52AC9A58D6C9}.Shipping|Win32.ActiveCfg = Shipping|Win32
- {1564A07D-230E-4C90-AEE6-52AC9A58D6C9}.Shipping|Win32.Build.0 = Shipping|Win32
- {1564A07D-230E-4C90-AEE6-52AC9A58D6C9}.Shipping|x64.ActiveCfg = Shipping|x64
- {1564A07D-230E-4C90-AEE6-52AC9A58D6C9}.Shipping|x64.Build.0 = Shipping|x64
- {0B07BA94-AA53-4FD4-ADB4-79EC2DA53B36}.Debug|Win32.ActiveCfg = Debug|Win32
- {0B07BA94-AA53-4FD4-ADB4-79EC2DA53B36}.Debug|Win32.Build.0 = Debug|Win32
- {0B07BA94-AA53-4FD4-ADB4-79EC2DA53B36}.Debug|x64.ActiveCfg = Debug|x64
- {0B07BA94-AA53-4FD4-ADB4-79EC2DA53B36}.Debug|x64.Build.0 = Debug|x64
- {0B07BA94-AA53-4FD4-ADB4-79EC2DA53B36}.Release|Win32.ActiveCfg = Release|Win32
- {0B07BA94-AA53-4FD4-ADB4-79EC2DA53B36}.Release|Win32.Build.0 = Release|Win32
- {0B07BA94-AA53-4FD4-ADB4-79EC2DA53B36}.Release|x64.ActiveCfg = Release|x64
- {0B07BA94-AA53-4FD4-ADB4-79EC2DA53B36}.Release|x64.Build.0 = Release|x64
- {0B07BA94-AA53-4FD4-ADB4-79EC2DA53B36}.Shipping|Win32.ActiveCfg = Shipping|Win32
- {0B07BA94-AA53-4FD4-ADB4-79EC2DA53B36}.Shipping|Win32.Build.0 = Shipping|Win32
- {0B07BA94-AA53-4FD4-ADB4-79EC2DA53B36}.Shipping|x64.ActiveCfg = Shipping|x64
- {0B07BA94-AA53-4FD4-ADB4-79EC2DA53B36}.Shipping|x64.Build.0 = Shipping|x64
- {AF1179E3-A838-46A3-A427-1E62AA4C52F4}.Debug|Win32.ActiveCfg = Debug|Win32
- {AF1179E3-A838-46A3-A427-1E62AA4C52F4}.Debug|Win32.Build.0 = Debug|Win32
- {AF1179E3-A838-46A3-A427-1E62AA4C52F4}.Debug|x64.ActiveCfg = Debug|x64
- {AF1179E3-A838-46A3-A427-1E62AA4C52F4}.Debug|x64.Build.0 = Debug|x64
- {AF1179E3-A838-46A3-A427-1E62AA4C52F4}.Release|Win32.ActiveCfg = Release|Win32
- {AF1179E3-A838-46A3-A427-1E62AA4C52F4}.Release|Win32.Build.0 = Release|Win32
- {AF1179E3-A838-46A3-A427-1E62AA4C52F4}.Release|x64.ActiveCfg = Release|x64
- {AF1179E3-A838-46A3-A427-1E62AA4C52F4}.Release|x64.Build.0 = Release|x64
- {AF1179E3-A838-46A3-A427-1E62AA4C52F4}.Shipping|Win32.ActiveCfg = Shipping|Win32
- {AF1179E3-A838-46A3-A427-1E62AA4C52F4}.Shipping|Win32.Build.0 = Shipping|Win32
- {AF1179E3-A838-46A3-A427-1E62AA4C52F4}.Shipping|x64.ActiveCfg = Shipping|x64
- {AF1179E3-A838-46A3-A427-1E62AA4C52F4}.Shipping|x64.Build.0 = Shipping|x64
- {86CB2525-0CF3-40D3-BF42-A0A95035EE8C}.Debug|Win32.ActiveCfg = Debug|Win32
- {86CB2525-0CF3-40D3-BF42-A0A95035EE8C}.Debug|Win32.Build.0 = Debug|Win32
- {86CB2525-0CF3-40D3-BF42-A0A95035EE8C}.Debug|x64.ActiveCfg = Debug|x64
- {86CB2525-0CF3-40D3-BF42-A0A95035EE8C}.Debug|x64.Build.0 = Debug|x64
- {86CB2525-0CF3-40D3-BF42-A0A95035EE8C}.Release|Win32.ActiveCfg = Release|Win32
- {86CB2525-0CF3-40D3-BF42-A0A95035EE8C}.Release|Win32.Build.0 = Release|Win32
- {86CB2525-0CF3-40D3-BF42-A0A95035EE8C}.Release|x64.ActiveCfg = Release|x64
- {86CB2525-0CF3-40D3-BF42-A0A95035EE8C}.Release|x64.Build.0 = Release|x64
- {86CB2525-0CF3-40D3-BF42-A0A95035EE8C}.Shipping|Win32.ActiveCfg = Shipping|Win32
- {86CB2525-0CF3-40D3-BF42-A0A95035EE8C}.Shipping|Win32.Build.0 = Shipping|Win32
- {86CB2525-0CF3-40D3-BF42-A0A95035EE8C}.Shipping|x64.ActiveCfg = Shipping|x64
- {86CB2525-0CF3-40D3-BF42-A0A95035EE8C}.Shipping|x64.Build.0 = Shipping|x64
- {15CBFEFF-7965-41F5-B4E2-21E8795C9159}.Debug|Win32.ActiveCfg = Debug|Win32
- {15CBFEFF-7965-41F5-B4E2-21E8795C9159}.Debug|Win32.Build.0 = Debug|Win32
- {15CBFEFF-7965-41F5-B4E2-21E8795C9159}.Debug|x64.ActiveCfg = Debug|x64
- {15CBFEFF-7965-41F5-B4E2-21E8795C9159}.Debug|x64.Build.0 = Debug|x64
- {15CBFEFF-7965-41F5-B4E2-21E8795C9159}.Release|Win32.ActiveCfg = Release|Win32
- {15CBFEFF-7965-41F5-B4E2-21E8795C9159}.Release|Win32.Build.0 = Release|Win32
- {15CBFEFF-7965-41F5-B4E2-21E8795C9159}.Release|x64.ActiveCfg = Release|x64
- {15CBFEFF-7965-41F5-B4E2-21E8795C9159}.Release|x64.Build.0 = Release|x64
- {15CBFEFF-7965-41F5-B4E2-21E8795C9159}.Shipping|Win32.ActiveCfg = Release|Win32
- {15CBFEFF-7965-41F5-B4E2-21E8795C9159}.Shipping|Win32.Build.0 = Release|Win32
- {15CBFEFF-7965-41F5-B4E2-21E8795C9159}.Shipping|x64.ActiveCfg = Release|x64
- {15CBFEFF-7965-41F5-B4E2-21E8795C9159}.Shipping|x64.Build.0 = Release|x64
- {3A214E06-B95E-4D61-A291-1F8DF2EC10FD}.Debug|Win32.ActiveCfg = Debug|Win32
- {3A214E06-B95E-4D61-A291-1F8DF2EC10FD}.Debug|Win32.Build.0 = Debug|Win32
- {3A214E06-B95E-4D61-A291-1F8DF2EC10FD}.Debug|x64.ActiveCfg = Debug|x64
- {3A214E06-B95E-4D61-A291-1F8DF2EC10FD}.Debug|x64.Build.0 = Debug|x64
- {3A214E06-B95E-4D61-A291-1F8DF2EC10FD}.Release|Win32.ActiveCfg = Release|Win32
- {3A214E06-B95E-4D61-A291-1F8DF2EC10FD}.Release|Win32.Build.0 = Release|Win32
- {3A214E06-B95E-4D61-A291-1F8DF2EC10FD}.Release|x64.ActiveCfg = Release|x64
- {3A214E06-B95E-4D61-A291-1F8DF2EC10FD}.Release|x64.Build.0 = Release|x64
- {3A214E06-B95E-4D61-A291-1F8DF2EC10FD}.Shipping|Win32.ActiveCfg = Release|Win32
- {3A214E06-B95E-4D61-A291-1F8DF2EC10FD}.Shipping|Win32.Build.0 = Release|Win32
- {3A214E06-B95E-4D61-A291-1F8DF2EC10FD}.Shipping|x64.ActiveCfg = Release|x64
- {3A214E06-B95E-4D61-A291-1F8DF2EC10FD}.Shipping|x64.Build.0 = Release|x64
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {A22C1FEC-DB0E-418F-8851-2DCB8230E6D6}
- EndGlobalSection
-EndGlobal
diff --git a/engine/compilers/VisualStudio 2022/Torque 2D.vcxproj b/engine/compilers/VisualStudio 2022/Torque 2D.vcxproj
deleted file mode 100644
index 17184ffbe..000000000
--- a/engine/compilers/VisualStudio 2022/Torque 2D.vcxproj
+++ /dev/null
@@ -1,1488 +0,0 @@
-
-
-
-
- Debug
- Win32
-
-
- Debug
- x64
-
-
- Release
- Win32
-
-
- Release
- x64
-
-
- Shipping
- Win32
-
-
- Shipping
- x64
-
-
-
- {1564A07D-230E-4C90-AEE6-52AC9A58D6C9}
- TorqueGame
- Torque2D
- 10.0
-
-
-
- Application
- false
- v143
-
-
- Application
- false
- v142
-
-
- Application
- false
- v143
-
-
- Application
- false
- v143
-
-
- Application
- false
- v143
-
-
- Application
- false
- v143
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- <_ProjectFileVersion>10.0.30319.1
- ../../../
- ../../Link/VC2012.$(Configuration).$(PlatformName)/$(ProjectName)/
-
-
- false
- ../../../
- ../../../
- ../../Link/VC2012.$(Configuration).$(PlatformName)/$(ProjectName)/
- ../../Link/VC2012.$(Configuration).$(PlatformName)/$(ProjectName)/
- false
- false
- false
- false
- Torque2D_DEBUG
- Torque2D_DEBUG
- Torque2D
- Torque2D
- Torque2D
- Torque2D
-
-
- ../../../
- ../../Link/VC2012.$(Configuration).$(PlatformName)/$(ProjectName)/
-
-
- ../../../
- ../../Link/VC2012.$(Configuration).$(PlatformName)/$(ProjectName)/
-
-
-
- $(OutDir)Torque2D.tlb
-
-
-
-
- Disabled
- ../../Lib/libogg/include;../../Lib/libvorbis/include;../../Lib/LeapSDK/include;../../Lib/zlib;../../Lib/lpng;../../Lib/ljpeg;../../Lib/openal/win32;../../source;../../source/persistence/rapidjson/include;../../source/persistence/libjson;../../source/testing/googleTest;../../source/testing/googleTest/include;../../source/spine;%(AdditionalIncludeDirectories)
- TORQUE_DEBUG;TORQUE_DEBUG_GUARD;_CRT_SECURE_NO_DEPRECATE;UNICODE;_HAS_STD_BYTE=0;%(PreprocessorDefinitions)
- EnableFastChecks
- MultiThreaded
- false
- true
-
-
- $(OutDir)
- $(IntDir)$(ProjectName).pdb
- false
- true
- EditAndContinue
- CompileAsCpp
- true
- Level3
- 4800;4100;4127;4512
- false
- true
- stdcpp17
-
-
- _DEBUG;%(PreprocessorDefinitions)
- 0x0409
- ../../Lib/MSPlatformSDK/Include;%(AdditionalIncludeDirectories)
-
-
- COMCTL32.LIB;COMDLG32.LIB;USER32.LIB;ADVAPI32.LIB;GDI32.LIB;RPCRT4.LIB;WINMM.LIB;WS2_32.LIB;vfw32.lib;Imm32.lib;shell32.lib;shlwapi.lib;ole32.lib;%(AdditionalDependencies)
- ../../../Torque2D_DEBUG.exe
- true
- ../../Lib/unicode;../../lib/MSPlatformSDK/Lib;%(AdditionalLibraryDirectories)
- LIBC;LIBCD;LIBCMTD;%(IgnoreSpecificDefaultLibraries)
- true
- $(IntDir)$(ProjectName).pdb
- Windows
- false
-
-
- MachineX86
- false
-
-
-
-
- $(OutDir)Torque2D.tlb
-
-
-
-
- Disabled
- ../../Lib/libogg/include;../../Lib/libvorbis/include;../../Lib/LeapSDK/include;../../Lib/zlib;../../Lib/lpng;../../Lib/ljpeg;../../Lib/openal/win32;../../source;../../source/persistence/rapidjson/include;../../source/persistence/libjson;../../source/testing/googleTest;../../source/testing/googleTest/include;../../source/spine;%(AdditionalIncludeDirectories)
- TORQUE_DEBUG;TORQUE_DEBUG_GUARD;_CRT_SECURE_NO_DEPRECATE;UNICODE;_HAS_STD_BYTE=0;%(PreprocessorDefinitions)
- EnableFastChecks
- MultiThreaded
- false
- true
-
-
- $(OutDir)
- $(IntDir)$(ProjectName).pdb
- false
- true
- ProgramDatabase
- CompileAsCpp
- true
- Level3
- 4800;4100;4127;4512
- false
- true
- true
- stdcpp17
-
-
- _DEBUG;%(PreprocessorDefinitions)
- 0x0409
- ../../Lib/MSPlatformSDK/Include;%(AdditionalIncludeDirectories)
-
-
- COMCTL32.LIB;COMDLG32.LIB;USER32.LIB;ADVAPI32.LIB;GDI32.LIB;RPCRT4.LIB;WINMM.LIB;WS2_32.LIB;vfw32.lib;Imm32.lib;shell32.lib;shlwapi.lib;ole32.lib;%(AdditionalDependencies)
- ../../../Torque2D_DEBUG.exe
- false
- ../../Lib/unicode;../../lib/MSPlatformSDK/Lib;%(AdditionalLibraryDirectories)
- LIBC;%(IgnoreSpecificDefaultLibraries)
- true
- $(IntDir)$(ProjectName).pdb
- Windows
- false
-
-
- false
- /NODEFAULTLIB:libcmtd.lib /NODEFAULTLIB:msvcrtd.lib %(AdditionalOptions)
-
-
-
-
- $(OutDir)Torque2D.tlb
-
-
-
-
- MinSpace
- OnlyExplicitInline
- ../../Lib/libogg/include;../../Lib/libvorbis/include;../../Lib/LeapSDK/include;../../Lib/zlib;../../Lib/lpng;../../Lib/ljpeg;../../Lib/openal/win32;../../source;../../source/persistence/rapidjson/include;../../source/persistence/libjson;../../source/testing/googleTest;../../source/testing/googleTest/include;../../source/spine;%(AdditionalIncludeDirectories)
- UNICODE;_CRT_SECURE_NO_DEPRECATE;_HAS_STD_BYTE=0;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- $(OutDir)
- $(IntDir)$(ProjectName).pdb
- false
- Level3
- true
- ProgramDatabase
- CompileAsCpp
- true
- 4800;4100;4127;4512
- true
- stdcpp17
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
- ../../Lib/MSPlatformSDK/Include;%(AdditionalIncludeDirectories)
-
-
- COMCTL32.LIB;COMDLG32.LIB;USER32.LIB;ADVAPI32.LIB;GDI32.LIB;WINMM.LIB;WS2_32.LIB;vfw32.lib;Imm32.lib;shell32.lib;shlwapi.lib;ole32.lib;RPCRT4.LIB;%(AdditionalDependencies)
- ../../../Torque2D.exe
- false
- ../../Lib/unicode;../../Lib/MSPlatformSDK/Lib;%(AdditionalLibraryDirectories)
- LIBC;%(IgnoreSpecificDefaultLibraries)
- true
- $(IntDir)$(ProjectName).pdb
- Windows
- false
-
-
- MachineX86
- false
- HighestAvailable
- false
-
-
-
-
-
-
-
-
-
-
-
-
-
- $(OutDir)Torque2D.tlb
-
-
-
-
- MinSpace
- OnlyExplicitInline
- ../../Lib/libogg/include;../../Lib/libvorbis/include;../../Lib/LeapSDK/include;../../Lib/zlib;../../Lib/lpng;../../Lib/ljpeg;../../Lib/openal/win32;../../source;../../source/persistence/rapidjson/include;../../source/persistence/libjson;../../source/testing/googleTest;../../source/testing/googleTest/include;../../source/spine;%(AdditionalIncludeDirectories)
- UNICODE;_CRT_SECURE_NO_DEPRECATE;_HAS_STD_BYTE=0;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- $(OutDir)
- $(IntDir)$(ProjectName).pdb
- false
- Level3
- true
- ProgramDatabase
- CompileAsCpp
- true
- 4800;4100;4127;4512
- true
- stdcpp17
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
- ../../Lib/MSPlatformSDK/Include;%(AdditionalIncludeDirectories)
-
-
- COMCTL32.LIB;COMDLG32.LIB;USER32.LIB;ADVAPI32.LIB;GDI32.LIB;WINMM.LIB;WS2_32.LIB;vfw32.lib;Imm32.lib;shell32.lib;shlwapi.lib;ole32.lib;RPCRT4.LIB;%(AdditionalDependencies)
- ../../../Torque2D.exe
- false
- ../../Lib/unicode;../../Lib/MSPlatformSDK/Lib;%(AdditionalLibraryDirectories)
- LIBC;%(IgnoreSpecificDefaultLibraries)
- true
- $(IntDir)$(ProjectName).pdb
- Windows
- false
-
-
- false
- HighestAvailable
- false
-
-
-
-
-
-
-
-
-
-
-
-
-
- $(OutDir)Torque2D.tlb
-
-
-
-
- MinSpace
- OnlyExplicitInline
- ../../Lib/LeapSDK/include;../../Lib/zlib;../../Lib/lpng;../../Lib/ljpeg;../../Lib/openal/win32;../../Source;../../Source/persistence/rapidjson/include;../../Source/persistence/libjson;%(AdditionalIncludeDirectories)
- TORQUE_SHIPPING;UNICODE;_CRT_SECURE_NO_DEPRECATE;_HAS_STD_BYTE=0;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- $(OutDir)
- $(IntDir)$(ProjectName).pdb
- false
- Level3
- true
- ProgramDatabase
- CompileAsCpp
- true
- 4800;4100;4127;4512
- true
- stdcpp17
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
- ../../Lib/MSPlatformSDK/Include;%(AdditionalIncludeDirectories)
-
-
- COMCTL32.LIB;COMDLG32.LIB;USER32.LIB;ADVAPI32.LIB;GDI32.LIB;WINMM.LIB;WS2_32.LIB;vfw32.lib;Imm32.lib;shell32.lib;shlwapi.lib;ole32.lib;RPCRT4.LIB;%(AdditionalDependencies)
- ../../../Torque2D.exe
- false
- ../../Lib/unicode;../../Lib/MSPlatformSDK/Lib;%(AdditionalLibraryDirectories)
- LIBC;%(IgnoreSpecificDefaultLibraries)
- false
- $(IntDir)$(ProjectName).pdb
- Windows
- false
-
-
- MachineX86
- false
- HighestAvailable
- false
-
-
-
-
-
-
-
-
-
-
-
-
-
- $(OutDir)Torque2D.tlb
-
-
-
-
- MinSpace
- OnlyExplicitInline
- ../../Lib/LeapSDK/include;../../Lib/zlib;../../Lib/lpng;../../Lib/ljpeg;../../Lib/openal/win32;../../Source;../../Source/persistence/rapidjson/include;../../Source/persistence/libjson;%(AdditionalIncludeDirectories)
- TORQUE_SHIPPING;UNICODE;_CRT_SECURE_NO_DEPRECATE;_HAS_STD_BYTE=0;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- $(OutDir)
- $(IntDir)$(ProjectName).pdb
- false
- Level3
- true
- ProgramDatabase
- CompileAsCpp
- true
- 4800;4100;4127;4512
- true
- stdcpp17
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
- ../../Lib/MSPlatformSDK/Include;%(AdditionalIncludeDirectories)
-
-
- COMCTL32.LIB;COMDLG32.LIB;USER32.LIB;ADVAPI32.LIB;GDI32.LIB;WINMM.LIB;WS2_32.LIB;vfw32.lib;Imm32.lib;shell32.lib;shlwapi.lib;ole32.lib;RPCRT4.LIB;%(AdditionalDependencies)
- ../../../Torque2D.exe
- false
- ../../Lib/unicode;../../Lib/MSPlatformSDK/Lib;%(AdditionalLibraryDirectories)
- LIBC;%(IgnoreSpecificDefaultLibraries)
- false
- $(IntDir)$(ProjectName).pdb
- Windows
- false
-
-
- false
- HighestAvailable
- false
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {15cbfeff-7965-41f5-b4e2-21e8795c9159}
- false
- false
- false
- true
- false
-
-
- {3a214e06-b95e-4d61-a291-1f8df2ec10fd}
- false
- false
- false
- true
- false
-
-
- {0b07ba94-aa53-4fd4-adb4-79ec2da53b36}
- false
-
-
- {af1179e3-a838-46a3-a427-1e62aa4c52f4}
- false
-
-
- {86cb2525-0cf3-40d3-bf42-a0a95035ee8c}
- false
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/engine/compilers/VisualStudio 2022/Torque 2D.vcxproj.filters b/engine/compilers/VisualStudio 2022/Torque 2D.vcxproj.filters
deleted file mode 100644
index 820baff08..000000000
--- a/engine/compilers/VisualStudio 2022/Torque 2D.vcxproj.filters
+++ /dev/null
@@ -1,3109 +0,0 @@
-
-
-
-
- {b90d3c2c-9694-4051-8b22-325c00d37951}
-
-
- {57099cfd-2f9b-468f-88a5-df769a6235b0}
-
-
- {8b011278-0d15-4ba9-ba57-24056b918233}
-
-
- {b0a55e16-3a1f-4eb5-8f5b-3396d456be74}
-
-
- {71933100-a4a2-464e-bb3f-bb04d9f078af}
-
-
- {7824869e-4359-4413-a615-38c8534d641b}
-
-
- {baaa5934-0805-46eb-be6e-ae5643535b4b}
-
-
- {6a14bfc6-1f7c-4527-91d3-ccf897ff4002}
-
-
- {bfd4849e-66c6-450b-9a01-0873aa28e21a}
-
-
- {1f640da6-77a4-4176-a018-9bfad5becf95}
-
-
- {4faf4110-842d-4178-81ca-0375f3804cac}
-
-
- {2f939a2b-3cd9-4e67-8311-bf751093f2a5}
-
-
- {84907fc2-76e8-4a3b-9df4-76451387d3f9}
-
-
- {e2574a74-ff0d-401d-818d-96e6c155a4b4}
-
-
- {18c9fe85-da18-45d5-9573-a71ce9373961}
-
-
- {4f9157b1-a024-450f-a352-1849cf2bddcc}
-
-
- {14385fbb-f8af-4849-bdd9-8c8f67d3928e}
-
-
- {9ec04d21-b122-45be-bd70-2869fa9cdd8e}
-
-
- {09cd4b77-b9b4-41f4-b17e-3ac25c27beee}
-
-
- {47a0d755-122c-490b-b11c-d06a74fe2ab2}
-
-
- {ec2d3f1a-e3d1-49d5-98d6-a3084e37d077}
-
-
- {b3b96a6a-462f-4c7d-92a5-009cabbe94a3}
-
-
- {d55ba677-1863-4206-92b7-1df94c48df84}
-
-
- {d016b107-aefd-41ac-a04c-6b75caaf114d}
-
-
- {d48f2bd5-39e5-4fdb-8472-79839fcd7e21}
-
-
- {187e7671-960f-49a7-898b-a10cf5900751}
-
-
- {c17330c5-953e-4e71-9acb-1b7806b3c932}
-
-
- {92e20c7c-410f-4df4-a9b9-7662dac44699}
-
-
- {513dc4d3-ba16-414a-8576-16b1dba4f6e8}
-
-
- {c48781cd-5e6a-4ffe-b866-e64582f00d6d}
-
-
- {eee14c00-6a21-4fa8-9339-572a11819062}
-
-
- {fcfa64cc-dd46-41c1-9ada-8b4a15051285}
-
-
- {fa588b4f-a8dd-4615-beee-dab4db372118}
-
-
- {27eea104-6e41-4cfb-bc50-56f9157081ce}
-
-
- {0688e2d7-d313-4a35-b90d-1df191825361}
-
-
- {04e382a2-a0a9-40c9-b227-a46e345fb032}
-
-
- {5c7d23f1-7194-4a6f-8bd3-fd5b6a0beb4f}
-
-
- {66757e8b-c204-4b92-92ab-19278019d17d}
-
-
- {23024162-0d6f-4259-8dac-a3a621d55065}
-
-
- {c1b891be-db08-48e9-b52a-f0cf3bac185e}
-
-
- {87703b23-784c-4769-b085-57c72b1b536e}
-
-
- {a2568a68-f396-4b3e-9ed6-bf73656752d3}
-
-
- {d13f86bc-f4dd-4329-8337-dd41e2db04a1}
-
-
- {f5858e14-ed7e-4887-a22e-4123ab41a6f1}
-
-
- {a9dbe516-9ed6-43a6-987e-6f2bc6f59928}
-
-
- {d98ec67b-b10a-4c7b-bb2f-a6394a861b67}
-
-
- {04d21b23-41a0-44a7-810e-f31c81fd5c81}
-
-
- {3f7e4a0c-c3da-4972-a9bc-9cb0e0ce622e}
-
-
- {d77ee12f-a922-4d39-9e6c-2ad87a5b4a4e}
-
-
- {9da3da8f-0660-4a53-b5cf-3994c2dca7e3}
-
-
- {78695f15-84c6-4505-a999-716fe79bdabe}
-
-
- {e706ea06-aa20-4487-a010-e0d00adc658f}
-
-
- {fa7e2f20-cd6d-4118-8a74-9f9c95119064}
-
-
- {7b04617f-42ef-4238-9a98-9d8309b64c93}
-
-
- {57e1271d-4358-4180-b168-4b9c2cbac907}
-
-
- {a9e97335-bed5-4f6a-9959-12f5f41dbdcb}
-
-
- {e11e344e-6418-4ed0-980a-77d66cd64d65}
-
-
- {1eb9e730-583b-4aa4-ac25-b83960799ba4}
-
-
- {30e1ec13-118b-4d50-8e04-76e76fcfdc01}
-
-
- {4d0b6ff3-58d2-4952-bd14-915a50a3b568}
-
-
- {9cbec746-dd4c-4b4c-b11f-37a126ea2c38}
-
-
- {427672e0-f4a2-45a9-b44c-92d190e961aa}
-
-
- {e1ff3412-7343-4dfb-bc99-bce90655557b}
-
-
- {cc1c1416-376b-4686-a4ac-21d1a35c9390}
-
-
- {447ecd65-a7a2-4e18-9c55-b53356c6f7a9}
-
-
- {b2903a96-6c49-4961-82a8-f1832989d4a4}
-
-
- {598766e4-7dc1-45b8-8acf-f133f4fced82}
-
-
- {1a2a7ebc-eda6-4a67-b9ab-bc9f437b5d5c}
-
-
-
-
- audio
-
-
- audio
-
-
- audio
-
-
- audio
-
-
- audio
-
-
- component
-
-
- component
-
-
- component\behaviors
-
-
- component\behaviors
-
-
- component\behaviors
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- game
-
-
- game
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- persistence
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform\menus
-
-
- platform\nativeDialogs
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32\menus
-
-
- platformWin32\nativeDialogs
-
-
- platformWin32\nativeDialogs
-
-
- platformWin32\threads
-
-
- platformWin32\threads
-
-
- gui\buttons
-
-
- gui\buttons
-
-
- gui\buttons
-
-
- gui\containers
-
-
- gui\containers
-
-
- gui\containers
-
-
- gui\containers
-
-
- gui\editor
-
-
- gui\editor
-
-
- gui\editor
-
-
- gui\editor
-
-
- gui\editor
-
-
- Box2D\Collision
-
-
- Box2D\Collision
-
-
- Box2D\Collision
-
-
- Box2D\Collision
-
-
- Box2D\Collision
-
-
- Box2D\Collision
-
-
- Box2D\Collision
-
-
- Box2D\Collision
-
-
- Box2D\Collision\Shapes
-
-
- Box2D\Collision\Shapes
-
-
- Box2D\Collision\Shapes
-
-
- Box2D\Collision\Shapes
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Dynamics
-
-
- Box2D\Dynamics
-
-
- Box2D\Dynamics
-
-
- Box2D\Dynamics
-
-
- Box2D\Dynamics
-
-
- Box2D\Dynamics
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Rope
-
-
- persistence\taml
-
-
- module
-
-
- module
-
-
- persistence\tinyXML
-
-
- persistence\tinyXML
-
-
- persistence\tinyXML
-
-
- persistence\tinyXML
-
-
- module
-
-
- assets
-
-
- assets
-
-
- assets
-
-
- assets
-
-
- assets
-
-
- gui\language
-
-
- graphics
-
-
- graphics
-
-
- graphics
-
-
- graphics
-
-
- graphics
-
-
- graphics
-
-
- graphics
-
-
- graphics
-
-
- messaging
-
-
- messaging
-
-
- messaging
-
-
- messaging
-
-
- messaging
-
-
- game
-
-
- debug
-
-
- math
-
-
- input
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- graphics
-
-
- collection
-
-
- collection
-
-
- collection
-
-
- io
-
-
- io
-
-
- io
-
-
- io
-
-
- io
-
-
- io
-
-
- io
-
-
- io
-
-
- io
-
-
- io
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- memory
-
-
- algorithm
-
-
- algorithm
-
-
- game
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- network
-
-
- network
-
-
- network
-
-
- network
-
-
- network
-
-
- network
-
-
- network
-
-
- network
-
-
- network
-
-
- network
-
-
- network
-
-
- network
-
-
- string
-
-
- string
-
-
- string
-
-
- string
-
-
- string
-
-
- io\resource
-
-
- io\resource
-
-
- collection
-
-
- collection
-
-
- platform
-
-
- network
-
-
- debug
-
-
- string
-
-
- network
-
-
- network
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- audio
-
-
- graphics
-
-
- graphics
-
-
- graphics
-
-
- graphics
-
-
- persistence\taml
-
-
- delegates
-
-
- 2d\assets
-
-
- 2d\assets
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- 2d\gui
-
-
- 2d\gui
-
-
- 2d\core
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- 2d\gui
-
-
- debug\remote
-
-
- debug\remote
-
-
- debug\remote
-
-
- graphics
-
-
- platform
-
-
- network
-
-
- testing\tests
-
-
- testing
-
-
- testing\tests
-
-
- testing\tests
-
-
- platform\nativeDialogs
-
-
- platformWin32\nativeDialogs
-
-
- 2d\assets
-
-
- 2d\assets
-
-
- 2d\assets
-
-
- 2d\assets
-
-
- 2d\sceneobject
-
-
- 2d\core
-
-
- assets
-
-
- assets
-
-
- persistence\taml
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\controllers
-
-
- 2d\controllers
-
-
- 2d\controllers\core
-
-
- 2d\controllers\core
-
-
- 2d\controllers
-
-
- 2d\core
-
-
- 2d\experimental\composites
-
-
- 2d\core
-
-
- 2d\core
-
-
- persistence\taml\binary
-
-
- persistence\taml\binary
-
-
- persistence\taml\json
-
-
- persistence\taml\json
-
-
- persistence\taml\xml
-
-
- persistence\taml\xml
-
-
- persistence\taml\xml
-
-
- persistence\taml\json
-
-
- gui\containers
-
-
- console
-
-
- audio
-
-
- io
-
-
- math
-
-
- memory
-
-
- console
-
-
- math
-
-
- audio
-
-
- 2d\assets
-
-
- 2d\sceneobject
-
-
- bitmapFont
-
-
- bitmapFont
-
-
- audio
-
-
-
-
-
-
- gui\containers
-
-
- Box2D\Particle
-
-
- Box2D\Particle
-
-
- Box2D\Particle
-
-
- Box2D\Particle
-
-
- Box2D\Particle
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- graphics
-
-
- console
-
-
- math
-
-
- gui\containers
-
-
- gui\containers
-
-
- gui\containers
-
-
- 2d\editorToy
-
-
- 2d\editorToy
-
-
- gui\buttons
-
-
- gui\editor
-
-
- gui\containers
-
-
- gui\editor
-
-
- algorithm
-
-
- math\noise
-
-
- algorithm
-
-
- math\noise
-
-
- gui
-
-
- gui\containers
-
-
- gui
-
-
-
-
- audio
-
-
- audio
-
-
- audio
-
-
- audio
-
-
- audio
-
-
- audio
-
-
- component
-
-
- component
-
-
- component\behaviors
-
-
- component\behaviors
-
-
- component\behaviors
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- game
-
-
- game
-
-
- game
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- persistence
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform\menus
-
-
- platform\nativeDialogs
-
-
- platform\nativeDialogs
-
-
- platform\threads
-
-
- platform\threads
-
-
- platform\threads
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32\nativeDialogs
-
-
- gui\buttons
-
-
- gui\buttons
-
-
- gui\buttons
-
-
- gui\containers
-
-
- gui\containers
-
-
- gui\containers
-
-
- gui\containers
-
-
- gui\editor
-
-
- gui\editor
-
-
- gui\editor
-
-
- gui\editor
-
-
- gui\editor
-
-
- Box2D
-
-
- Box2D\Collision
-
-
- Box2D\Collision
-
-
- Box2D\Collision
-
-
- Box2D\Collision
-
-
- Box2D\Collision
-
-
- Box2D\Collision\Shapes
-
-
- Box2D\Collision\Shapes
-
-
- Box2D\Collision\Shapes
-
-
- Box2D\Collision\Shapes
-
-
- Box2D\Collision\Shapes
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Dynamics
-
-
- Box2D\Dynamics
-
-
- Box2D\Dynamics
-
-
- Box2D\Dynamics
-
-
- Box2D\Dynamics
-
-
- Box2D\Dynamics
-
-
- Box2D\Dynamics
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Contacts
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Dynamics\Joints
-
-
- Box2D\Rope
-
-
- component\behaviors
-
-
- component\behaviors
-
-
- component\behaviors
-
-
- component\behaviors
-
-
- persistence\taml
-
-
- persistence\taml
-
-
- persistence\taml
-
-
- module
-
-
- module
-
-
- module
-
-
- persistence\tinyXML
-
-
- persistence\tinyXML
-
-
- module
-
-
- module
-
-
- assets
-
-
- assets
-
-
- assets
-
-
- assets
-
-
- assets
-
-
- assets
-
-
- assets
-
-
- assets
-
-
- assets
-
-
- assets
-
-
- assets
-
-
- assets
-
-
- assets
-
-
- module
-
-
- module
-
-
- assets
-
-
- assets
-
-
- gui\language
-
-
- graphics
-
-
- graphics
-
-
- graphics
-
-
- graphics
-
-
- messaging
-
-
- messaging
-
-
- messaging
-
-
- messaging
-
-
- messaging
-
-
- game
-
-
- debug
-
-
- math
-
-
- input
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- graphics
-
-
- collection
-
-
- collection
-
-
- collection
-
-
- collection
-
-
- collection
-
-
- collection
-
-
- collection
-
-
- collection
-
-
- collection
-
-
- collection
-
-
- collection
-
-
- io
-
-
- io
-
-
- io
-
-
- io
-
-
- io
-
-
- io
-
-
- io
-
-
- io
-
-
- io
-
-
- io
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- io\zip
-
-
- memory
-
-
- memory
-
-
- collection
-
-
- collection
-
-
- algorithm
-
-
- algorithm
-
-
- algorithm
-
-
- collection
-
-
- game
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- network
-
-
- network
-
-
- network
-
-
- network
-
-
- network
-
-
- network
-
-
- network
-
-
- network
-
-
- string
-
-
- string
-
-
- string
-
-
- string
-
-
- string
-
-
- memory
-
-
- io\resource
-
-
- memory
-
-
- collection
-
-
- collection
-
-
- collection
-
-
- platform
-
-
- network
-
-
- debug
-
-
- string
-
-
- network
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- sim
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- console
-
-
- audio
-
-
- graphics
-
-
- graphics
-
-
- graphics
-
-
- graphics
-
-
- graphics
-
-
- persistence\taml
-
-
- delegates
-
-
- delegates
-
-
- delegates
-
-
- 2d\assets
-
-
- 2d\assets
-
-
- 2d\assets
-
-
- 2d\assets
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- 2d\gui
-
-
- 2d\gui
-
-
- 2d\gui
-
-
- 2d\core
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- 2d\scene
-
-
- algorithm
-
-
- 2d\gui
-
-
- 2d\gui
-
-
- debug\remote
-
-
- debug\remote
-
-
- debug\remote
-
-
- debug\remote
-
-
- debug\remote
-
-
- debug\remote
-
-
- graphics
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- platform
-
-
- game
-
-
- network
-
-
- testing
-
-
- platform
-
-
- platform
-
-
- platformWin32
-
-
- persistence\taml
-
-
- 2d\assets
-
-
- 2d\assets
-
-
- 2d\assets
-
-
- 2d\assets
-
-
- 2d\assets
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\assets
-
-
- 2d\core
-
-
- assets
-
-
- assets
-
-
- persistence\taml
-
-
- sim
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\controllers
-
-
- 2d\controllers
-
-
- 2d\controllers
-
-
- 2d\controllers
-
-
- 2d\controllers\core
-
-
- 2d\controllers\core
-
-
- 2d\controllers\core
-
-
- 2d\controllers\core
-
-
- 2d\controllers\core
-
-
- 2d\controllers
-
-
- 2d\controllers
-
-
- 2d\core
-
-
- 2d\core
-
-
- 2d\experimental\composites
-
-
- 2d\experimental\composites
-
-
- 2d\core
-
-
- 2d\core
-
-
- persistence\rapidjson
-
-
- persistence\rapidjson
-
-
- persistence\rapidjson
-
-
- persistence\rapidjson
-
-
- persistence\rapidjson
-
-
- persistence\rapidjson
-
-
- persistence\rapidjson
-
-
- persistence\rapidjson
-
-
- persistence\rapidjson
-
-
- persistence\rapidjson
-
-
- persistence\rapidjson
-
-
- persistence\rapidjson
-
-
- persistence\rapidjson\internal
-
-
- persistence\rapidjson\internal
-
-
- persistence\rapidjson\internal
-
-
- persistence\taml\binary
-
-
- persistence\taml\binary
-
-
- persistence\taml\json
-
-
- persistence\taml\json
-
-
- persistence\taml\xml
-
-
- persistence\taml\xml
-
-
- persistence\taml\xml
-
-
- persistence\taml
-
-
- persistence\taml
-
-
- persistence\taml\json
-
-
- gui\containers
-
-
- sim
-
-
- input
-
-
- gui
-
-
- platform
-
-
- string
-
-
- console
-
-
- platform
-
-
- game
-
-
- console
-
-
- sim
-
-
- sim
-
-
- persistence
-
-
- messaging
-
-
- io
-
-
- platform\nativeDialogs
-
-
- console
-
-
- network
-
-
- game
-
-
- platform\menus
-
-
- io
-
-
- collection
-
-
- console
-
-
- messaging
-
-
- math
-
-
- math
-
-
- math
-
-
- math
-
-
- component
-
-
- component
-
-
- io
-
-
- network
-
-
- messaging
-
-
- network
-
-
- network
-
-
- sim
-
-
- graphics
-
-
- console
-
-
- graphics
-
-
- network
-
-
- platform
-
-
- platform
-
-
- debug
-
-
- debug
-
-
- network
-
-
- network
-
-
- io\resource
-
-
- graphics
-
-
-
- platformWin32
-
-
- network
-
-
- console
-
-
- console
-
-
- console
-
-
- platform
-
-
- graphics
-
-
- graphics
-
-
- platformWin32
-
-
- platformWin32
-
-
- 2d\core
-
-
- platformWin32
-
-
- platformWin32
-
-
- platformWin32
-
-
- network
-
-
- network
-
-
- network
-
-
- testing
-
-
- platform\nativeDialogs
-
-
- sim
-
-
- platformWin32
-
-
- string
-
-
- io\zip
-
-
- gui
-
-
- console
-
-
- audio
-
-
- platformWin32
-
-
- 2d\assets
-
-
- 2d\assets
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- bitmapFont
-
-
- bitmapFont
-
-
- bitmapFont
-
-
- bitmapFont
-
-
- audio
-
-
-
-
-
-
-
-
-
-
- gui\buttons
-
-
- gui\containers
-
-
-
- gui\containers
-
-
- gui\containers
-
-
- gui\containers
-
-
- Box2D\Particle
-
-
- Box2D\Particle
-
-
- Box2D\Particle
-
-
- Box2D\Particle
-
-
- Box2D\Particle
-
-
- Box2D\Particle
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- Box2D\Common
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- 2d\sceneobject
-
-
- graphics
-
-
- graphics
-
-
- console
-
-
- console
-
-
- math
-
-
- gui\containers
-
-
- gui\containers
-
-
- gui\containers
-
-
- gui\containers
-
-
- gui\buttons
-
-
- gui\containers
-
-
- gui\containers
-
-
- gui\containers
-
-
- 2d\editorToy
-
-
- 2d\editorToy
-
-
- 2d\editorToy
-
-
- gui
-
-
- gui\containers
-
-
- gui
-
-
- gui\buttons
-
-
- gui\buttons
-
-
- gui\editor
-
-
- gui\editor
-
-
- gui
-
-
- gui\containers
-
-
- gui\editor
-
-
- gui\editor
-
-
- algorithm
-
-
- math\noise
-
-
- math\noise
-
-
- algorithm
-
-
- math\noise
-
-
- math\noise
-
-
- gui
-
-
- gui
-
-
- gui\containers
-
-
- gui\containers
-
-
- gui
-
-
- gui
-
-
- gui
-
-
- graphics
-
-
-
-
-
-
-
-
-
-
- Box2D\Particle
-
-
-
\ No newline at end of file
diff --git a/engine/compilers/VisualStudio 2022/libogg.vcxproj b/engine/compilers/VisualStudio 2022/libogg.vcxproj
deleted file mode 100644
index 4734f45e4..000000000
--- a/engine/compilers/VisualStudio 2022/libogg.vcxproj
+++ /dev/null
@@ -1,185 +0,0 @@
-
-
-
-
- Debug
- Win32
-
-
- Debug
- x64
-
-
- Release
- Win32
-
-
- Release
- x64
-
-
-
-
-
-
-
-
-
-
-
- {15CBFEFF-7965-41F5-B4E2-21E8795C9159}
- libogg
- Win32Proj
- 10.0
-
-
-
- StaticLibrary
- Unicode
- false
- v143
-
-
- StaticLibrary
- Unicode
- v143
-
-
- StaticLibrary
- Unicode
- true
- v142
-
-
- StaticLibrary
- Unicode
- v143
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- <_ProjectFileVersion>10.0.30319.1
- .\../../Link/Debug\
- .\../../Link/Debug/libogg\
- .\../../Link/Debug\
- .\../../Link/Debug/libogg\
- .\../../Link/Release\
- .\../../Link/Release/libogg\
- $(SolutionDir)$(Platform)\$(Configuration)\
- $(Platform)\$(Configuration)\
-
-
- $(ProjectName)_DEBUG
-
-
-
- Disabled
- ..\..\Lib\libogg\include;%(AdditionalIncludeDirectories)
- WIN32;_DEBUG;_WINDOWS;_USRDLL;LIBOGG_EXPORTS;%(PreprocessorDefinitions)
- false
- EnableFastChecks
- MultiThreadedDebug
-
-
- Level3
- EditAndContinue
- CompileAsC
- Cdecl
- .\../../Link/Debug/libogg/
- .\../../Link/Debug/libogg/
- .\../../Link/Debug/libogg/
- .\../../Link/Debug/lpng/libogg.pch
-
-
-
-
- X64
-
-
- Disabled
- ..\..\Lib\libogg\include;%(AdditionalIncludeDirectories)
- WIN32;_DEBUG;_WINDOWS;_USRDLL;LIBOGG_EXPORTS;%(PreprocessorDefinitions)
- true
- EnableFastChecks
- MultiThreadedDebug
-
-
- Level3
- ProgramDatabase
- CompileAsC
- Cdecl
- .\../../Link/Debug/libogg/
- .\../../Link/Debug/libogg/
- .\../../Link/Debug/libogg/
-
-
-
-
- MaxSpeed
- AnySuitable
- true
- Speed
- ..\..\Lib\libogg\include;%(AdditionalIncludeDirectories)
- WIN32;NDEBUG;_WINDOWS;_USRDLL;LIBOGG_EXPORTS;%(PreprocessorDefinitions)
- true
-
-
- MultiThreaded
- false
-
-
- Level3
-
-
- CompileAsC
- 4244;%(DisableSpecificWarnings)
- Cdecl
- .\../../Link/Release/libogg/
- .\../../Link/Release/libogg/
- .\../../Link/Release/libogg/
-
-
-
-
- X64
-
-
- MaxSpeed
- AnySuitable
- true
- Speed
- ..\..\Lib\libogg\include;%(AdditionalIncludeDirectories)
- WIN32;NDEBUG;_WINDOWS;_USRDLL;LIBOGG_EXPORTS;%(PreprocessorDefinitions)
- true
-
-
- MultiThreaded
- false
-
-
- Level4
-
-
- CompileAsC
- 4244;%(DisableSpecificWarnings)
- Cdecl
-
-
-
-
-
-
\ No newline at end of file
diff --git a/engine/compilers/VisualStudio 2022/libvorbis.vcxproj b/engine/compilers/VisualStudio 2022/libvorbis.vcxproj
deleted file mode 100644
index 3c472107f..000000000
--- a/engine/compilers/VisualStudio 2022/libvorbis.vcxproj
+++ /dev/null
@@ -1,266 +0,0 @@
-
-
-
-
- Debug
- Win32
-
-
- Debug
- x64
-
-
- Release
- Win32
-
-
- Release
- x64
-
-
-
- {3A214E06-B95E-4D61-A291-1F8DF2EC10FD}
- libvorbis
- Win32Proj
- 10.0
-
-
-
- StaticLibrary
- Unicode
- false
- v143
-
-
- StaticLibrary
- Unicode
- v143
-
-
- StaticLibrary
- Unicode
- true
- v142
-
-
- StaticLibrary
- Unicode
- v143
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- <_ProjectFileVersion>10.0.30319.1
- .\../../Link/Debug\
- .\../../Link/Debug/libvorbis\
- .\../../Link/Debug\
- .\../../Link/Debug/libvorbis\
- .\../../Link/Release\
- .\../../Link/Release/libvorbis\
- .\../../Link/Release/
- .\../../Link/Release/libvorbis
-
-
- $(ProjectName)_DEBUG
-
-
-
- Disabled
- ..\..\Lib\libvorbis\lib;..\..\Lib\libvorbis\include;..\..\Lib\libogg\include;%(AdditionalIncludeDirectories)
- WIN32;_DEBUG;_WINDOWS;_USRDLL;LIBVORBIS_EXPORTS;%(PreprocessorDefinitions)
- false
- EnableFastChecks
- MultiThreadedDebug
-
-
- Level3
- EditAndContinue
- CompileAsC
- Cdecl
- .\../../Link/Debug/libvorbis/
- .\../../Link/Debug/libvorbis/
- .\../../Link/Debug/libvorbis/
- .\../../Link/Debug/lpng/libvorbis.pch
-
-
-
-
- X64
-
-
- Disabled
- ..\..\Lib\libvorbis\lib;..\..\Lib\libvorbis\include;..\..\Lib\libogg\include;%(AdditionalIncludeDirectories)
- WIN32;_DEBUG;_WINDOWS;_USRDLL;LIBVORBIS_EXPORTS;%(PreprocessorDefinitions)
- false
- EnableFastChecks
- MultiThreadedDebug
-
-
- Level3
- ProgramDatabase
- CompileAsC
- Cdecl
- .\../../Link/Debug/libvorbis/
- .\../../Link/Debug/libvorbis/
- 4244;4100;4267;4189;4305;4127;4706;%(DisableSpecificWarnings)
- AnySuitable
- true
- Speed
- false
- true
- false
- .\../../Link/Debug/libvorbis/
-
-
-
-
- Full
- AnySuitable
- true
- Speed
- ..\..\Lib\libvorbis\lib;..\..\Lib\libvorbis\include;..\..\Lib\libogg\include;%(AdditionalIncludeDirectories)
- WIN32;NDEBUG;_WINDOWS;_USRDLL;LIBVORBIS_EXPORTS;%(PreprocessorDefinitions)
- true
-
-
- MultiThreaded
- false
-
-
- Level3
- ProgramDatabase
- CompileAsC
- 4244;4100;4267;4189;4305;4127;4706;%(DisableSpecificWarnings)
- Cdecl
- .\../../Link/Release/libvorbis/
- .\../../Link/Release/libvorbis/
- .\../../Link/Release/libvorbis/
-
-
-
-
-
-
-
-
- X64
-
-
- Full
- AnySuitable
- true
- Speed
- ..\..\Lib\libvorbis\lib;..\..\Lib\libvorbis\include;..\..\Lib\libogg\include;%(AdditionalIncludeDirectories)
- WIN32;NDEBUG;_WINDOWS;_USRDLL;LIBVORBIS_EXPORTS;%(PreprocessorDefinitions)
- true
-
-
- MultiThreadedDLL
- false
-
-
- Level4
- ProgramDatabase
- CompileAsC
- 4244;4100;4267;4189;4305;4127;4706;%(DisableSpecificWarnings)
- Cdecl
-
-
- true
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {15cbfeff-7965-41f5-b4e2-21e8795c9159}
-
-
-
-
-
-
\ No newline at end of file
diff --git a/engine/compilers/VisualStudio 2022/ljpeg.vcxproj b/engine/compilers/VisualStudio 2022/ljpeg.vcxproj
deleted file mode 100644
index 8558bc1d8..000000000
--- a/engine/compilers/VisualStudio 2022/ljpeg.vcxproj
+++ /dev/null
@@ -1,1128 +0,0 @@
-
-
-
-
- Debug
- Win32
-
-
- Debug
- x64
-
-
- Release
- Win32
-
-
- Release
- x64
-
-
- Shipping
- Win32
-
-
- Shipping
- x64
-
-
-
- {0B07BA94-AA53-4FD4-ADB4-79EC2DA53B36}
- 10.0
-
-
-
- StaticLibrary
- false
- v143
-
-
- StaticLibrary
- false
- v143
-
-
- StaticLibrary
- false
- v143
-
-
- StaticLibrary
- false
- v142
-
-
- StaticLibrary
- false
- v143
-
-
- StaticLibrary
- false
- v143
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- <_ProjectFileVersion>10.0.30319.1
- .\../../Link/Release\
- .\../../Link/Release\
- .\../../Link/Release/ljpeg\
- .\../../Link/Release/ljpeg\
- .\../../Link/Debug\
- .\../../Link/Debug/ljpeg\
- $(ProjectName)_DEBUG
- $(ProjectName)_DEBUG
-
-
-
- MaxSpeed
- OnlyExplicitInline
- ljpeg;%(AdditionalIncludeDirectories)
- _CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- .\../../Link/Release/ljpeg/ljpeg.pch
- .\../../Link/Release/ljpeg/
- .\../../Link/Release/ljpeg/
- .\../../Link/Release/ljpeg/
- Level3
- true
- Default
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- .\../../Link/Release\ljpeg.lib
- true
-
-
-
-
- MaxSpeed
- OnlyExplicitInline
- ljpeg;%(AdditionalIncludeDirectories)
- _CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- .\../../Link/Release/ljpeg/ljpeg.pch
- .\../../Link/Release/ljpeg/
- .\../../Link/Release/ljpeg/
- .\../../Link/Release/ljpeg/
- Level3
- true
- Default
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- .\../../Link/Release\ljpeg.lib
- true
-
-
-
-
- MaxSpeed
- OnlyExplicitInline
- ljpeg;%(AdditionalIncludeDirectories)
- _CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- .\../../Link/Release/ljpeg/ljpeg.pch
- .\../../Link/Release/ljpeg/
- .\../../Link/Release/ljpeg/
- .\../../Link/Release/ljpeg/
- Level3
- true
- Default
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- .\../../Link/Release\ljpeg.lib
- true
-
-
-
-
- MaxSpeed
- OnlyExplicitInline
- ljpeg;%(AdditionalIncludeDirectories)
- _CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- .\../../Link/Release/ljpeg/ljpeg.pch
- .\../../Link/Release/ljpeg/
- .\../../Link/Release/ljpeg/
- .\../../Link/Release/ljpeg/
- Level3
- true
- Default
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- .\../../Link/Release\ljpeg.lib
- true
-
-
-
-
- Disabled
- ljpeg;%(AdditionalIncludeDirectories)
- TORQUE_DEBUG;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- EnableFastChecks
- MultiThreadedDebug
- false
- true
-
-
- .\../../Link/Debug/ljpeg/ljpeg.pch
- .\../../Link/Debug/ljpeg/
- .\../../Link/Debug/ljpeg/
- .\../../Link/Debug/ljpeg/
- Level3
- true
- EditAndContinue
- Default
-
-
- _DEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- $(OutDir)$(TargetName)$(TargetExt)
- true
-
-
-
-
- Disabled
- ljpeg;%(AdditionalIncludeDirectories)
- TORQUE_DEBUG;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- EnableFastChecks
- MultiThreadedDebug
- false
- true
-
-
- .\../../Link/Debug/ljpeg/ljpeg.pch
- .\../../Link/Debug/ljpeg/
- .\../../Link/Debug/ljpeg/
- .\../../Link/Debug/ljpeg/
- Level3
- true
- ProgramDatabase
- Default
-
-
- _DEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- .\../../Link/Debug\ljpeg.lib
- true
-
-
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
- Disabled
- Disabled
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(PreprocessorDefinitions)
- %(PreprocessorDefinitions)
- EnableFastChecks
- EnableFastChecks
- MaxSpeed
- MaxSpeed
- MaxSpeed
- MaxSpeed
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
- %(AdditionalIncludeDirectories)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/engine/compilers/VisualStudio 2022/ljpeg.vcxproj.filters b/engine/compilers/VisualStudio 2022/ljpeg.vcxproj.filters
deleted file mode 100644
index e7df8bc60..000000000
--- a/engine/compilers/VisualStudio 2022/ljpeg.vcxproj.filters
+++ /dev/null
@@ -1,65 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/engine/compilers/VisualStudio 2022/lpng.vcxproj b/engine/compilers/VisualStudio 2022/lpng.vcxproj
deleted file mode 100644
index 4868bf1a8..000000000
--- a/engine/compilers/VisualStudio 2022/lpng.vcxproj
+++ /dev/null
@@ -1,316 +0,0 @@
-
-
-
-
- Debug
- Win32
-
-
- Debug
- x64
-
-
- Release
- Win32
-
-
- Release
- x64
-
-
- Shipping
- Win32
-
-
- Shipping
- x64
-
-
-
- {AF1179E3-A838-46A3-A427-1E62AA4C52F4}
- 10.0
-
-
-
- StaticLibrary
- false
- v143
-
-
- StaticLibrary
- false
- v142
-
-
- StaticLibrary
- false
- v143
-
-
- StaticLibrary
- false
- v143
-
-
- StaticLibrary
- false
- v143
-
-
- StaticLibrary
- false
- v143
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- <_ProjectFileVersion>10.0.30319.1
- .\../../Link/Debug\
- .\../../Link/Debug/lpng\
- .\../../Link/Release\
- .\../../Link/Release\
- .\../../Link/Release/lpng\
- .\../../Link/Release/lpng\
- $(ProjectName)_DEBUG
- $(ProjectName)_DEBUG
-
-
-
- Disabled
- ..\..\Lib\lpng;..\..\Lib\zlib;%(AdditionalIncludeDirectories)
- TORQUE_DEBUG;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- EnableFastChecks
- MultiThreadedDebug
- false
- true
-
-
- .\../../Link/Debug/lpng/lpng.pch
- .\../../Link/Debug/lpng/
- .\../../Link/Debug/lpng/
- .\../../Link/Debug/lpng/
- Level3
- true
- EditAndContinue
- Default
-
-
- _DEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- $(OutDir)$(TargetName)$(TargetExt)
- true
-
-
-
-
- Disabled
- ..\..\Lib\lpng;..\..\Lib\zlib;%(AdditionalIncludeDirectories)
- TORQUE_DEBUG;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- EnableFastChecks
- MultiThreadedDebug
- false
- true
-
-
- .\../../Link/Debug/lpng/lpng.pch
- .\../../Link/Debug/lpng/
- .\../../Link/Debug/lpng/
- .\../../Link/Debug/lpng/
- Level3
- true
- ProgramDatabase
- Default
-
-
- _DEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- .\../../Link/Debug\lpng.lib
- true
-
-
-
-
- MaxSpeed
- OnlyExplicitInline
- ..\..\Lib\lpng;..\..\Lib\zlib;%(AdditionalIncludeDirectories)
- _CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- .\../../Link/Release/lpng/lpng.pch
- .\../../Link/Release/lpng/
- .\../../Link/Release/lpng/
- .\../../Link/Release/lpng/
- Level3
- true
- Default
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- .\../../Link/Release\lpng.lib
- true
-
-
-
-
- MaxSpeed
- OnlyExplicitInline
- ..\..\Lib\lpng;..\..\Lib\zlib;%(AdditionalIncludeDirectories)
- _CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- .\../../Link/Release/lpng/lpng.pch
- .\../../Link/Release/lpng/
- .\../../Link/Release/lpng/
- .\../../Link/Release/lpng/
- Level3
- true
- Default
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- .\../../Link/Release\lpng.lib
- true
-
-
-
-
- MaxSpeed
- OnlyExplicitInline
- ..\..\Lib\lpng;..\..\Lib\zlib;%(AdditionalIncludeDirectories)
- _CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- .\../../Link/Release/lpng/lpng.pch
- .\../../Link/Release/lpng/
- .\../../Link/Release/lpng/
- .\../../Link/Release/lpng/
- Level3
- true
- Default
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- .\../../Link/Release\lpng.lib
- true
-
-
-
-
- MaxSpeed
- OnlyExplicitInline
- ..\..\Lib\lpng;..\..\Lib\zlib;%(AdditionalIncludeDirectories)
- _CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- .\../../Link/Release/lpng/lpng.pch
- .\../../Link/Release/lpng/
- .\../../Link/Release/lpng/
- .\../../Link/Release/lpng/
- Level3
- true
- Default
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- .\../../Link/Release\lpng.lib
- true
-
-
-
-
- {86cb2525-0cf3-40d3-bf42-a0a95035ee8c}
- false
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/engine/compilers/VisualStudio 2022/lpng.vcxproj.filters b/engine/compilers/VisualStudio 2022/lpng.vcxproj.filters
deleted file mode 100644
index dd7259716..000000000
--- a/engine/compilers/VisualStudio 2022/lpng.vcxproj.filters
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/engine/compilers/VisualStudio 2022/resource.h b/engine/compilers/VisualStudio 2022/resource.h
deleted file mode 100644
index 2e8e9751f..000000000
--- a/engine/compilers/VisualStudio 2022/resource.h
+++ /dev/null
@@ -1,16 +0,0 @@
-//{{NO_DEPENDENCIES}}
-// Microsoft Visual C++ generated include file.
-// Used by Torque 2D.rc
-//
-#define IDI_TORQUE2D 107
-
-// Next default values for new objects
-//
-#ifdef APSTUDIO_INVOKED
-#ifndef APSTUDIO_READONLY_SYMBOLS
-#define _APS_NEXT_RESOURCE_VALUE 108
-#define _APS_NEXT_COMMAND_VALUE 40001
-#define _APS_NEXT_CONTROL_VALUE 1000
-#define _APS_NEXT_SYMED_VALUE 101
-#endif
-#endif
diff --git a/engine/compilers/VisualStudio 2022/zlib.vcxproj b/engine/compilers/VisualStudio 2022/zlib.vcxproj
deleted file mode 100644
index 0e3ca474c..000000000
--- a/engine/compilers/VisualStudio 2022/zlib.vcxproj
+++ /dev/null
@@ -1,314 +0,0 @@
-
-
-
-
- Debug
- Win32
-
-
- Debug
- x64
-
-
- Release
- Win32
-
-
- Release
- x64
-
-
- Shipping
- Win32
-
-
- Shipping
- x64
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {86CB2525-0CF3-40D3-BF42-A0A95035EE8C}
- 10.0
-
-
-
- StaticLibrary
- false
- v143
-
-
- StaticLibrary
- false
- v142
-
-
- StaticLibrary
- false
- v143
-
-
- StaticLibrary
- false
- v143
-
-
- StaticLibrary
- false
- v143
-
-
- StaticLibrary
- false
- v143
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- <_ProjectFileVersion>10.0.30319.1
- .\../../Link/Debug\
- .\../../Link/Debug/zlib\
- .\../../Link/Release\
- .\../../Link/Release\
- .\../../Link/Release/zlib\
- .\../../Link/Release/zlib\
- $(ProjectName)_DEBUG
- $(ProjectName)_DEBUG
-
-
-
- Disabled
- zlib;%(AdditionalIncludeDirectories)
- TORQUE_DEBUG;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- EnableFastChecks
- MultiThreadedDebug
- false
- true
-
-
- .\../../Link/Debug/zlib/zlib.pch
- .\../../Link/Debug/zlib/
- .\../../Link/Debug/zlib/
- .\../../Link/Debug/zlib/
- Level3
- true
- EditAndContinue
- Default
-
-
- _DEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- $(OutDir)$(TargetName)$(TargetExt)
- true
-
-
-
-
- Disabled
- zlib;%(AdditionalIncludeDirectories)
- TORQUE_DEBUG;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- EnableFastChecks
- MultiThreadedDebug
- false
- true
-
-
- .\../../Link/Debug/zlib/zlib.pch
- .\../../Link/Debug/zlib/
- .\../../Link/Debug/zlib/
- .\../../Link/Debug/zlib/
- Level3
- true
- ProgramDatabase
- Default
-
-
- _DEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- .\../../Link/Debug\zlib.lib
- true
-
-
-
-
- MaxSpeed
- OnlyExplicitInline
- zlib;%(AdditionalIncludeDirectories)
- _CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- .\../../Link/Release/zlib/zlib.pch
- .\../../Link/Release/zlib/
- .\../../Link/Release/zlib/
- .\../../Link/Release/zlib/
- Level3
- true
- Default
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- .\../../Link/Release\zlib.lib
- true
-
-
-
-
- MaxSpeed
- OnlyExplicitInline
- zlib;%(AdditionalIncludeDirectories)
- _CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- .\../../Link/Release/zlib/zlib.pch
- .\../../Link/Release/zlib/
- .\../../Link/Release/zlib/
- .\../../Link/Release/zlib/
- Level3
- true
- Default
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- .\../../Link/Release\zlib.lib
- true
-
-
-
-
- MaxSpeed
- OnlyExplicitInline
- zlib;%(AdditionalIncludeDirectories)
- _CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- .\../../Link/Release/zlib/zlib.pch
- .\../../Link/Release/zlib/
- .\../../Link/Release/zlib/
- .\../../Link/Release/zlib/
- Level3
- true
- Default
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- .\../../Link/Release\zlib.lib
- true
-
-
-
-
- MaxSpeed
- OnlyExplicitInline
- zlib;%(AdditionalIncludeDirectories)
- _CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)
- true
- MultiThreaded
- true
- false
- true
-
-
- .\../../Link/Release/zlib/zlib.pch
- .\../../Link/Release/zlib/
- .\../../Link/Release/zlib/
- .\../../Link/Release/zlib/
- Level3
- true
- Default
-
-
- NDEBUG;%(PreprocessorDefinitions)
- 0x0409
-
-
- .\../../Link/Release\zlib.lib
- true
-
-
-
-
-
-
\ No newline at end of file
diff --git a/engine/compilers/VisualStudio 2022/zlib.vcxproj.filters b/engine/compilers/VisualStudio 2022/zlib.vcxproj.filters
deleted file mode 100644
index 5795e3953..000000000
--- a/engine/compilers/VisualStudio 2022/zlib.vcxproj.filters
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/engine/compilers/Xcode/Torque2D.xcodeproj/project.pbxproj b/engine/compilers/Xcode/Torque2D.xcodeproj/project.pbxproj
deleted file mode 100755
index 8c6e6ec11..000000000
--- a/engine/compilers/Xcode/Torque2D.xcodeproj/project.pbxproj
+++ /dev/null
@@ -1,4354 +0,0 @@
-// !$*UTF8*$!
-{
- archiveVersion = 1;
- classes = {
- };
- objectVersion = 55;
- objects = {
-
-/* Begin PBXBuildFile section */
- 06D1686A1C1F949D009A1AD1 /* vorbisStreamSource.cc in Sources */ = {isa = PBXBuildFile; fileRef = 06D168681C1F949D009A1AD1 /* vorbisStreamSource.cc */; };
- 06D1686B1C1F949D009A1AD1 /* vorbisStreamSource.h in Sources */ = {isa = PBXBuildFile; fileRef = 06D168691C1F949D009A1AD1 /* vorbisStreamSource.h */; };
- 07738F0427EA9E08009B4B15 /* mFluid.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 07738F0327EA9E08009B4B15 /* mFluid.cpp */; };
- 0787E04B27EBC869001EAA71 /* trees.c in Sources */ = {isa = PBXBuildFile; fileRef = 0787E03227EBC867001EAA71 /* trees.c */; };
- 0787E04C27EBC869001EAA71 /* inftrees.c in Sources */ = {isa = PBXBuildFile; fileRef = 0787E03327EBC867001EAA71 /* inftrees.c */; };
- 0787E04D27EBC869001EAA71 /* zutil.c in Sources */ = {isa = PBXBuildFile; fileRef = 0787E03527EBC867001EAA71 /* zutil.c */; };
- 0787E04E27EBC869001EAA71 /* compress.c in Sources */ = {isa = PBXBuildFile; fileRef = 0787E03627EBC867001EAA71 /* compress.c */; };
- 0787E04F27EBC869001EAA71 /* inflate.c in Sources */ = {isa = PBXBuildFile; fileRef = 0787E03727EBC867001EAA71 /* inflate.c */; };
- 0787E05027EBC869001EAA71 /* infback.c in Sources */ = {isa = PBXBuildFile; fileRef = 0787E03827EBC867001EAA71 /* infback.c */; };
- 0787E05127EBC869001EAA71 /* gzclose.c in Sources */ = {isa = PBXBuildFile; fileRef = 0787E03B27EBC868001EAA71 /* gzclose.c */; };
- 0787E05227EBC869001EAA71 /* gzread.c in Sources */ = {isa = PBXBuildFile; fileRef = 0787E03C27EBC868001EAA71 /* gzread.c */; };
- 0787E05327EBC869001EAA71 /* deflate.c in Sources */ = {isa = PBXBuildFile; fileRef = 0787E03D27EBC868001EAA71 /* deflate.c */; };
- 0787E05427EBC869001EAA71 /* adler32.c in Sources */ = {isa = PBXBuildFile; fileRef = 0787E04227EBC868001EAA71 /* adler32.c */; };
- 0787E05527EBC869001EAA71 /* gzwrite.c in Sources */ = {isa = PBXBuildFile; fileRef = 0787E04527EBC869001EAA71 /* gzwrite.c */; };
- 0787E05627EBC869001EAA71 /* inffast.c in Sources */ = {isa = PBXBuildFile; fileRef = 0787E04627EBC869001EAA71 /* inffast.c */; };
- 0787E05727EBC869001EAA71 /* uncompr.c in Sources */ = {isa = PBXBuildFile; fileRef = 0787E04727EBC869001EAA71 /* uncompr.c */; };
- 0787E05827EBC869001EAA71 /* crc32.c in Sources */ = {isa = PBXBuildFile; fileRef = 0787E04827EBC869001EAA71 /* crc32.c */; };
- 0787E05927EBC869001EAA71 /* gzlib.c in Sources */ = {isa = PBXBuildFile; fileRef = 0787E04927EBC869001EAA71 /* gzlib.c */; };
- 07F98829274F1B0B009ECC0D /* guiMenuBarCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 07F98825274F1B0B009ECC0D /* guiMenuBarCtrl.cc */; };
- 07F9882A274F1B0B009ECC0D /* guiParticleGraphInspector.cc in Sources */ = {isa = PBXBuildFile; fileRef = 07F98828274F1B0B009ECC0D /* guiParticleGraphInspector.cc */; };
- 07F9883D274F1C21009ECC0D /* guiPanelCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 07F9882C274F1C20009ECC0D /* guiPanelCtrl.cc */; };
- 07F9883E274F1C21009ECC0D /* guiExpandCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 07F9882D274F1C20009ECC0D /* guiExpandCtrl.cc */; };
- 07F9883F274F1C22009ECC0D /* guiTabPageCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 07F98831274F1C20009ECC0D /* guiTabPageCtrl.cc */; };
- 07F98840274F1C22009ECC0D /* guiChainCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 07F98835274F1C21009ECC0D /* guiChainCtrl.cc */; };
- 07F98841274F1C22009ECC0D /* guiSceneScrollCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 07F98837274F1C21009ECC0D /* guiSceneScrollCtrl.cc */; };
- 07F98847274F1C7F009ECC0D /* guiDropDownCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 07F98846274F1C7F009ECC0D /* guiDropDownCtrl.cc */; };
- 07F9884E274F2596009ECC0D /* gColor.cc in Sources */ = {isa = PBXBuildFile; fileRef = 07F9884C274F2596009ECC0D /* gColor.cc */; };
- 2A03300D165D1D2100E9CD70 /* unitTesting.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2A03300B165D1D2100E9CD70 /* unitTesting.cc */; };
- 2A033011165D1D4100E9CD70 /* platformFileIoTests.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2A033010165D1D4100E9CD70 /* platformFileIoTests.cc */; };
- 2A25739016A48DAC00363C6F /* ParticlePlayer.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2A25738E16A48DAC00363C6F /* ParticlePlayer.cc */; };
- 2A6F78CE16A4528C005C76D9 /* ParticleAssetEmitter.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2A6F78CC16A4528C005C76D9 /* ParticleAssetEmitter.cc */; };
- 2AA3655916F3552200E7A900 /* ImageFrameProvider.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2AA3655516F3552200E7A900 /* ImageFrameProvider.cc */; };
- 2AA3655A16F3552200E7A900 /* ImageFrameProviderCore.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2AA3655716F3552200E7A900 /* ImageFrameProviderCore.cc */; };
- 2AA6865F16D69943003CEF0A /* SceneObjectList.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2AA6865A16D69943003CEF0A /* SceneObjectList.cc */; };
- 2AA6866016D69943003CEF0A /* SceneObjectSet.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2AA6865D16D69943003CEF0A /* SceneObjectSet.cc */; };
- 2AB14A0516D7CDC300EABBF2 /* PointForceController.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2AB14A0316D7CDC200EABBF2 /* PointForceController.cc */; };
- 2AB4C19E16DE9F0600B02479 /* GroupedSceneController.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2AB4C19816DE9F0600B02479 /* GroupedSceneController.cc */; };
- 2AB4C19F16DE9F0600B02479 /* PickingSceneController.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2AB4C19B16DE9F0600B02479 /* PickingSceneController.cc */; };
- 2AB4C1A316DE9F1100B02479 /* AmbientForceController.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2AB4C1A116DE9F1100B02479 /* AmbientForceController.cc */; };
- 2AB97A1D16B66BC70080F940 /* tamlCustom.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2AB97A1B16B66BC70080F940 /* tamlCustom.cc */; };
- 2ABF5C8F16569A0C00BBBF1D /* osxMutex.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2ABF5C8E16569A0C00BBBF1D /* osxMutex.mm */; };
- 2AC5C7E81667C85700A0D046 /* platformStringTests.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2AC5C7E71667C85700A0D046 /* platformStringTests.cc */; };
- 2ACAFD4A1705CF4A0022601C /* tamlJSONParser.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2ACAFD481705CF4A0022601C /* tamlJSONParser.cc */; };
- 2ACF5A2816E52D4B00F838D9 /* SpriteBatchQuery.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2ACF5A2516E52D4B00F838D9 /* SpriteBatchQuery.cc */; };
- 2ACFC0A8166CE1AB00FE7370 /* platformMemoryTests.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2ACFC0A7166CE1AB00FE7370 /* platformMemoryTests.cc */; };
- 2AD42140170433FE005BB8AD /* tamlXmlParser.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2AD42139170433FE005BB8AD /* tamlXmlParser.cc */; };
- 2AD42141170433FE005BB8AD /* tamlXmlReader.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2AD4213B170433FE005BB8AD /* tamlXmlReader.cc */; };
- 2AD42142170433FE005BB8AD /* tamlXmlWriter.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2AD4213E170433FE005BB8AD /* tamlXmlWriter.cc */; };
- 2AD4214717043408005BB8AD /* tamlJSONReader.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2AD4214317043408005BB8AD /* tamlJSONReader.cc */; };
- 2AD4214817043408005BB8AD /* tamlJSONWriter.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2AD4214517043408005BB8AD /* tamlJSONWriter.cc */; };
- 2AD4214D17043413005BB8AD /* tamlBinaryReader.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2AD4214917043413005BB8AD /* tamlBinaryReader.cc */; };
- 2AD4214E17043413005BB8AD /* tamlBinaryWriter.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2AD4214B17043413005BB8AD /* tamlBinaryWriter.cc */; };
- 2ADCAC1516A41E5500E07619 /* ParticleAsset.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2ADCAC1116A41E5500E07619 /* ParticleAsset.cc */; };
- 2ADCAC1716A41E5500E07619 /* ParticleAssetField.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2ADCAC1316A41E5500E07619 /* ParticleAssetField.cc */; };
- 2AE2938516EF4C220015E200 /* WaveComposite.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2AE2938316EF4C220015E200 /* WaveComposite.cc */; };
- 2AE2F55D16D6B08800B6A058 /* BuoyancyController.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2AE2F55B16D6B08800B6A058 /* BuoyancyController.cc */; };
- 2AE5B54216A6D860006908D5 /* ParticleAssetFieldCollection.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2AE5B54016A6D860006908D5 /* ParticleAssetFieldCollection.cc */; };
- 2AF1C54016B439BB00C1CF3A /* declaredAssets.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2AF1C53C16B439BB00C1CF3A /* declaredAssets.cc */; };
- 2AF1C54116B439BB00C1CF3A /* referencedAssets.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2AF1C53E16B439BB00C1CF3A /* referencedAssets.cc */; };
- 2AF3633916A9BBE0004ED7AA /* ParticleSystem.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2AF3633716A9BBE0004ED7AA /* ParticleSystem.cc */; };
- 2B4314C21F1D024900A5C0B7 /* platformNet_ScriptBinding.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2B4314BD1F1D024900A5C0B7 /* platformNet_ScriptBinding.cc */; };
- 2B4314C31F1D024900A5C0B7 /* platformNet.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2B4314BE1F1D024900A5C0B7 /* platformNet.cpp */; };
- 2B4314C41F1D024900A5C0B7 /* platformNetAsync.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2B4314C01F1D024900A5C0B7 /* platformNetAsync.cpp */; };
- 2B5F12AC1F1DBC7C006D2B4F /* byteBuffer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2B5F12AA1F1DBC7C006D2B4F /* byteBuffer.cpp */; };
- 32F6F52E24A5E110008E28D2 /* b2Rope.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F4B524A5E110008E28D2 /* b2Rope.cpp */; };
- 32F6F52F24A5E110008E28D2 /* b2Particle.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F4B824A5E110008E28D2 /* b2Particle.cpp */; };
- 32F6F53224A5E110008E28D2 /* b2ParticleGroup.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F4BD24A5E110008E28D2 /* b2ParticleGroup.cpp */; };
- 32F6F53324A5E110008E28D2 /* b2ParticleSystem.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F4BF24A5E110008E28D2 /* b2ParticleSystem.cpp */; };
- 32F6F53424A5E110008E28D2 /* b2VoronoiDiagram.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F4C224A5E110008E28D2 /* b2VoronoiDiagram.cpp */; };
- 32F6F53524A5E110008E28D2 /* b2BlockAllocator.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F4C524A5E110008E28D2 /* b2BlockAllocator.cpp */; };
- 32F6F53624A5E110008E28D2 /* b2Draw.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F4C724A5E110008E28D2 /* b2Draw.cpp */; };
- 32F6F53724A5E110008E28D2 /* b2FreeList.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F4C924A5E110008E28D2 /* b2FreeList.cpp */; };
- 32F6F53824A5E110008E28D2 /* b2Math.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F4CE24A5E110008E28D2 /* b2Math.cpp */; };
- 32F6F53924A5E110008E28D2 /* b2Settings.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F4D024A5E110008E28D2 /* b2Settings.cpp */; };
- 32F6F53A24A5E110008E28D2 /* b2StackAllocator.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F4D324A5E110008E28D2 /* b2StackAllocator.cpp */; };
- 32F6F53B24A5E110008E28D2 /* b2Stat.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F4D524A5E110008E28D2 /* b2Stat.cpp */; };
- 32F6F53C24A5E110008E28D2 /* b2Timer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F4D724A5E110008E28D2 /* b2Timer.cpp */; };
- 32F6F53D24A5E110008E28D2 /* b2TrackedBlock.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F4D924A5E110008E28D2 /* b2TrackedBlock.cpp */; };
- 32F6F53E24A5E110008E28D2 /* b2Body.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F4DD24A5E110008E28D2 /* b2Body.cpp */; };
- 32F6F53F24A5E110008E28D2 /* b2ContactManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F4DF24A5E110008E28D2 /* b2ContactManager.cpp */; };
- 32F6F54024A5E110008E28D2 /* b2Fixture.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F4E124A5E110008E28D2 /* b2Fixture.cpp */; };
- 32F6F54124A5E110008E28D2 /* b2Island.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F4E324A5E110008E28D2 /* b2Island.cpp */; };
- 32F6F54224A5E110008E28D2 /* b2World.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F4E624A5E110008E28D2 /* b2World.cpp */; };
- 32F6F54324A5E110008E28D2 /* b2WorldCallbacks.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F4E824A5E110008E28D2 /* b2WorldCallbacks.cpp */; };
- 32F6F54424A5E110008E28D2 /* b2ChainAndCircleContact.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F4EB24A5E110008E28D2 /* b2ChainAndCircleContact.cpp */; };
- 32F6F54524A5E110008E28D2 /* b2ChainAndPolygonContact.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F4ED24A5E110008E28D2 /* b2ChainAndPolygonContact.cpp */; };
- 32F6F54624A5E110008E28D2 /* b2CircleContact.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F4EF24A5E110008E28D2 /* b2CircleContact.cpp */; };
- 32F6F54724A5E110008E28D2 /* b2Contact.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F4F124A5E110008E28D2 /* b2Contact.cpp */; };
- 32F6F54824A5E110008E28D2 /* b2ContactSolver.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F4F324A5E110008E28D2 /* b2ContactSolver.cpp */; };
- 32F6F54924A5E110008E28D2 /* b2EdgeAndCircleContact.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F4F524A5E110008E28D2 /* b2EdgeAndCircleContact.cpp */; };
- 32F6F54A24A5E110008E28D2 /* b2EdgeAndPolygonContact.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F4F724A5E110008E28D2 /* b2EdgeAndPolygonContact.cpp */; };
- 32F6F54B24A5E111008E28D2 /* b2PolygonAndCircleContact.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F4F924A5E110008E28D2 /* b2PolygonAndCircleContact.cpp */; };
- 32F6F54C24A5E111008E28D2 /* b2PolygonContact.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F4FB24A5E110008E28D2 /* b2PolygonContact.cpp */; };
- 32F6F54D24A5E111008E28D2 /* b2DistanceJoint.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F4FE24A5E110008E28D2 /* b2DistanceJoint.cpp */; };
- 32F6F54E24A5E111008E28D2 /* b2FrictionJoint.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F50024A5E110008E28D2 /* b2FrictionJoint.cpp */; };
- 32F6F54F24A5E111008E28D2 /* b2GearJoint.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F50224A5E110008E28D2 /* b2GearJoint.cpp */; };
- 32F6F55024A5E111008E28D2 /* b2Joint.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F50424A5E110008E28D2 /* b2Joint.cpp */; };
- 32F6F55124A5E111008E28D2 /* b2MotorJoint.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F50624A5E110008E28D2 /* b2MotorJoint.cpp */; };
- 32F6F55224A5E111008E28D2 /* b2MouseJoint.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F50824A5E110008E28D2 /* b2MouseJoint.cpp */; };
- 32F6F55324A5E111008E28D2 /* b2PrismaticJoint.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F50A24A5E110008E28D2 /* b2PrismaticJoint.cpp */; };
- 32F6F55424A5E111008E28D2 /* b2PulleyJoint.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F50C24A5E110008E28D2 /* b2PulleyJoint.cpp */; };
- 32F6F55524A5E111008E28D2 /* b2RevoluteJoint.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F50E24A5E110008E28D2 /* b2RevoluteJoint.cpp */; };
- 32F6F55624A5E111008E28D2 /* b2RopeJoint.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F51024A5E110008E28D2 /* b2RopeJoint.cpp */; };
- 32F6F55724A5E111008E28D2 /* b2WeldJoint.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F51224A5E110008E28D2 /* b2WeldJoint.cpp */; };
- 32F6F55824A5E111008E28D2 /* b2WheelJoint.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F51424A5E110008E28D2 /* b2WheelJoint.cpp */; };
- 32F6F55924A5E111008E28D2 /* b2BroadPhase.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F51724A5E110008E28D2 /* b2BroadPhase.cpp */; };
- 32F6F55A24A5E111008E28D2 /* b2CollideCircle.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F51924A5E110008E28D2 /* b2CollideCircle.cpp */; };
- 32F6F55B24A5E111008E28D2 /* b2CollideEdge.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F51A24A5E110008E28D2 /* b2CollideEdge.cpp */; };
- 32F6F55C24A5E111008E28D2 /* b2CollidePolygon.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F51B24A5E110008E28D2 /* b2CollidePolygon.cpp */; };
- 32F6F55D24A5E111008E28D2 /* b2Collision.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F51C24A5E110008E28D2 /* b2Collision.cpp */; };
- 32F6F55E24A5E111008E28D2 /* b2Distance.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F51E24A5E110008E28D2 /* b2Distance.cpp */; };
- 32F6F55F24A5E111008E28D2 /* b2DynamicTree.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F52024A5E110008E28D2 /* b2DynamicTree.cpp */; };
- 32F6F56024A5E111008E28D2 /* b2TimeOfImpact.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F52224A5E110008E28D2 /* b2TimeOfImpact.cpp */; };
- 32F6F56124A5E111008E28D2 /* b2ChainShape.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F52524A5E110008E28D2 /* b2ChainShape.cpp */; };
- 32F6F56224A5E111008E28D2 /* b2CircleShape.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F52724A5E110008E28D2 /* b2CircleShape.cpp */; };
- 32F6F56324A5E111008E28D2 /* b2EdgeShape.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F52924A5E110008E28D2 /* b2EdgeShape.cpp */; };
- 32F6F56424A5E111008E28D2 /* b2PolygonShape.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F52B24A5E110008E28D2 /* b2PolygonShape.cpp */; };
- 32F6F56B24A5E192008E28D2 /* Path.cc in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F56524A5E191008E28D2 /* Path.cc */; };
- 32F6F56C24A5E192008E28D2 /* LightObject.cc in Sources */ = {isa = PBXBuildFile; fileRef = 32F6F56624A5E192008E28D2 /* LightObject.cc */; };
- 86063A251654180000362D83 /* platformOSX.mm in Sources */ = {isa = PBXBuildFile; fileRef = 86063A241654180000362D83 /* platformOSX.mm */; };
- 8609FE2F16556DD2004662ED /* osxSemaphore.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8609FE2E16556DD2004662ED /* osxSemaphore.mm */; };
- 8609FE3116556E5A004662ED /* osxThread.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8609FE3016556E5A004662ED /* osxThread.mm */; };
- 8609FE361655716E004662ED /* osxPopupMenu.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8609FE351655716E004662ED /* osxPopupMenu.mm */; };
- 8609FE38165572EC004662ED /* osxFont.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8609FE37165572EC004662ED /* osxFont.mm */; };
- 861CD8D01678F6C200DAE1A0 /* fileDialog.cc in Sources */ = {isa = PBXBuildFile; fileRef = 861CD8CF1678F6C200DAE1A0 /* fileDialog.cc */; };
- 8645C96C1887231C004ED987 /* mPoint.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 8645C96B1887231C004ED987 /* mPoint.cpp */; };
- 8652C279165586520052D0CB /* osxAudio.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8652C278165586520052D0CB /* osxAudio.mm */; };
- 8652F2A016C146CF00639EFE /* torque2d.icns in Resources */ = {isa = PBXBuildFile; fileRef = 8652F29F16C146CF00639EFE /* torque2d.icns */; };
- 8658B174165A7BFB0087ABC1 /* osxCPU.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8658B171165A7BFB0087ABC1 /* osxCPU.mm */; };
- 8658B176165A7BFB0087ABC1 /* osxString.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8658B173165A7BFB0087ABC1 /* osxString.mm */; };
- 865A20CA16515B1E00527C44 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 869FF8BF1651518C002FE082 /* AppKit.framework */; };
- 865A20CC16515B1E00527C44 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 869FF8BC1651518C002FE082 /* Cocoa.framework */; };
- 865A20CD16515B1E00527C44 /* CoreData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 869FF8C01651518C002FE082 /* CoreData.framework */; };
- 865A20CE16515B1E00527C44 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 865A20C216515ACE00527C44 /* CoreFoundation.framework */; };
- 865A20CF16515B1E00527C44 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 869FF8C11651518C002FE082 /* Foundation.framework */; };
- 865A20D016515B1E00527C44 /* OpenAL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 865A20C316515ACE00527C44 /* OpenAL.framework */; };
- 865A20D116515B1E00527C44 /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 865A20C416515ACE00527C44 /* OpenGL.framework */; };
- 865A2305165187FF00527C44 /* jcapimin.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22D7165187FF00527C44 /* jcapimin.c */; };
- 865A2306165187FF00527C44 /* jcapistd.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22D8165187FF00527C44 /* jcapistd.c */; };
- 865A2307165187FF00527C44 /* jccoefct.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22D9165187FF00527C44 /* jccoefct.c */; };
- 865A2308165187FF00527C44 /* jccolor.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22DA165187FF00527C44 /* jccolor.c */; };
- 865A2309165187FF00527C44 /* jcdctmgr.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22DB165187FF00527C44 /* jcdctmgr.c */; };
- 865A230A165187FF00527C44 /* jchuff.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22DC165187FF00527C44 /* jchuff.c */; };
- 865A230B165187FF00527C44 /* jcinit.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22DD165187FF00527C44 /* jcinit.c */; };
- 865A230C165187FF00527C44 /* jcmainct.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22DE165187FF00527C44 /* jcmainct.c */; };
- 865A230D165187FF00527C44 /* jcmarker.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22DF165187FF00527C44 /* jcmarker.c */; };
- 865A230E165187FF00527C44 /* jcmaster.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22E0165187FF00527C44 /* jcmaster.c */; };
- 865A230F165187FF00527C44 /* jcomapi.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22E1165187FF00527C44 /* jcomapi.c */; };
- 865A2310165187FF00527C44 /* jcparam.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22E2165187FF00527C44 /* jcparam.c */; };
- 865A2311165187FF00527C44 /* jcphuff.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22E3165187FF00527C44 /* jcphuff.c */; };
- 865A2312165187FF00527C44 /* jcprepct.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22E4165187FF00527C44 /* jcprepct.c */; };
- 865A2313165187FF00527C44 /* jcsample.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22E5165187FF00527C44 /* jcsample.c */; };
- 865A2314165187FF00527C44 /* jctrans.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22E6165187FF00527C44 /* jctrans.c */; };
- 865A2315165187FF00527C44 /* jdapimin.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22E7165187FF00527C44 /* jdapimin.c */; };
- 865A2316165187FF00527C44 /* jdapistd.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22E8165187FF00527C44 /* jdapistd.c */; };
- 865A2317165187FF00527C44 /* jdatadst.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22E9165187FF00527C44 /* jdatadst.c */; };
- 865A2318165187FF00527C44 /* jdatasrc.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22EA165187FF00527C44 /* jdatasrc.c */; };
- 865A2319165187FF00527C44 /* jdcoefct.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22EB165187FF00527C44 /* jdcoefct.c */; };
- 865A231A165187FF00527C44 /* jdcolor.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22EC165187FF00527C44 /* jdcolor.c */; };
- 865A231B165187FF00527C44 /* jddctmgr.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22ED165187FF00527C44 /* jddctmgr.c */; };
- 865A231C165187FF00527C44 /* jdhuff.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22EE165187FF00527C44 /* jdhuff.c */; };
- 865A231D165187FF00527C44 /* jdinput.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22EF165187FF00527C44 /* jdinput.c */; };
- 865A231E165187FF00527C44 /* jdmainct.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22F0165187FF00527C44 /* jdmainct.c */; };
- 865A231F165187FF00527C44 /* jdmarker.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22F1165187FF00527C44 /* jdmarker.c */; };
- 865A2320165187FF00527C44 /* jdmaster.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22F2165187FF00527C44 /* jdmaster.c */; };
- 865A2321165187FF00527C44 /* jdmerge.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22F3165187FF00527C44 /* jdmerge.c */; };
- 865A2322165187FF00527C44 /* jdphuff.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22F4165187FF00527C44 /* jdphuff.c */; };
- 865A2323165187FF00527C44 /* jdpostct.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22F5165187FF00527C44 /* jdpostct.c */; };
- 865A2324165187FF00527C44 /* jdsample.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22F6165187FF00527C44 /* jdsample.c */; };
- 865A2325165187FF00527C44 /* jdtrans.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22F7165187FF00527C44 /* jdtrans.c */; };
- 865A2326165187FF00527C44 /* jerror.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22F8165187FF00527C44 /* jerror.c */; };
- 865A2327165187FF00527C44 /* jfdctflt.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22F9165187FF00527C44 /* jfdctflt.c */; };
- 865A2328165187FF00527C44 /* jfdctfst.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22FA165187FF00527C44 /* jfdctfst.c */; };
- 865A2329165187FF00527C44 /* jfdctint.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22FB165187FF00527C44 /* jfdctint.c */; };
- 865A232A165187FF00527C44 /* jidctflt.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22FC165187FF00527C44 /* jidctflt.c */; };
- 865A232B165187FF00527C44 /* jidctfst.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22FD165187FF00527C44 /* jidctfst.c */; };
- 865A232C165187FF00527C44 /* jidctint.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22FE165187FF00527C44 /* jidctint.c */; };
- 865A232D165187FF00527C44 /* jidctred.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A22FF165187FF00527C44 /* jidctred.c */; };
- 865A232E165187FF00527C44 /* jmemansi.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A2300165187FF00527C44 /* jmemansi.c */; };
- 865A232F165187FF00527C44 /* jmemmgr.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A2301165187FF00527C44 /* jmemmgr.c */; };
- 865A2330165187FF00527C44 /* jquant1.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A2302165187FF00527C44 /* jquant1.c */; };
- 865A2331165187FF00527C44 /* jquant2.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A2303165187FF00527C44 /* jquant2.c */; };
- 865A2332165187FF00527C44 /* jutils.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A2304165187FF00527C44 /* jutils.c */; };
- 865A23421651881300527C44 /* png.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A23331651881300527C44 /* png.c */; };
- 865A23431651881300527C44 /* pngerror.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A23341651881300527C44 /* pngerror.c */; };
- 865A23441651881300527C44 /* pngget.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A23351651881300527C44 /* pngget.c */; };
- 865A23451651881300527C44 /* pngmem.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A23361651881300527C44 /* pngmem.c */; };
- 865A23461651881300527C44 /* pngpread.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A23371651881300527C44 /* pngpread.c */; };
- 865A23471651881300527C44 /* pngread.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A23381651881300527C44 /* pngread.c */; };
- 865A23481651881300527C44 /* pngrio.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A23391651881300527C44 /* pngrio.c */; };
- 865A23491651881300527C44 /* pngrtran.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A233A1651881300527C44 /* pngrtran.c */; };
- 865A234A1651881300527C44 /* pngrutil.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A233B1651881300527C44 /* pngrutil.c */; };
- 865A234B1651881300527C44 /* pngset.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A233C1651881300527C44 /* pngset.c */; };
- 865A234C1651881300527C44 /* pngtrans.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A233D1651881300527C44 /* pngtrans.c */; };
- 865A234D1651881300527C44 /* pngwio.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A233E1651881300527C44 /* pngwio.c */; };
- 865A234E1651881300527C44 /* pngwrite.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A233F1651881300527C44 /* pngwrite.c */; };
- 865A234F1651881300527C44 /* pngwtran.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A23401651881300527C44 /* pngwtran.c */; };
- 865A23501651881300527C44 /* pngwutil.c in Sources */ = {isa = PBXBuildFile; fileRef = 865A23411651881300527C44 /* pngwutil.c */; };
- 865A235A16518AD300527C44 /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 865A235816518AD300527C44 /* AppDelegate.mm */; };
- 865A235B16518AD300527C44 /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = 865A235916518AD300527C44 /* main.mm */; };
- 865BD2F9166FA7F80064F595 /* osxInputManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = 865BD2F8166FA7F80064F595 /* osxInputManager.mm */; };
- 866381D31655484400C8C551 /* mRandom.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80AF16518D4600D96ADF /* mRandom.cc */; };
- 866381D8165550FF00C8C551 /* osxFileIO.mm in Sources */ = {isa = PBXBuildFile; fileRef = 866381D7165550FF00C8C551 /* osxFileIO.mm */; };
- 866381DA1655562400C8C551 /* osxMemory.mm in Sources */ = {isa = PBXBuildFile; fileRef = 866381D91655562400C8C551 /* osxMemory.mm */; };
- 866381DC165556AD00C8C551 /* osxMath.mm in Sources */ = {isa = PBXBuildFile; fileRef = 866381DB165556AD00C8C551 /* osxMath.mm */; };
- 866381E51655615200C8C551 /* osxInput.mm in Sources */ = {isa = PBXBuildFile; fileRef = 866381E41655615200C8C551 /* osxInput.mm */; };
- 866381E91655674B00C8C551 /* osxTime.mm in Sources */ = {isa = PBXBuildFile; fileRef = 866381E81655674B00C8C551 /* osxTime.mm */; };
- 86854E341663AAE6009FAFB2 /* osxOpenGLDevice.mm in Sources */ = {isa = PBXBuildFile; fileRef = 86854E331663AAE6009FAFB2 /* osxOpenGLDevice.mm */; };
- 8694ADC81656B06B0080ABAC /* osxEvents.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8694ADC51656B06B0080ABAC /* osxEvents.mm */; };
- 8694ADC91656B06B0080ABAC /* osxWindow.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8694ADC61656B06B0080ABAC /* osxWindow.mm */; };
- 8694ADD11656B7FC0080ABAC /* osxVideo.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8694ADD01656B7FC0080ABAC /* osxVideo.mm */; };
- 8694ADD51656BDE60080ABAC /* osxGL.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8694ADD41656BDE60080ABAC /* osxGL.mm */; };
- 86C281CD16A4307E00F030F4 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 86C281CB16A4307E00F030F4 /* MainMenu.xib */; };
- 86D76F78165683240046D71F /* osxOutlineGL.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86D76F76165683240046D71F /* osxOutlineGL.cc */; };
- 86D76F791656868D0046D71F /* AnimationAsset.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7E7716518D4600D96ADF /* AnimationAsset.cc */; };
- 86D76F7B1656868D0046D71F /* ImageAsset.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7E7C16518D4600D96ADF /* ImageAsset.cc */; };
- 86D76F7C1656868D0046D71F /* BatchRender.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7E8116518D4600D96ADF /* BatchRender.cc */; };
- 86D76F7D1656868D0046D71F /* CoreMath.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7E8316518D4600D96ADF /* CoreMath.cc */; };
- 86D76F7E1656868D0046D71F /* RenderProxy.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7E8516518D4600D96ADF /* RenderProxy.cc */; };
- 86D76F7F1656868D0046D71F /* SpriteBase.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7E8816518D4600D96ADF /* SpriteBase.cc */; };
- 86D76F801656868D0046D71F /* SpriteBatch.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7E8B16518D4600D96ADF /* SpriteBatch.cc */; };
- 86D76F811656868D0046D71F /* SpriteBatchItem.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7E8D16518D4600D96ADF /* SpriteBatchItem.cc */; };
- 86D76F831656868D0046D71F /* Utility.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7E9116518D4600D96ADF /* Utility.cc */; };
- 86D76F841656868D0046D71F /* Vector2.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7E9316518D4600D96ADF /* Vector2.cc */; };
- 86D76F851656868D0046D71F /* guiImageButtonCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7E9716518D4600D96ADF /* guiImageButtonCtrl.cc */; };
- 86D76F861656868D0046D71F /* guiSceneObjectCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7E9A16518D4600D96ADF /* guiSceneObjectCtrl.cc */; };
- 86D76F871656868D0046D71F /* guiSpriteCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7E9C16518D4600D96ADF /* guiSpriteCtrl.cc */; };
- 86D76F881656868D0046D71F /* SceneWindow.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7E9F16518D4600D96ADF /* SceneWindow.cc */; };
- 86D76F891656868D0046D71F /* ContactFilter.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7EA316518D4600D96ADF /* ContactFilter.cc */; };
- 86D76F8A1656868D0046D71F /* DebugDraw.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7EA516518D4600D96ADF /* DebugDraw.cc */; };
- 86D76F8B1656868D0046D71F /* Scene.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7EA916518D4600D96ADF /* Scene.cc */; };
- 86D76F8C1656868D0046D71F /* WorldQuery.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7EB316518D4600D96ADF /* WorldQuery.cc */; };
- 86D76F8D165686B00046D71F /* SceneRenderFactories.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7EAC16518D4600D96ADF /* SceneRenderFactories.cpp */; };
- 86D76F8E165686B00046D71F /* SceneRenderQueue.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7EAF16518D4600D96ADF /* SceneRenderQueue.cpp */; };
- 86D76F90165686B00046D71F /* CompositeSprite.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7EBB16518D4600D96ADF /* CompositeSprite.cc */; };
- 86D76F93165686B00046D71F /* SceneObject.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7EC316518D4600D96ADF /* SceneObject.cc */; };
- 86D76F96165686B00046D71F /* Scroller.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7ECE16518D4600D96ADF /* Scroller.cc */; };
- 86D76F97165686B00046D71F /* ShapeVector.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7ED116518D4600D96ADF /* ShapeVector.cc */; };
- 86D76F98165686B00046D71F /* Sprite.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7ED416518D4600D96ADF /* Sprite.cc */; };
- 86D76F99165686B00046D71F /* Trigger.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7ED716518D4600D96ADF /* Trigger.cc */; };
- 86D76F9B165686D80046D71F /* hashFunction.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7EE416518D4600D96ADF /* hashFunction.cc */; };
- 86D76F9C165686D80046D71F /* assetFieldTypes.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7EEC16518D4600D96ADF /* assetFieldTypes.cc */; };
- 86D76F9D165686D80046D71F /* assetManager.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7EEE16518D4600D96ADF /* assetManager.cc */; };
- 86D76F9F165686D80046D71F /* assetQuery.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7EF416518D4600D96ADF /* assetQuery.cc */; };
- 86D76FA1165686D80046D71F /* assetTagsManifest.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7EF916518D4600D96ADF /* assetTagsManifest.cc */; };
- 86D76FA2165686D80046D71F /* audio.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7F0116518D4600D96ADF /* audio.cc */; };
- 86D76FA3165686D80046D71F /* AudioAsset.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7F0316518D4600D96ADF /* AudioAsset.cc */; };
- 86D76FA4165686D80046D71F /* audioBuffer.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7F0516518D4600D96ADF /* audioBuffer.cc */; };
- 86D76FA5165686D80046D71F /* audioDataBlock.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7F0716518D4600D96ADF /* audioDataBlock.cc */; };
- 86D76FA7165686D80046D71F /* audioStreamSourceFactory.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7F0B16518D4600D96ADF /* audioStreamSourceFactory.cc */; };
- 86D76FA8165686D80046D71F /* wavStreamSource.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7F0D16518D4600D96ADF /* wavStreamSource.cc */; };
- 86D76FA9165686D80046D71F /* bitTables.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7F1216518D4600D96ADF /* bitTables.cc */; };
- 86D76FAA165686D80046D71F /* hashTable.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7F1716518D4600D96ADF /* hashTable.cc */; };
- 86D76FAB165686D80046D71F /* nameTags.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7F1A16518D4600D96ADF /* nameTags.cpp */; };
- 86D76FAC165686D80046D71F /* undo.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7F1F16518D4600D96ADF /* undo.cc */; };
- 86D76FAD165686D80046D71F /* vector.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7F2116518D4600D96ADF /* vector.cc */; };
- 86D76FAF165687060046D71F /* crc.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7EE116518D4600D96ADF /* crc.cc */; };
- 86D76FB0165687060046D71F /* assetBase.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7EE816518D4600D96ADF /* assetBase.cc */; };
- 86D76FB7165687060046D71F /* behaviorComponent.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7F3616518D4600D96ADF /* behaviorComponent.cpp */; };
- 86D76FB8165687060046D71F /* behaviorInstance.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7F3A16518D4600D96ADF /* behaviorInstance.cpp */; };
- 86D76FB9165687060046D71F /* behaviorTemplate.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7F3D16518D4600D96ADF /* behaviorTemplate.cpp */; };
- 86D76FBA165687060046D71F /* dynamicConsoleMethodComponent.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7F4016518D4600D96ADF /* dynamicConsoleMethodComponent.cpp */; };
- 86D76FBC165687060046D71F /* simComponent.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7F4516518D4600D96ADF /* simComponent.cpp */; };
- 86D76FBD165687060046D71F /* consoleDictionary.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC82B316518DF400D96ADF /* consoleDictionary.cc */; };
- 86D76FBE165687060046D71F /* consoleExprEvalState.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC82B516518DF400D96ADF /* consoleExprEvalState.cc */; };
- 86D76FBF165687060046D71F /* consoleNamespace.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC82B716518DF400D96ADF /* consoleNamespace.cc */; };
- 86D76FC0165687060046D71F /* consoleBaseType.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC82B916518DF400D96ADF /* consoleBaseType.cc */; };
- 86D76FC1165687060046D71F /* ConsoleTypeValidators.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC82BB16518DF400D96ADF /* ConsoleTypeValidators.cc */; };
- 86D76FC2165687060046D71F /* Package.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC82BE16518DF400D96ADF /* Package.cc */; };
- 86D76FC3165687060046D71F /* astAlloc.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC82C016518DF400D96ADF /* astAlloc.cc */; };
- 86D76FC4165687060046D71F /* astNodes.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC82C116518DF400D96ADF /* astNodes.cc */; };
- 86D76FC5165687060046D71F /* cmdgram.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC82C216518DF400D96ADF /* cmdgram.cc */; };
- 86D76FC6165687060046D71F /* CMDscan.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC82C316518DF400D96ADF /* CMDscan.cc */; };
- 86D76FC7165687060046D71F /* codeBlock.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC82C416518DF400D96ADF /* codeBlock.cc */; };
- 86D76FC8165687060046D71F /* compiledEval.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC82C516518DF400D96ADF /* compiledEval.cc */; };
- 86D76FC9165687060046D71F /* compiler.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC82C616518DF400D96ADF /* compiler.cc */; };
- 86D76FCA165687060046D71F /* console.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC82C716518DF400D96ADF /* console.cc */; };
- 86D76FCB165687060046D71F /* consoleDoc.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC82C816518DF400D96ADF /* consoleDoc.cc */; };
- 86D76FCC165687060046D71F /* consoleFunctions.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC82C916518DF400D96ADF /* consoleFunctions.cc */; };
- 86D76FCD165687060046D71F /* consoleLogger.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC82CA16518DF400D96ADF /* consoleLogger.cc */; };
- 86D76FCE165687060046D71F /* consoleObject.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC82CB16518DF400D96ADF /* consoleObject.cc */; };
- 86D76FCF165687060046D71F /* consoleParser.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC82CC16518DF400D96ADF /* consoleParser.cc */; };
- 86D76FD0165687060046D71F /* consoleTypes.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC82CD16518DF400D96ADF /* consoleTypes.cc */; };
- 86D76FD1165687060046D71F /* profiler.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7F7416518D4600D96ADF /* profiler.cc */; };
- 86D76FD2165687060046D71F /* RemoteDebugger1.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7F7716518D4600D96ADF /* RemoteDebugger1.cc */; };
- 86D76FD3165687060046D71F /* RemoteDebuggerBase.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7F7A16518D4600D96ADF /* RemoteDebuggerBase.cc */; };
- 86D76FD4165687060046D71F /* RemoteDebuggerBridge.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7F7D16518D4600D96ADF /* RemoteDebuggerBridge.cc */; };
- 86D76FD5165687060046D71F /* telnetDebugger.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7F8016518D4600D96ADF /* telnetDebugger.cc */; };
- 86D76FD6165687060046D71F /* delegateSignal.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7F8416518D4600D96ADF /* delegateSignal.cpp */; };
- 86D76FE9165687060046D71F /* defaultGame.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7FAF16518D4600D96ADF /* defaultGame.cc */; };
- 86D76FEA165687060046D71F /* gameConnection.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7FB116518D4600D96ADF /* gameConnection.cc */; };
- 86D76FEB165687060046D71F /* gameInterface.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7FB316518D4600D96ADF /* gameInterface.cc */; };
- 86D76FED165687060046D71F /* version.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7FB716518D4600D96ADF /* version.cc */; };
- 86D76FEE165687060046D71F /* bitmapBmp.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7FBA16518D4600D96ADF /* bitmapBmp.cc */; };
- 86D76FEF165687060046D71F /* bitmapJpeg.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7FBB16518D4600D96ADF /* bitmapJpeg.cc */; };
- 86D76FF0165687060046D71F /* bitmapPng.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7FBC16518D4600D96ADF /* bitmapPng.cc */; };
- 86D76FF3165687060046D71F /* dgl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7FC116518D4600D96ADF /* dgl.cc */; };
- 86D76FF4165687060046D71F /* dglMatrix.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7FC316518D4600D96ADF /* dglMatrix.cc */; };
- 86D76FF5165687060046D71F /* DynamicTexture.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7FC416518D4600D96ADF /* DynamicTexture.cc */; };
- 86D76FF6165687060046D71F /* gBitmap.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7FC616518D4600D96ADF /* gBitmap.cc */; };
- 86D76FF7165687060046D71F /* gFont.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7FC816518D4600D96ADF /* gFont.cc */; };
- 86D76FF8165687060046D71F /* gPalette.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7FCA16518D4600D96ADF /* gPalette.cc */; };
- 86D76FF9165687060046D71F /* PNGImage.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7FCC16518D4600D96ADF /* PNGImage.cpp */; };
- 86D76FFA165687060046D71F /* splineUtil.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7FCE16518D4600D96ADF /* splineUtil.cc */; };
- 86D76FFB165687060046D71F /* TextureDictionary.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7FD016518D4600D96ADF /* TextureDictionary.cc */; };
- 86D76FFC165687060046D71F /* TextureHandle.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7FD216518D4600D96ADF /* TextureHandle.cc */; };
- 86D76FFD165687060046D71F /* TextureManager.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7FD416518D4600D96ADF /* TextureManager.cc */; };
- 86D77001165687060046D71F /* guiButtonCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7FDE16518D4600D96ADF /* guiButtonCtrl.cc */; };
- 86D77002165687060046D71F /* guiCheckBoxCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7FE016518D4600D96ADF /* guiCheckBoxCtrl.cc */; };
- 86D77004165687060046D71F /* guiRadioCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7FE416518D4600D96ADF /* guiRadioCtrl.cc */; };
- 86D77008165687060046D71F /* guiDragAndDropCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7FED16518D4600D96ADF /* guiDragAndDropCtrl.cc */; };
- 86D7700E165687060046D71F /* guiScrollCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7FF916518D4600D96ADF /* guiScrollCtrl.cc */; };
- 86D77010165687060046D71F /* guiTabBookCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7FFD16518D4600D96ADF /* guiTabBookCtrl.cc */; };
- 86D77011165687060046D71F /* guiWindowCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC7FFF16518D4600D96ADF /* guiWindowCtrl.cc */; };
- 86D77013165687060046D71F /* guiDebugger.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC800316518D4600D96ADF /* guiDebugger.cc */; };
- 86D77014165687060046D71F /* guiEditCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC800516518D4600D96ADF /* guiEditCtrl.cc */; };
- 86D77016165687060046D71F /* guiGraphCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC800916518D4600D96ADF /* guiGraphCtrl.cc */; };
- 86D77018165687060046D71F /* guiInspector.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC800D16518D4600D96ADF /* guiInspector.cc */; };
- 86D77019165687060046D71F /* guiInspectorTypes.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC800F16518D4600D96ADF /* guiInspectorTypes.cc */; };
- 86D7701C165687060046D71F /* guiArrayCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC801516518D4600D96ADF /* guiArrayCtrl.cc */; };
- 86D77021165687060046D71F /* guiCanvas.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC801E16518D4600D96ADF /* guiCanvas.cc */; };
- 86D77023165687060046D71F /* guiConsole.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC802216518D4600D96ADF /* guiConsole.cc */; };
- 86D77024165687060046D71F /* guiConsoleEditCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC802416518D4600D96ADF /* guiConsoleEditCtrl.cc */; };
- 86D77026165687060046D71F /* guiControl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC802816518D4600D96ADF /* guiControl.cc */; };
- 86D77027165687060046D71F /* guiDefaultControlRender.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC802A16518D4600D96ADF /* guiDefaultControlRender.cc */; };
- 86D77029165687060046D71F /* guiInputCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC802D16518D4600D96ADF /* guiInputCtrl.cc */; };
- 86D7702A165687060046D71F /* guiListBoxCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC802F16518D4600D96ADF /* guiListBoxCtrl.cc */; };
- 86D7702B165687060046D71F /* guiMessageVectorCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC803116518D4600D96ADF /* guiMessageVectorCtrl.cc */; };
- 86D77031165687060046D71F /* guiProgressCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC803D16518D4600D96ADF /* guiProgressCtrl.cc */; };
- 86D77033165687060046D71F /* guiSliderCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC804116518D4600D96ADF /* guiSliderCtrl.cc */; };
- 86D77036165687060046D71F /* guiTextEditCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC804716518D4600D96ADF /* guiTextEditCtrl.cc */; };
- 86D77037165687060046D71F /* guiTextEditSliderCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC804916518D4600D96ADF /* guiTextEditSliderCtrl.cc */; };
- 86D7703A165687060046D71F /* guiTreeViewCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC804F16518D4600D96ADF /* guiTreeViewCtrl.cc */; };
- 86D7703B165687060046D71F /* guiTypes.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC805116518D4600D96ADF /* guiTypes.cc */; };
- 86D7703C165687060046D71F /* lang.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC805416518D4600D96ADF /* lang.cc */; };
- 86D7703D165687060046D71F /* messageVector.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC805616518D4600D96ADF /* messageVector.cc */; };
- 86D7703E165687220046D71F /* actionMap.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC805916518D4600D96ADF /* actionMap.cc */; };
- 86D7703F165687220046D71F /* bitStream.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC805C16518D4600D96ADF /* bitStream.cc */; };
- 86D77040165687220046D71F /* bufferStream.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC805E16518D4600D96ADF /* bufferStream.cc */; };
- 86D77041165687220046D71F /* fileObject.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC806116518D4600D96ADF /* fileObject.cc */; };
- 86D77042165687220046D71F /* fileStream.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC806316518D4600D96ADF /* fileStream.cc */; };
- 86D77043165687220046D71F /* fileStreamObject.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC806516518D4600D96ADF /* fileStreamObject.cc */; };
- 86D77045165687220046D71F /* filterStream.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC806816518D4600D96ADF /* filterStream.cc */; };
- 86D77046165687220046D71F /* memStream.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC806A16518D4600D96ADF /* memStream.cc */; };
- 86D77047165687220046D71F /* nStream.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC806C16518D4600D96ADF /* nStream.cc */; };
- 86D77048165687220046D71F /* resizeStream.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC806D16518D4600D96ADF /* resizeStream.cc */; };
- 86D77049165687220046D71F /* resourceDictionary.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC807016518D4600D96ADF /* resourceDictionary.cc */; };
- 86D7704A165687220046D71F /* resourceManager.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC807116518D4600D96ADF /* resourceManager.cc */; };
- 86D7704B165687220046D71F /* streamObject.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC807416518D4600D96ADF /* streamObject.cc */; };
- 86D7704C165687220046D71F /* centralDir.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC807716518D4600D96ADF /* centralDir.cc */; };
- 86D7704D165687220046D71F /* compressor.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC807916518D4600D96ADF /* compressor.cc */; };
- 86D7704E165687220046D71F /* deflate.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC807B16518D4600D96ADF /* deflate.cc */; };
- 86D7704F165687220046D71F /* extraField.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC807C16518D4600D96ADF /* extraField.cc */; };
- 86D77050165687220046D71F /* fileHeader.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC807E16518D4600D96ADF /* fileHeader.cc */; };
- 86D77051165687220046D71F /* stored.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC808016518D4600D96ADF /* stored.cc */; };
- 86D77052165687220046D71F /* zipArchive.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC808616518D4600D96ADF /* zipArchive.cc */; };
- 86D77053165687220046D71F /* zipCryptStream.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC808816518D4600D96ADF /* zipCryptStream.cc */; };
- 86D77054165687220046D71F /* zipObject.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC808A16518D4600D96ADF /* zipObject.cc */; };
- 86D77055165687220046D71F /* zipSubStream.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC808D16518D4600D96ADF /* zipSubStream.cc */; };
- 86D77056165687220046D71F /* zipTempStream.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC808F16518D4600D96ADF /* zipTempStream.cc */; };
- 86D770571656873C0046D71F /* mathTypes.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC809316518D4600D96ADF /* mathTypes.cc */; };
- 86D770581656873C0046D71F /* mathUtils.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC809516518D4600D96ADF /* mathUtils.cc */; };
- 86D770591656873C0046D71F /* mBox.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC809716518D4600D96ADF /* mBox.cc */; };
- 86D7705B1656873C0046D71F /* mMath_C.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC809D16518D4600D96ADF /* mMath_C.cc */; };
- 86D7705C1656873C0046D71F /* mMathAltivec.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC809E16518D4600D96ADF /* mMathAltivec.cc */; };
- 86D7705D1656873C0046D71F /* mMathFn.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80A116518D4600D96ADF /* mMathFn.cc */; };
- 86D7705E1656873C0046D71F /* mMatrix.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80A516518D4600D96ADF /* mMatrix.cc */; };
- 86D7705F1656873C0046D71F /* mPlaneTransformer.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80A816518D4600D96ADF /* mPlaneTransformer.cc */; };
- 86D770601656873C0046D71F /* mQuadPatch.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80AB16518D4600D96ADF /* mQuadPatch.cc */; };
- 86D770611656873C0046D71F /* mQuat.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80AD16518D4600D96ADF /* mQuat.cc */; };
- 86D770621656873C0046D71F /* mSolver.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80B216518D4600D96ADF /* mSolver.cc */; };
- 86D770631656873C0046D71F /* mSplinePatch.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80B416518D4600D96ADF /* mSplinePatch.cc */; };
- 86D770641656873C0046D71F /* rectClipper.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80B616518D4600D96ADF /* rectClipper.cpp */; };
- 86D770651656873C0046D71F /* dataChunker.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80B916518D4600D96ADF /* dataChunker.cc */; };
- 86D770671656873C0046D71F /* dispatcher.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80C016518D4600D96ADF /* dispatcher.cc */; };
- 86D770681656873C0046D71F /* eventManager.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80C216518D4600D96ADF /* eventManager.cc */; };
- 86D770691656873C0046D71F /* message.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80C416518D4600D96ADF /* message.cc */; };
- 86D7706A1656873C0046D71F /* messageForwarder.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80C616518D4600D96ADF /* messageForwarder.cc */; };
- 86D7706B1656873C0046D71F /* scriptMsgListener.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80C816518D4600D96ADF /* scriptMsgListener.cc */; };
- 86D7706C1656873C0046D71F /* moduleDefinition.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80CC16518D4600D96ADF /* moduleDefinition.cc */; };
- 86D7706D1656873C0046D71F /* moduleManager.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80CF16518D4600D96ADF /* moduleManager.cc */; };
- 86D7706E1656873C0046D71F /* moduleMergeDefinition.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80D216518D4600D96ADF /* moduleMergeDefinition.cc */; };
- 86D7706F1656873C0046D71F /* networkProcessList.cc in Sources */ = {isa = PBXBuildFile; fileRef = 864ECFED165279E100012416 /* networkProcessList.cc */; };
- 86D770701656873C0046D71F /* connectionProtocol.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80D616518D4600D96ADF /* connectionProtocol.cc */; };
- 86D770711656873C0046D71F /* connectionStringTable.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80D816518D4600D96ADF /* connectionStringTable.cc */; };
- 86D770721656873C0046D71F /* httpObject.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80DA16518D4600D96ADF /* httpObject.cc */; };
- 86D770731656873C0046D71F /* netConnection.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80DC16518D4600D96ADF /* netConnection.cc */; };
- 86D770741656873C0046D71F /* netDownload.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80DE16518D4600D96ADF /* netDownload.cc */; };
- 86D770751656873C0046D71F /* netEvent.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80DF16518D4600D96ADF /* netEvent.cc */; };
- 86D770761656873C0046D71F /* netGhost.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80E016518D4600D96ADF /* netGhost.cc */; };
- 86D770771656873C0046D71F /* netInterface.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80E116518D4600D96ADF /* netInterface.cc */; };
- 86D770781656873C0046D71F /* netObject.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80E316518D4600D96ADF /* netObject.cc */; };
- 86D770791656873C0046D71F /* netStringTable.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80E516518D4600D96ADF /* netStringTable.cc */; };
- 86D7707A1656873C0046D71F /* netTest.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80E716518D4600D96ADF /* netTest.cc */; };
- 86D7707B1656873C0046D71F /* RemoteCommandEvent.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80E816518D4600D96ADF /* RemoteCommandEvent.cc */; };
- 86D7707C1656873C0046D71F /* serverQuery.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80E916518D4600D96ADF /* serverQuery.cc */; };
- 86D7707D1656873C0046D71F /* tcpObject.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80EB16518D4600D96ADF /* tcpObject.cc */; };
- 86D7707E1656873C0046D71F /* telnetConsole.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80ED16518D4600D96ADF /* telnetConsole.cc */; };
- 86D7707F1656873C0046D71F /* SimXMLDocument.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80F016518D4600D96ADF /* SimXMLDocument.cpp */; };
- 86D770801656873C0046D71F /* taml.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80F316518D4600D96ADF /* taml.cc */; };
- 86D770841656873C0046D71F /* tamlWriteNode.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC80FD16518D4600D96ADF /* tamlWriteNode.cc */; };
- 86D770881656873C0046D71F /* tinystr.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 86BC810716518D4600D96ADF /* tinystr.cpp */; };
- 86D770891656873C0046D71F /* tinyxml.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 86BC810916518D4600D96ADF /* tinyxml.cpp */; };
- 86D7708A1656873C0046D71F /* tinyxmlerror.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 86BC810B16518D4600D96ADF /* tinyxmlerror.cpp */; };
- 86D7708B1656873C0046D71F /* tinyxmlparser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 86BC810C16518D4600D96ADF /* tinyxmlparser.cpp */; };
- 86D7708D1656873C0046D71F /* CursorManager.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC834C16518FE800D96ADF /* CursorManager.cc */; };
- 86D7708E1656873C0046D71F /* platform.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC834D16518FE800D96ADF /* platform.cc */; };
- 86D7708F1656873C0046D71F /* platformAssert.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC834E16518FE800D96ADF /* platformAssert.cc */; };
- 86D770901656873C0046D71F /* platformCPU.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC834F16518FE800D96ADF /* platformCPU.cc */; };
- 86D770911656873C0046D71F /* platformFileIO.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC835016518FE800D96ADF /* platformFileIO.cc */; };
- 86D770921656873C0046D71F /* platformFont.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC834416518FE800D96ADF /* platformFont.cc */; };
- 86D770931656873C0046D71F /* platformMemory.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC835116518FE800D96ADF /* platformMemory.cc */; };
- 86D770951656873C0046D71F /* platformString.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC835316518FE800D96ADF /* platformString.cc */; };
- 86D770961656873C0046D71F /* platformVideo.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC835416518FE800D96ADF /* platformVideo.cc */; };
- 86D770971656873C0046D71F /* Tickable.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC834A16518FE800D96ADF /* Tickable.cc */; };
- 86D770981656873C0046D71F /* popupMenu.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC833816518FB100D96ADF /* popupMenu.cc */; };
- 86D770991656873C0046D71F /* msgBox.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 86BC833B16518FBC00D96ADF /* msgBox.cpp */; };
- 86D770AA1656873C0046D71F /* scriptGroup.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC812D16518D4600D96ADF /* scriptGroup.cc */; };
- 86D770AB1656873C0046D71F /* scriptObject.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC812F16518D4600D96ADF /* scriptObject.cc */; };
- 86D770AC1656873C0046D71F /* simBase.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC813116518D4600D96ADF /* simBase.cc */; };
- 86D770AD1656873C0046D71F /* simConsoleEvent.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC813316518D4600D96ADF /* simConsoleEvent.cc */; };
- 86D770AE1656873C0046D71F /* simConsoleThreadExecEvent.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC813516518D4600D96ADF /* simConsoleThreadExecEvent.cc */; };
- 86D770AF1656873C0046D71F /* simDatablock.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC813716518D4600D96ADF /* simDatablock.cc */; };
- 86D770B01656873C0046D71F /* simDictionary.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC813A16518D4600D96ADF /* simDictionary.cc */; };
- 86D770B11656873C0046D71F /* simFieldDictionary.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC813D16518D4600D96ADF /* simFieldDictionary.cc */; };
- 86D770B21656873C0046D71F /* simManager.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC813F16518D4600D96ADF /* simManager.cc */; };
- 86D770B31656873C0046D71F /* simObject.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC814016518D4600D96ADF /* simObject.cc */; };
- 86D770B41656873C0046D71F /* SimObjectList.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC814216518D4600D96ADF /* SimObjectList.cc */; };
- 86D770B51656873C0046D71F /* simSerialize.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 86BC814516518D4600D96ADF /* simSerialize.cpp */; };
- 86D770B61656873C0046D71F /* simSet.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC814616518D4600D96ADF /* simSet.cc */; };
- 86D770B71656873C0046D71F /* findMatch.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC814916518D4600D96ADF /* findMatch.cc */; };
- 86D770B81656873C0046D71F /* stringBuffer.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC814B16518D4600D96ADF /* stringBuffer.cc */; };
- 86D770B91656873C0046D71F /* stringStack.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC814D16518D4600D96ADF /* stringStack.cc */; };
- 86D770BA1656873C0046D71F /* stringTable.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC814F16518D4600D96ADF /* stringTable.cc */; };
- 86D770BB1656873C0046D71F /* stringUnit.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 86BC815116518D4600D96ADF /* stringUnit.cpp */; };
- 86D770BC1656873C0046D71F /* unicode.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86BC815316518D4600D96ADF /* unicode.cc */; };
- 86D770C3165687450046D71F /* osxFileDialogs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8609FE3216556F22004662ED /* osxFileDialogs.mm */; };
- 86DE5688171F05F60054CB83 /* guiGridCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 86DE5686171F05F60054CB83 /* guiGridCtrl.cc */; };
- 86EA5B401678C7C700598E68 /* osxCocoaUtilities.mm in Sources */ = {isa = PBXBuildFile; fileRef = 86EA5B3F1678C7C700598E68 /* osxCocoaUtilities.mm */; };
- 86EC5AC7165C1E0100757872 /* osxTorqueView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 86EC5AC6165C1E0100757872 /* osxTorqueView.mm */; };
- B350D12F174ED1FE00033EBB /* math_ScriptBinding.cc in Sources */ = {isa = PBXBuildFile; fileRef = B350D12C174ED1FE00033EBB /* math_ScriptBinding.cc */; };
- B350D131174ED23E00033EBB /* frameAllocator_ScriptBinding.cc in Sources */ = {isa = PBXBuildFile; fileRef = B350D130174ED23E00033EBB /* frameAllocator_ScriptBinding.cc */; };
- B350D158174EF62400033EBB /* fileSystem_ScriptBinding.cc in Sources */ = {isa = PBXBuildFile; fileRef = B350D156174EF62400033EBB /* fileSystem_ScriptBinding.cc */; };
- B350D164174EF71B00033EBB /* metaScripting_ScriptBinding.cc in Sources */ = {isa = PBXBuildFile; fileRef = B350D161174EF71B00033EBB /* metaScripting_ScriptBinding.cc */; };
- B350D172174EF91900033EBB /* audio_ScriptBinding.cc in Sources */ = {isa = PBXBuildFile; fileRef = B350D171174EF91900033EBB /* audio_ScriptBinding.cc */; };
- D000F9731CB0CF4800C4D097 /* audioDescriptions.cc in Sources */ = {isa = PBXBuildFile; fileRef = D000F9711CB0CF4800C4D097 /* audioDescriptions.cc */; };
- D000F97B1CB0D16A00C4D097 /* BitmapFont.cc in Sources */ = {isa = PBXBuildFile; fileRef = D000F9751CB0D16A00C4D097 /* BitmapFont.cc */; };
- D000F97C1CB0D16A00C4D097 /* BitmapFontCharacter.cc in Sources */ = {isa = PBXBuildFile; fileRef = D000F9771CB0D16A00C4D097 /* BitmapFontCharacter.cc */; };
- D000F9801CB0D1B300C4D097 /* FontAsset.cc in Sources */ = {isa = PBXBuildFile; fileRef = D000F97E1CB0D1B300C4D097 /* FontAsset.cc */; };
- D000F9841CB0D25A00C4D097 /* TextSprite.cc in Sources */ = {isa = PBXBuildFile; fileRef = D000F9821CB0D25A00C4D097 /* TextSprite.cc */; };
- D078022D2AFED59E00EAA843 /* guiColorPopupCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = D07802272AFED59E00EAA843 /* guiColorPopupCtrl.cc */; };
- D078022E2AFED59E00EAA843 /* guiColorPickerCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = D078022C2AFED59E00EAA843 /* guiColorPickerCtrl.cc */; };
- D07802322AFED5FF00EAA843 /* guiFrameSetCtrl.cc in Sources */ = {isa = PBXBuildFile; fileRef = D07802312AFED5FF00EAA843 /* guiFrameSetCtrl.cc */; };
- D07802372B02C32800EAA843 /* pcg_basic.c in Sources */ = {isa = PBXBuildFile; fileRef = D07802362B02C32800EAA843 /* pcg_basic.c */; };
- D078023B2B02C34C00EAA843 /* Perlin.cc in Sources */ = {isa = PBXBuildFile; fileRef = D07802392B02C34C00EAA843 /* Perlin.cc */; };
- D07802422B02C39500EAA843 /* RandomNumberGenerator.cc in Sources */ = {isa = PBXBuildFile; fileRef = D078023D2B02C39500EAA843 /* RandomNumberGenerator.cc */; };
- D07802432B02C39500EAA843 /* NoiseGenerator.cc in Sources */ = {isa = PBXBuildFile; fileRef = D078023E2B02C39500EAA843 /* NoiseGenerator.cc */; };
- D0D55C571EAAA5A500B2C750 /* AUTHORS in Resources */ = {isa = PBXBuildFile; fileRef = D0D55C4B1EAAA5A500B2C750 /* AUTHORS */; };
- D0D55C581EAAA5A500B2C750 /* CHANGES in Resources */ = {isa = PBXBuildFile; fileRef = D0D55C4C1EAAA5A500B2C750 /* CHANGES */; };
- D0D55C591EAAA5A500B2C750 /* COPYING in Resources */ = {isa = PBXBuildFile; fileRef = D0D55C4D1EAAA5A500B2C750 /* COPYING */; };
- D0D55C5A1EAAA5A500B2C750 /* README in Resources */ = {isa = PBXBuildFile; fileRef = D0D55C531EAAA5A500B2C750 /* README */; };
- D0D55C5B1EAAA5A500B2C750 /* bitwise.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D55C551EAAA5A500B2C750 /* bitwise.c */; };
- D0D55C5C1EAAA5A500B2C750 /* framing.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D55C561EAAA5A500B2C750 /* framing.c */; };
- D0D55CAC1EAAA5BB00B2C750 /* analysis.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D55C5E1EAAA5BB00B2C750 /* analysis.c */; };
- D0D55CAE1EAAA5BB00B2C750 /* bitrate.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D55C611EAAA5BB00B2C750 /* bitrate.c */; };
- D0D55CAF1EAAA5BB00B2C750 /* block.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D55C631EAAA5BB00B2C750 /* block.c */; };
- D0D55CB01EAAA5BB00B2C750 /* codebook.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D55C641EAAA5BB00B2C750 /* codebook.c */; };
- D0D55CB11EAAA5BB00B2C750 /* envelope.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D55C671EAAA5BB00B2C750 /* envelope.c */; };
- D0D55CB21EAAA5BB00B2C750 /* floor0.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D55C691EAAA5BB00B2C750 /* floor0.c */; };
- D0D55CB31EAAA5BB00B2C750 /* floor1.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D55C6A1EAAA5BB00B2C750 /* floor1.c */; };
- D0D55CB41EAAA5BB00B2C750 /* info.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D55C711EAAA5BB00B2C750 /* info.c */; };
- D0D55CB51EAAA5BB00B2C750 /* lookup.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D55C8F1EAAA5BB00B2C750 /* lookup.c */; };
- D0D55CB61EAAA5BB00B2C750 /* lookups.pl in Resources */ = {isa = PBXBuildFile; fileRef = D0D55C921EAAA5BB00B2C750 /* lookups.pl */; };
- D0D55CB71EAAA5BB00B2C750 /* lpc.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D55C931EAAA5BB00B2C750 /* lpc.c */; };
- D0D55CB81EAAA5BB00B2C750 /* lsp.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D55C951EAAA5BB00B2C750 /* lsp.c */; };
- D0D55CB91EAAA5BB00B2C750 /* mapping0.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D55C971EAAA5BB00B2C750 /* mapping0.c */; };
- D0D55CBA1EAAA5BB00B2C750 /* mdct.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D55C991EAAA5BB00B2C750 /* mdct.c */; };
- D0D55CBB1EAAA5BB00B2C750 /* psy.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D55C9D1EAAA5BB00B2C750 /* psy.c */; };
- D0D55CBC1EAAA5BB00B2C750 /* registry.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D55C9F1EAAA5BB00B2C750 /* registry.c */; };
- D0D55CBD1EAAA5BB00B2C750 /* res0.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D55CA11EAAA5BB00B2C750 /* res0.c */; };
- D0D55CBE1EAAA5BB00B2C750 /* sharedbook.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D55CA31EAAA5BB00B2C750 /* sharedbook.c */; };
- D0D55CBF1EAAA5BB00B2C750 /* smallft.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D55CA41EAAA5BB00B2C750 /* smallft.c */; };
- D0D55CC01EAAA5BB00B2C750 /* synthesis.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D55CA61EAAA5BB00B2C750 /* synthesis.c */; };
- D0D55CC21EAAA5BB00B2C750 /* vorbisenc.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D55CA81EAAA5BB00B2C750 /* vorbisenc.c */; };
- D0D55CC31EAAA5BB00B2C750 /* vorbisfile.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D55CA91EAAA5BB00B2C750 /* vorbisfile.c */; };
- D0D55CC41EAAA5BB00B2C750 /* window.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D55CAA1EAAA5BB00B2C750 /* window.c */; };
-/* End PBXBuildFile section */
-
-/* Begin PBXFileReference section */
- 06D168681C1F949D009A1AD1 /* vorbisStreamSource.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = vorbisStreamSource.cc; sourceTree = ""; };
- 06D168691C1F949D009A1AD1 /* vorbisStreamSource.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = vorbisStreamSource.h; sourceTree = ""; };
- 07738F0227EA9E08009B4B15 /* mFluid.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = mFluid.h; sourceTree = ""; };
- 07738F0327EA9E08009B4B15 /* mFluid.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = mFluid.cpp; sourceTree = ""; };
- 0787E03127EBC867001EAA71 /* inffixed.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = inffixed.h; path = ../../lib/zlib/inffixed.h; sourceTree = ""; };
- 0787E03227EBC867001EAA71 /* trees.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = trees.c; path = ../../lib/zlib/trees.c; sourceTree = ""; };
- 0787E03327EBC867001EAA71 /* inftrees.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = inftrees.c; path = ../../lib/zlib/inftrees.c; sourceTree = ""; };
- 0787E03427EBC867001EAA71 /* inflate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = inflate.h; path = ../../lib/zlib/inflate.h; sourceTree = ""; };
- 0787E03527EBC867001EAA71 /* zutil.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = zutil.c; path = ../../lib/zlib/zutil.c; sourceTree = ""; };
- 0787E03627EBC867001EAA71 /* compress.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = compress.c; path = ../../lib/zlib/compress.c; sourceTree = ""; };
- 0787E03727EBC867001EAA71 /* inflate.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = inflate.c; path = ../../lib/zlib/inflate.c; sourceTree = ""; };
- 0787E03827EBC867001EAA71 /* infback.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = infback.c; path = ../../lib/zlib/infback.c; sourceTree = ""; };
- 0787E03927EBC867001EAA71 /* zutil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = zutil.h; path = ../../lib/zlib/zutil.h; sourceTree = ""; };
- 0787E03A27EBC867001EAA71 /* deflate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = deflate.h; path = ../../lib/zlib/deflate.h; sourceTree = ""; };
- 0787E03B27EBC868001EAA71 /* gzclose.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = gzclose.c; path = ../../lib/zlib/gzclose.c; sourceTree = ""; };
- 0787E03C27EBC868001EAA71 /* gzread.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = gzread.c; path = ../../lib/zlib/gzread.c; sourceTree = ""; };
- 0787E03D27EBC868001EAA71 /* deflate.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = deflate.c; path = ../../lib/zlib/deflate.c; sourceTree = ""; };
- 0787E03E27EBC868001EAA71 /* crc32.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = crc32.h; path = ../../lib/zlib/crc32.h; sourceTree = ""; };
- 0787E03F27EBC868001EAA71 /* inffast.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = inffast.h; path = ../../lib/zlib/inffast.h; sourceTree = ""; };
- 0787E04027EBC868001EAA71 /* zlib.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = zlib.h; path = ../../lib/zlib/zlib.h; sourceTree = ""; };
- 0787E04127EBC868001EAA71 /* inftrees.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = inftrees.h; path = ../../lib/zlib/inftrees.h; sourceTree = ""; };
- 0787E04227EBC868001EAA71 /* adler32.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = adler32.c; path = ../../lib/zlib/adler32.c; sourceTree = ""; };
- 0787E04327EBC868001EAA71 /* trees.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = trees.h; path = ../../lib/zlib/trees.h; sourceTree = ""; };
- 0787E04427EBC868001EAA71 /* gzguts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = gzguts.h; path = ../../lib/zlib/gzguts.h; sourceTree = ""; };
- 0787E04527EBC869001EAA71 /* gzwrite.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = gzwrite.c; path = ../../lib/zlib/gzwrite.c; sourceTree = ""; };
- 0787E04627EBC869001EAA71 /* inffast.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = inffast.c; path = ../../lib/zlib/inffast.c; sourceTree = ""; };
- 0787E04727EBC869001EAA71 /* uncompr.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = uncompr.c; path = ../../lib/zlib/uncompr.c; sourceTree = ""; };
- 0787E04827EBC869001EAA71 /* crc32.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = crc32.c; path = ../../lib/zlib/crc32.c; sourceTree = ""; };
- 0787E04927EBC869001EAA71 /* gzlib.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = gzlib.c; path = ../../lib/zlib/gzlib.c; sourceTree = ""; };
- 0787E04A27EBC869001EAA71 /* zconf.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = zconf.h; path = ../../lib/zlib/zconf.h; sourceTree = ""; };
- 07F98823274F1B0B009ECC0D /* guiMenuBarCtrl_ScriptBinding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = guiMenuBarCtrl_ScriptBinding.h; sourceTree = ""; };
- 07F98824274F1B0B009ECC0D /* guiParticleGraphInspector.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = guiParticleGraphInspector.h; sourceTree = ""; };
- 07F98825274F1B0B009ECC0D /* guiMenuBarCtrl.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = guiMenuBarCtrl.cc; sourceTree = ""; };
- 07F98826274F1B0B009ECC0D /* guiMenuBarCtrl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = guiMenuBarCtrl.h; sourceTree = ""; };
- 07F98827274F1B0B009ECC0D /* guiParticleGraphInspector_ScriptBinding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = guiParticleGraphInspector_ScriptBinding.h; sourceTree = ""; };
- 07F98828274F1B0B009ECC0D /* guiParticleGraphInspector.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = guiParticleGraphInspector.cc; sourceTree = ""; };
- 07F9882B274F1C20009ECC0D /* guiSceneScrollCtrl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = guiSceneScrollCtrl.h; sourceTree = ""; };
- 07F9882C274F1C20009ECC0D /* guiPanelCtrl.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = guiPanelCtrl.cc; sourceTree = ""; };
- 07F9882D274F1C20009ECC0D /* guiExpandCtrl.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = guiExpandCtrl.cc; sourceTree = ""; };
- 07F9882E274F1C20009ECC0D /* guiPanelCtrl_ScriptBinding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = guiPanelCtrl_ScriptBinding.h; sourceTree = ""; };
- 07F9882F274F1C20009ECC0D /* guiGridCtrl_ScriptBinding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = guiGridCtrl_ScriptBinding.h; sourceTree = ""; };
- 07F98830274F1C20009ECC0D /* guiPanelCtrl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = guiPanelCtrl.h; sourceTree = ""; };
- 07F98831274F1C20009ECC0D /* guiTabPageCtrl.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = guiTabPageCtrl.cc; sourceTree = ""; };
- 07F98832274F1C21009ECC0D /* guiExpandCtrl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = guiExpandCtrl.h; sourceTree = ""; };
- 07F98833274F1C21009ECC0D /* guiDragAndDropCtrl_ScriptBinding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = guiDragAndDropCtrl_ScriptBinding.h; sourceTree = ""; };
- 07F98834274F1C21009ECC0D /* guiExpandCtrl_ScriptBinding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = guiExpandCtrl_ScriptBinding.h; sourceTree = ""; };
- 07F98835274F1C21009ECC0D /* guiChainCtrl.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = guiChainCtrl.cc; sourceTree = ""; };
- 07F98836274F1C21009ECC0D /* guiTabPageCtrl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = guiTabPageCtrl.h; sourceTree = ""; };
- 07F98837274F1C21009ECC0D /* guiSceneScrollCtrl.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = guiSceneScrollCtrl.cc; sourceTree = ""; };
- 07F98838274F1C21009ECC0D /* guiChainCtrl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = guiChainCtrl.h; sourceTree = ""; };
- 07F98839274F1C21009ECC0D /* guiChainCtrl_ScriptBinding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = guiChainCtrl_ScriptBinding.h; sourceTree = ""; };
- 07F9883A274F1C21009ECC0D /* guiScrollCtrl_ScriptBinding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = guiScrollCtrl_ScriptBinding.h; sourceTree = ""; };
- 07F9883B274F1C21009ECC0D /* guiWindowCtrl_ScriptBinding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = guiWindowCtrl_ScriptBinding.h; sourceTree = ""; };
- 07F9883C274F1C21009ECC0D /* guiTabBookCtrl_ScriptBinding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = guiTabBookCtrl_ScriptBinding.h; sourceTree = ""; };
- 07F98842274F1C7E009ECC0D /* guiCheckBoxCtrl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = guiCheckBoxCtrl.h; sourceTree = ""; };
- 07F98843274F1C7E009ECC0D /* guiButtonCtrl_ScriptBinding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = guiButtonCtrl_ScriptBinding.h; sourceTree = ""; };
- 07F98844274F1C7F009ECC0D /* guiDropDownCtrl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = guiDropDownCtrl.h; sourceTree = ""; };
- 07F98845274F1C7F009ECC0D /* guiDropDownCtrl_ScriptBinding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = guiDropDownCtrl_ScriptBinding.h; sourceTree = ""; };
- 07F98846274F1C7F009ECC0D /* guiDropDownCtrl.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = guiDropDownCtrl.cc; sourceTree = ""; };
- 07F98848274F1CCF009ECC0D /* guiListBoxCtrl_ScriptBinding.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = guiListBoxCtrl_ScriptBinding.h; sourceTree = ""; };
- 07F98849274F1D07009ECC0D /* guiProgressCtrl_ScriptBinding.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = guiProgressCtrl_ScriptBinding.h; sourceTree = ""; };
- 07F9884A274F1D21009ECC0D /* guiTextEditCtrl_ScriptBinding.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = guiTextEditCtrl_ScriptBinding.h; sourceTree = ""; };
- 07F9884B274F2596009ECC0D /* gColor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gColor.h; sourceTree = ""; };
- 07F9884C274F2596009ECC0D /* gColor.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = gColor.cc; sourceTree = ""; };
- 07F9884D274F2596009ECC0D /* gColor_ScriptBinding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gColor_ScriptBinding.h; sourceTree = ""; };
- 2797C9E117F4E12500625B51 /* eaxtypes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = eaxtypes.h; sourceTree = ""; };
- 2A03300B165D1D2100E9CD70 /* unitTesting.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = unitTesting.cc; path = ../../../source/testing/unitTesting.cc; sourceTree = ""; };
- 2A03300C165D1D2100E9CD70 /* unitTesting.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = unitTesting.h; path = ../../../source/testing/unitTesting.h; sourceTree = ""; };
- 2A033010165D1D4100E9CD70 /* platformFileIoTests.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = platformFileIoTests.cc; path = ../../../source/testing/tests/platformFileIoTests.cc; sourceTree = ""; };
- 2A0A68DF166E268E0093AD41 /* osxFont.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = osxFont.h; sourceTree = ""; };
- 2A25738D16A48DAC00363C6F /* ParticlePlayer_ScriptBinding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ParticlePlayer_ScriptBinding.h; sourceTree = ""; };
- 2A25738E16A48DAC00363C6F /* ParticlePlayer.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ParticlePlayer.cc; sourceTree = ""; };
- 2A25738F16A48DAC00363C6F /* ParticlePlayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ParticlePlayer.h; sourceTree = ""; };
- 2A6F78CC16A4528C005C76D9 /* ParticleAssetEmitter.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ParticleAssetEmitter.cc; sourceTree = ""; };
- 2A6F78CD16A4528C005C76D9 /* ParticleAssetEmitter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ParticleAssetEmitter.h; sourceTree = ""; };
- 2AA3655516F3552200E7A900 /* ImageFrameProvider.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ImageFrameProvider.cc; sourceTree = ""; };
- 2AA3655616F3552200E7A900 /* ImageFrameProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ImageFrameProvider.h; sourceTree = ""; };
- 2AA3655716F3552200E7A900 /* ImageFrameProviderCore.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ImageFrameProviderCore.cc; sourceTree = ""; };
- 2AA3655816F3552200E7A900 /* ImageFrameProviderCore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ImageFrameProviderCore.h; sourceTree = ""; };
- 2AA6865A16D69943003CEF0A /* SceneObjectList.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SceneObjectList.cc; sourceTree = ""; };
- 2AA6865B16D69943003CEF0A /* SceneObjectList.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SceneObjectList.h; sourceTree = ""; };
- 2AA6865C16D69943003CEF0A /* SceneObjectSet_ScriptBinding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SceneObjectSet_ScriptBinding.h; sourceTree = ""; };
- 2AA6865D16D69943003CEF0A /* SceneObjectSet.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SceneObjectSet.cc; sourceTree = ""; };
- 2AA6865E16D69943003CEF0A /* SceneObjectSet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SceneObjectSet.h; sourceTree = ""; };
- 2AB14A0216D7CDC200EABBF2 /* PointForceController_ScriptBinding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PointForceController_ScriptBinding.h; path = controllers/PointForceController_ScriptBinding.h; sourceTree = ""; };
- 2AB14A0316D7CDC200EABBF2 /* PointForceController.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = PointForceController.cc; path = controllers/PointForceController.cc; sourceTree = ""; };
- 2AB14A0416D7CDC300EABBF2 /* PointForceController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PointForceController.h; path = controllers/PointForceController.h; sourceTree = ""; };
- 2AB4A5221705A84D0043CBAA /* tamlParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = tamlParser.h; sourceTree = ""; };
- 2AB4A5231705A84D0043CBAA /* tamlVisitor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = tamlVisitor.h; sourceTree = ""; };
- 2AB4C19716DE9F0600B02479 /* GroupedSceneController_ScriptBinding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = GroupedSceneController_ScriptBinding.h; path = controllers/core/GroupedSceneController_ScriptBinding.h; sourceTree = ""; };
- 2AB4C19816DE9F0600B02479 /* GroupedSceneController.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = GroupedSceneController.cc; path = controllers/core/GroupedSceneController.cc; sourceTree = ""; };
- 2AB4C19916DE9F0600B02479 /* GroupedSceneController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = GroupedSceneController.h; path = controllers/core/GroupedSceneController.h; sourceTree = ""; };
- 2AB4C19A16DE9F0600B02479 /* PickingSceneController_ScriptBinding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PickingSceneController_ScriptBinding.h; path = controllers/core/PickingSceneController_ScriptBinding.h; sourceTree = ""; };
- 2AB4C19B16DE9F0600B02479 /* PickingSceneController.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = PickingSceneController.cc; path = controllers/core/PickingSceneController.cc; sourceTree = ""; };
- 2AB4C19C16DE9F0600B02479 /* PickingSceneController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PickingSceneController.h; path = controllers/core/PickingSceneController.h; sourceTree = ""; };
- 2AB4C19D16DE9F0600B02479 /* SceneController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SceneController.h; path = controllers/core/SceneController.h; sourceTree = ""; };
- 2AB4C1A016DE9F1100B02479 /* AmbientForceController_ScriptBinding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AmbientForceController_ScriptBinding.h; path = controllers/AmbientForceController_ScriptBinding.h; sourceTree = ""; };
- 2AB4C1A116DE9F1100B02479 /* AmbientForceController.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = AmbientForceController.cc; path = controllers/AmbientForceController.cc; sourceTree = ""; };
- 2AB4C1A216DE9F1100B02479 /* AmbientForceController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AmbientForceController.h; path = controllers/AmbientForceController.h; sourceTree = ""; };
- 2AB97A1B16B66BC70080F940 /* tamlCustom.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = tamlCustom.cc; sourceTree = ""; };
- 2AB97A1C16B66BC70080F940 /* tamlCustom.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = tamlCustom.h; sourceTree = ""; };
- 2ABF5C8E16569A0C00BBBF1D /* osxMutex.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = osxMutex.mm; sourceTree = ""; };
- 2AC5C7E71667C85700A0D046 /* platformStringTests.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = platformStringTests.cc; path = ../../../source/testing/tests/platformStringTests.cc; sourceTree = ""; };
- 2ACAFD481705CF4A0022601C /* tamlJSONParser.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = tamlJSONParser.cc; path = json/tamlJSONParser.cc; sourceTree = ""; };
- 2ACAFD491705CF4A0022601C /* tamlJSONParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = tamlJSONParser.h; path = json/tamlJSONParser.h; sourceTree = ""; };
- 2ACF5A2516E52D4B00F838D9 /* SpriteBatchQuery.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SpriteBatchQuery.cc; sourceTree = ""; };
- 2ACF5A2616E52D4B00F838D9 /* SpriteBatchQuery.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SpriteBatchQuery.h; sourceTree = ""; };
- 2ACF5A2716E52D4B00F838D9 /* SpriteBatchQueryResult.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SpriteBatchQueryResult.h; sourceTree = ""; };
- 2ACFC0A7166CE1AB00FE7370 /* platformMemoryTests.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = platformMemoryTests.cc; path = ../../../source/testing/tests/platformMemoryTests.cc; sourceTree = ""; };
- 2AD07B2616D15F5A0070DC79 /* simObjectTimerEvent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = simObjectTimerEvent.h; sourceTree = ""; };
- 2AD35A541663608E00C75F30 /* platformFileIO.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = platformFileIO.h; sourceTree = ""; };
- 2AD42126170433B3005BB8AD /* allocators.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = allocators.h; path = rapidjson/include/rapidjson/allocators.h; sourceTree = ""; };
- 2AD42127170433B3005BB8AD /* document.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = document.h; path = rapidjson/include/rapidjson/document.h; sourceTree = ""; };
- 2AD42128170433B3005BB8AD /* encodedstream.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = encodedstream.h; path = rapidjson/include/rapidjson/encodedstream.h; sourceTree = ""; };
- 2AD42129170433B3005BB8AD /* encodings.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = encodings.h; path = rapidjson/include/rapidjson/encodings.h; sourceTree = ""; };
- 2AD4212A170433B3005BB8AD /* filereadstream.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = filereadstream.h; path = rapidjson/include/rapidjson/filereadstream.h; sourceTree = ""; };
- 2AD4212B170433B3005BB8AD /* filestream.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = filestream.h; path = rapidjson/include/rapidjson/filestream.h; sourceTree = ""; };
- 2AD4212C170433B3005BB8AD /* filewritestream.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = filewritestream.h; path = rapidjson/include/rapidjson/filewritestream.h; sourceTree = ""; };
- 2AD4212D170433B3005BB8AD /* prettywriter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = prettywriter.h; path = rapidjson/include/rapidjson/prettywriter.h; sourceTree = ""; };
- 2AD4212E170433B3005BB8AD /* rapidjson.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = rapidjson.h; path = rapidjson/include/rapidjson/rapidjson.h; sourceTree = ""; };
- 2AD4212F170433B3005BB8AD /* reader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = reader.h; path = rapidjson/include/rapidjson/reader.h; sourceTree = ""; };
- 2AD42130170433B3005BB8AD /* stringbuffer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = stringbuffer.h; path = rapidjson/include/rapidjson/stringbuffer.h; sourceTree = ""; };
- 2AD42131170433B3005BB8AD /* writer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = writer.h; path = rapidjson/include/rapidjson/writer.h; sourceTree = ""; };
- 2AD42133170433C7005BB8AD /* pow10.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = pow10.h; path = rapidjson/include/rapidjson/internal/pow10.h; sourceTree = ""; };
- 2AD42134170433C7005BB8AD /* stack.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = stack.h; path = rapidjson/include/rapidjson/internal/stack.h; sourceTree = ""; };
- 2AD42135170433C7005BB8AD /* strfunc.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = strfunc.h; path = rapidjson/include/rapidjson/internal/strfunc.h; sourceTree = ""; };
- 2AD42139170433FE005BB8AD /* tamlXmlParser.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = tamlXmlParser.cc; path = xml/tamlXmlParser.cc; sourceTree = ""; };
- 2AD4213A170433FE005BB8AD /* tamlXmlParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = tamlXmlParser.h; path = xml/tamlXmlParser.h; sourceTree = ""; };
- 2AD4213B170433FE005BB8AD /* tamlXmlReader.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = tamlXmlReader.cc; path = xml/tamlXmlReader.cc; sourceTree = ""; };
- 2AD4213C170433FE005BB8AD /* tamlXmlReader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = tamlXmlReader.h; path = xml/tamlXmlReader.h; sourceTree = ""; };
- 2AD4213E170433FE005BB8AD /* tamlXmlWriter.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = tamlXmlWriter.cc; path = xml/tamlXmlWriter.cc; sourceTree = ""; };
- 2AD4213F170433FE005BB8AD /* tamlXmlWriter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = tamlXmlWriter.h; path = xml/tamlXmlWriter.h; sourceTree = ""; };
- 2AD4214317043408005BB8AD /* tamlJSONReader.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = tamlJSONReader.cc; path = json/tamlJSONReader.cc; sourceTree = ""; };
- 2AD4214417043408005BB8AD /* tamlJSONReader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = tamlJSONReader.h; path = json/tamlJSONReader.h; sourceTree = ""; };
- 2AD4214517043408005BB8AD /* tamlJSONWriter.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = tamlJSONWriter.cc; path = json/tamlJSONWriter.cc; sourceTree = ""; };
- 2AD4214617043408005BB8AD /* tamlJSONWriter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = tamlJSONWriter.h; path = json/tamlJSONWriter.h; sourceTree = ""; };
- 2AD4214917043413005BB8AD /* tamlBinaryReader.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = tamlBinaryReader.cc; path = binary/tamlBinaryReader.cc; sourceTree = ""; };
- 2AD4214A17043413005BB8AD /* tamlBinaryReader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = tamlBinaryReader.h; path = binary/tamlBinaryReader.h; sourceTree = ""; };
- 2AD4214B17043413005BB8AD /* tamlBinaryWriter.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = tamlBinaryWriter.cc; path = binary/tamlBinaryWriter.cc; sourceTree = ""; };
- 2AD4214C17043413005BB8AD /* tamlBinaryWriter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = tamlBinaryWriter.h; path = binary/tamlBinaryWriter.h; sourceTree = ""; };
- 2ADCAC0E16A41E4400E07619 /* tamlChildren.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = tamlChildren.h; sourceTree = ""; };
- 2ADCAC1016A41E5500E07619 /* ParticleAsset_ScriptBinding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ParticleAsset_ScriptBinding.h; sourceTree = ""; };
- 2ADCAC1116A41E5500E07619 /* ParticleAsset.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ParticleAsset.cc; sourceTree = ""; };
- 2ADCAC1216A41E5500E07619 /* ParticleAsset.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ParticleAsset.h; sourceTree = ""; };
- 2ADCAC1316A41E5500E07619 /* ParticleAssetField.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ParticleAssetField.cc; sourceTree = ""; };
- 2ADCAC1416A41E5500E07619 /* ParticleAssetField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ParticleAssetField.h; sourceTree = ""; };
- 2AE2938216EF4C220015E200 /* WaveComposite_ScriptBinding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = WaveComposite_ScriptBinding.h; path = experimental/composites/WaveComposite_ScriptBinding.h; sourceTree = ""; };
- 2AE2938316EF4C220015E200 /* WaveComposite.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = WaveComposite.cc; path = experimental/composites/WaveComposite.cc; sourceTree = ""; };
- 2AE2938416EF4C220015E200 /* WaveComposite.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = WaveComposite.h; path = experimental/composites/WaveComposite.h; sourceTree = ""; };
- 2AE2F55A16D6B08800B6A058 /* BuoyancyController_ScriptBinding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BuoyancyController_ScriptBinding.h; path = controllers/BuoyancyController_ScriptBinding.h; sourceTree = ""; };
- 2AE2F55B16D6B08800B6A058 /* BuoyancyController.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = BuoyancyController.cc; path = controllers/BuoyancyController.cc; sourceTree = ""; };
- 2AE2F55C16D6B08800B6A058 /* BuoyancyController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BuoyancyController.h; path = controllers/BuoyancyController.h; sourceTree = ""; };
- 2AE5B54016A6D860006908D5 /* ParticleAssetFieldCollection.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ParticleAssetFieldCollection.cc; sourceTree = ""; };
- 2AE5B54116A6D860006908D5 /* ParticleAssetFieldCollection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ParticleAssetFieldCollection.h; sourceTree = ""; };
- 2AF1C53C16B439BB00C1CF3A /* declaredAssets.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = declaredAssets.cc; sourceTree = ""; };
- 2AF1C53D16B439BB00C1CF3A /* declaredAssets.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = declaredAssets.h; sourceTree = ""; };
- 2AF1C53E16B439BB00C1CF3A /* referencedAssets.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = referencedAssets.cc; sourceTree = ""; };
- 2AF1C53F16B439BB00C1CF3A /* referencedAssets.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = referencedAssets.h; sourceTree = ""; };
- 2AF3633716A9BBE0004ED7AA /* ParticleSystem.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ParticleSystem.cc; sourceTree = ""; };
- 2AF3633816A9BBE0004ED7AA /* ParticleSystem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ParticleSystem.h; sourceTree = ""; };
- 2AF80CFF16A80CB400CE13F1 /* ParticleAssetEmitter_ScriptBinding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ParticleAssetEmitter_ScriptBinding.h; sourceTree = ""; };
- 2B4314BD1F1D024900A5C0B7 /* platformNet_ScriptBinding.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = platformNet_ScriptBinding.cc; sourceTree = ""; };
- 2B4314BE1F1D024900A5C0B7 /* platformNet.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = platformNet.cpp; sourceTree = ""; };
- 2B4314BF1F1D024900A5C0B7 /* platformNet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = platformNet.h; sourceTree = ""; };
- 2B4314C01F1D024900A5C0B7 /* platformNetAsync.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = platformNetAsync.cpp; sourceTree = ""; };
- 2B4314C11F1D024900A5C0B7 /* platformNetAsync.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = platformNetAsync.h; sourceTree = ""; };
- 2B4314C51F1D026300A5C0B7 /* tmm_off.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = tmm_off.h; sourceTree = ""; };
- 2B4314C61F1D026300A5C0B7 /* tmm_on.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = tmm_on.h; sourceTree = ""; };
- 2B4314C71F1D026A00A5C0B7 /* typetraits.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = typetraits.h; sourceTree = ""; };
- 2B5F12AA1F1DBC7C006D2B4F /* byteBuffer.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = byteBuffer.cpp; sourceTree = ""; };
- 2B5F12AB1F1DBC7C006D2B4F /* byteBuffer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = byteBuffer.h; sourceTree = ""; };
- 32F6F4B524A5E110008E28D2 /* b2Rope.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = b2Rope.cpp; sourceTree = ""; };
- 32F6F4B624A5E110008E28D2 /* b2Rope.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = b2Rope.h; sourceTree = ""; };
- 32F6F4B824A5E110008E28D2 /* b2Particle.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = b2Particle.cpp; sourceTree = ""; };
- 32F6F4B924A5E110008E28D2 /* b2Particle.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = b2Particle.h; sourceTree = ""; };
- 32F6F4BD24A5E110008E28D2 /* b2ParticleGroup.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = b2ParticleGroup.cpp; sourceTree = ""; };
- 32F6F4BE24A5E110008E28D2 /* b2ParticleGroup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = b2ParticleGroup.h; sourceTree = ""; };
- 32F6F4BF24A5E110008E28D2 /* b2ParticleSystem.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = b2ParticleSystem.cpp; sourceTree = ""; };
- 32F6F4C024A5E110008E28D2 /* b2ParticleSystem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = b2ParticleSystem.h; sourceTree = ""; };
- 32F6F4C124A5E110008E28D2 /* b2StackQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = b2StackQueue.h; sourceTree = ""; };
- 32F6F4C224A5E110008E28D2 /* b2VoronoiDiagram.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = b2VoronoiDiagram.cpp; sourceTree = ""; };
- 32F6F4C324A5E110008E28D2 /* b2VoronoiDiagram.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = b2VoronoiDiagram.h; sourceTree = ""; };
- 32F6F4C524A5E110008E28D2 /* b2BlockAllocator.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = b2BlockAllocator.cpp; sourceTree = ""; };
- 32F6F4C624A5E110008E28D2 /* b2BlockAllocator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = b2BlockAllocator.h; sourceTree = ""; };
- 32F6F4C724A5E110008E28D2 /* b2Draw.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = b2Draw.cpp; sourceTree = ""; };
- 32F6F4C824A5E110008E28D2 /* b2Draw.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = b2Draw.h; sourceTree = ""; };
- 32F6F4C924A5E110008E28D2 /* b2FreeList.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = b2FreeList.cpp; sourceTree = ""; };
- 32F6F4CA24A5E110008E28D2 /* b2FreeList.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = b2FreeList.h; sourceTree = ""; };
- 32F6F4CB24A5E110008E28D2 /* b2GrowableBuffer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = b2GrowableBuffer.h; sourceTree = ""; };
- 32F6F4CC24A5E110008E28D2 /* b2GrowableStack.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = b2GrowableStack.h; sourceTree = ""; };
- 32F6F4CD24A5E110008E28D2 /* b2IntrusiveList.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = b2IntrusiveList.h; sourceTree = ""; };
- 32F6F4CE24A5E110008E28D2 /* b2Math.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = b2Math.cpp; sourceTree = ""; };
- 32F6F4CF24A5E110008E28D2 /* b2Math.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = b2Math.h; sourceTree = ""; };
- 32F6F4D024A5E110008E28D2 /* b2Settings.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = b2Settings.cpp; sourceTree = "