Tools: one toolbar button for all tools, styled tool windows
- ToolBox: instead of a text button per tool (which did not fit the 60x60 icon row and overlapped the icons) the HUD gets one "tools" icon button with a popup listing every visible tool, sorted by localized name. - ToolWindow: chrome that follows the UI style guide (title on the left, close on the right, drag-to-move, double-click/maximize, edge resize), a status bar with buffered status/hint text, dirty-state confirmation and keyboard shortcuts (Ctrl+S in _Input, other keys only when no focused control consumed them). - ToolTheme: denser copy of the game theme for editor toolbars; ToolStatusKind: status bar message kinds. - New tools icon and the tool window texts (unsaved changes, maximize, close, menu title) in en/ru.
This commit is contained in:
1 parent
6884cd5374
commit
b2a0f1c10a
7 files changed
+422
-143
No files matched your search
@@ -259,3 +259,10 @@ ui/journal/entry_of = Entry {0} of {1}
|
|||||||
ui/journal/finished = Completed
|
ui/journal/finished = Completed
|
||||||
ui/journal/seals = Seals
|
ui/journal/seals = Seals
|
||||||
ui/journal/no_entries = The pages of this entry are still empty. Ink will appear as the story moves on.
|
ui/journal/no_entries = The pages of this entry are still empty. Ink will appear as the story moves on.
|
||||||
|
|
||||||
|
tools/unsaved_title = Unsaved changes
|
||||||
|
tools/unsaved_text = You have unsaved changes. Close without saving?
|
||||||
|
tools/unsaved_discard_text = There are unsaved changes. Discard them?
|
||||||
|
tools/window_maximize = Maximize / restore (double-click the title)
|
||||||
|
tools/window_close = Close
|
||||||
|
tools/menu_title = Tools
|
||||||
@@ -259,3 +259,10 @@ ui/journal/seals = Печати
|
|||||||
ui/journal/no_entries = Страницы этой записи ещё пусты. Чернила появятся, когда история сдвинется с места.
|
ui/journal/no_entries = Страницы этой записи ещё пусты. Чернила появятся, когда история сдвинется с места.
|
||||||
mod_interface_name = Интерфейс
|
mod_interface_name = Интерфейс
|
||||||
mod_interface_desc = Подсистема UI движка: набор виджетов (Widget/ModWidget, списки), ViewBox фона, тулбар ToolBox, GameMenu/SaveIcon, панели уведомлений, дизайн-система UiKit и DeviceInput. Ядро работает с модом через контракты IWidget/IToolbarHost и мосты.
|
mod_interface_desc = Подсистема UI движка: набор виджетов (Widget/ModWidget, списки), ViewBox фона, тулбар ToolBox, GameMenu/SaveIcon, панели уведомлений, дизайн-система UiKit и DeviceInput. Ядро работает с модом через контракты IWidget/IToolbarHost и мосты.
|
||||||
|
|
||||||
|
tools/unsaved_title = Есть изменения
|
||||||
|
tools/unsaved_text = У вас есть несохранённые изменения. Закрыть без сохранения?
|
||||||
|
tools/unsaved_discard_text = Есть несохранённые изменения. Отбросить их?
|
||||||
|
tools/window_maximize = Развернуть / восстановить (двойной клик по заголовку)
|
||||||
|
tools/window_close = Закрыть
|
||||||
|
tools/menu_title = Инструменты
|
||||||
+28
-11
@@ -89,29 +89,46 @@ namespace Cthangover.Mods.Interface.Tool
|
|||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds one "tools" toolbar button whose popup lists every visible
|
||||||
|
/// <see cref="IToolBoxButton"/>. Tools used to get a text button each,
|
||||||
|
/// which did not fit the 60×60 icon row and overlapped its icons; a
|
||||||
|
/// single icon keeps the HUD row uniform no matter how many tools are
|
||||||
|
/// installed. Nothing is added when no tool button is visible.
|
||||||
|
/// </summary>
|
||||||
private void AddToolButtons(HBoxContainer container)
|
private void AddToolButtons(HBoxContainer container)
|
||||||
{
|
{
|
||||||
if (container == null)
|
if (container == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
foreach (var buttonDef in ToolBoxButtonFactory.Instance.GetVisible())
|
var visible = new List<IToolBoxButton>(ToolBoxButtonFactory.Instance.GetVisible());
|
||||||
{
|
if (visible.Count == 0)
|
||||||
var toolId = buttonDef.ToolId;
|
return;
|
||||||
var title = TranslationServer.Translate(buttonDef.LocaleKey);
|
|
||||||
|
|
||||||
Texture2D icon = null;
|
visible.Sort((a, b) => string.Compare(
|
||||||
if (!string.IsNullOrEmpty(buttonDef.IconPath) && ResourceLoader.Exists(buttonDef.IconPath))
|
TranslationServer.Translate(a.LocaleKey), TranslationServer.Translate(b.LocaleKey),
|
||||||
icon = ResourceLoader.Load<Texture2D>(buttonDef.IconPath);
|
System.StringComparison.CurrentCulture));
|
||||||
|
|
||||||
var btn = CreateToolbarButton(title, icon);
|
var btn = CreateToolbarButton(TranslationServer.Translate("tools/menu_title"), UIIconFactory.Instance.Get("tools"));
|
||||||
|
var menu = new PopupMenu { Theme = ToolTheme.Get() };
|
||||||
|
foreach (var buttonDef in visible)
|
||||||
|
menu.AddItem(TranslationServer.Translate(buttonDef.LocaleKey));
|
||||||
|
menu.IndexPressed += index => OpenTool(visible[(int)index].ToolId);
|
||||||
|
btn.AddChild(menu);
|
||||||
btn.Pressed += () =>
|
btn.Pressed += () =>
|
||||||
|
{
|
||||||
|
var rect = btn.GetGlobalRect();
|
||||||
|
menu.Position = new Vector2I((int)rect.Position.X, (int)rect.End.Y + UiTokens.Space1);
|
||||||
|
menu.Popup();
|
||||||
|
};
|
||||||
|
container.AddChild(btn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OpenTool(string toolId)
|
||||||
{
|
{
|
||||||
var tool = ToolFactory.Instance.Get(toolId);
|
var tool = ToolFactory.Instance.Get(toolId);
|
||||||
if (tool != null)
|
if (tool != null)
|
||||||
ToolWindow.ShowWindow(tool.CreateWindow());
|
ToolWindow.ShowWindow(tool.CreateWindow());
|
||||||
};
|
|
||||||
container.AddChild(btn);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Cthangover.Mods.Interface.Tool
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Tone of a message in the <see cref="ToolWindow"/> status bar, mapped to the
|
||||||
|
/// style guide's inline messages: neutral info, ok (<c>.ct-message--ok</c>)
|
||||||
|
/// and error (<c>.ct-message--err</c>).
|
||||||
|
/// </summary>
|
||||||
|
public enum ToolStatusKind
|
||||||
|
{
|
||||||
|
Info,
|
||||||
|
Ok,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using Cthangover.Core.UI.Theming;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Cthangover.Mods.Interface.Tool
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Dense variant of <see cref="CthangoverTheme"/> for developer tool windows.
|
||||||
|
/// The game theme is tuned for player panels (buttons padded 18×11, inputs
|
||||||
|
/// 12×9), which makes an editor toolbar of ten controls overflow even a
|
||||||
|
/// full-HD window. This theme keeps every colour, radius and type variation
|
||||||
|
/// of the shared theme — the style guide still holds — and only tightens
|
||||||
|
/// the content margins of the boxes, so tools fit more controls without a
|
||||||
|
/// second palette. Built once by copying the shared theme and replacing its
|
||||||
|
/// flat boxes with tightened duplicates; the shared theme is never mutated.
|
||||||
|
/// </summary>
|
||||||
|
public static class ToolTheme
|
||||||
|
{
|
||||||
|
private const int MaxVerticalMargin = 5;
|
||||||
|
private const int MaxHorizontalMargin = 10;
|
||||||
|
|
||||||
|
private static Theme instance;
|
||||||
|
|
||||||
|
/// <summary>Shared dense theme instance, built lazily from the game theme.</summary>
|
||||||
|
public static Theme Get() => instance ??= Build();
|
||||||
|
|
||||||
|
private static Theme Build()
|
||||||
|
{
|
||||||
|
var theme = (Theme)CthangoverTheme.Get().Duplicate();
|
||||||
|
theme.DefaultFontSize = UiTokens.FontBody;
|
||||||
|
|
||||||
|
foreach (var type in theme.GetTypeList())
|
||||||
|
{
|
||||||
|
foreach (var name in theme.GetStyleboxList(type))
|
||||||
|
{
|
||||||
|
if (theme.GetStylebox(name, type) is not StyleBoxFlat flat)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var dense = (StyleBoxFlat)flat.Duplicate();
|
||||||
|
dense.ContentMarginTop = Mathf.Min(dense.ContentMarginTop, MaxVerticalMargin);
|
||||||
|
dense.ContentMarginBottom = Mathf.Min(dense.ContentMarginBottom, MaxVerticalMargin);
|
||||||
|
dense.ContentMarginLeft = Mathf.Min(dense.ContentMarginLeft, MaxHorizontalMargin);
|
||||||
|
dense.ContentMarginRight = Mathf.Min(dense.ContentMarginRight, MaxHorizontalMargin);
|
||||||
|
theme.SetStylebox(name, type, dense);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (theme.HasFontSize("font_size", type) && theme.GetFontSize("font_size", type) > UiTokens.FontBody)
|
||||||
|
theme.SetFontSize("font_size", type, UiTokens.FontBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
return theme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+313
-132
@@ -8,23 +8,34 @@ using Cthangover.Core.UI.Tool;
|
|||||||
namespace Cthangover.Mods.Interface.Tool
|
namespace Cthangover.Mods.Interface.Tool
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base for tool popup windows with dirty-state tracking and custom borderless
|
/// Base for developer tool windows: borderless chrome that follows the UI
|
||||||
/// chrome (title bar with minimize/maximize/close, drag-to-move, edge resize).
|
/// style guide (title left in the header, ✕ on the right, drag-to-move,
|
||||||
/// Subclasses build their UI in the constructor exactly as before — the chrome
|
/// double-click/□ to maximize, edge resize), a status bar, dirty-state
|
||||||
/// is applied transparently by <see cref="SetupChrome"/> after construction.
|
/// tracking and keyboard shortcuts.
|
||||||
|
/// <para/>
|
||||||
|
/// Subclasses build their UI in the constructor and add it as direct
|
||||||
|
/// children; <see cref="SetupChrome"/> (run by <see cref="Open{T}"/> and
|
||||||
|
/// <see cref="ShowWindow"/>) later moves those children into the content
|
||||||
|
/// area between the header and the status bar. Status and hint text may be
|
||||||
|
/// set before the chrome exists — it is buffered and shown once built.
|
||||||
|
/// <para/>
|
||||||
|
/// Tool windows use <see cref="ToolTheme"/>, a denser copy of the game
|
||||||
|
/// theme: same tokens, smaller paddings, so editor toolbars fit.
|
||||||
|
/// <para/>
|
||||||
|
/// Shortcuts: Ctrl+S calls <see cref="OnSaveShortcut"/> even while a text
|
||||||
|
/// field has focus (checked in <c>_Input</c>, before the GUI); every other
|
||||||
|
/// key reaches <see cref="HandleKey"/> only if no focused control consumed
|
||||||
|
/// it (<c>_UnhandledInput</c>), so Delete in a text field never deletes a
|
||||||
|
/// selected object.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract partial class ToolWindow : Window
|
public abstract partial class ToolWindow : Window
|
||||||
{
|
{
|
||||||
// ── existing fields ──────────────────────────────────────────
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the window has unsaved changes. When <c>true</c>, the title bar shows a "*" prefix
|
/// Whether the window has unsaved changes. When <c>true</c>, the title bar shows a "*" prefix
|
||||||
/// and <see cref="HandleCloseRequested"/> prompts for confirmation before closing.
|
/// and closing asks for confirmation first.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected bool _dirty;
|
protected bool _dirty;
|
||||||
private string _baseTitle;
|
private readonly string _baseTitle;
|
||||||
|
|
||||||
// ── chrome fields ───────────────────────────────────────────
|
|
||||||
|
|
||||||
private bool _chromeBuilt;
|
private bool _chromeBuilt;
|
||||||
|
|
||||||
@@ -35,13 +46,16 @@ namespace Cthangover.Mods.Interface.Tool
|
|||||||
private Label _titleLabel;
|
private Label _titleLabel;
|
||||||
private Button _maximizeBtn;
|
private Button _maximizeBtn;
|
||||||
private HBoxContainer _titleBar;
|
private HBoxContainer _titleBar;
|
||||||
|
private Label _statusLabel;
|
||||||
|
private Label _hintLabel;
|
||||||
|
private string _pendingStatus = string.Empty;
|
||||||
|
private ToolStatusKind _pendingStatusKind;
|
||||||
|
private string _pendingHint = string.Empty;
|
||||||
|
|
||||||
// drag
|
|
||||||
private bool _draggingTitle;
|
private bool _draggingTitle;
|
||||||
private Vector2 _titleDragMouseStart;
|
private Vector2 _titleDragMouseStart;
|
||||||
private Vector2I _titleDragWindowStart;
|
private Vector2I _titleDragWindowStart;
|
||||||
|
|
||||||
// resize
|
|
||||||
private enum ResizeEdge { None, Left, Right, Top, Bottom, TopLeft, TopRight, BottomLeft, BottomRight }
|
private enum ResizeEdge { None, Left, Right, Top, Bottom, TopLeft, TopRight, BottomLeft, BottomRight }
|
||||||
private bool _resizing;
|
private bool _resizing;
|
||||||
private ResizeEdge _resizeEdge;
|
private ResizeEdge _resizeEdge;
|
||||||
@@ -51,9 +65,18 @@ namespace Cthangover.Mods.Interface.Tool
|
|||||||
|
|
||||||
private const int EDGE_ZONE = 6;
|
private const int EDGE_ZONE = 6;
|
||||||
private const int CORNER_ZONE = 14;
|
private const int CORNER_ZONE = 14;
|
||||||
private const int TITLE_BAR_HEIGHT = 32;
|
private const int TITLE_BAR_HEIGHT = 40;
|
||||||
|
private const int TITLE_BUTTON_SIZE = 38;
|
||||||
|
private const float DefaultSizeRatio = 0.9f;
|
||||||
|
|
||||||
// ── viewport sizing ─────────────────────────────────────────
|
/// <summary>
|
||||||
|
/// Height of the in-game HUD icon row. HUD canvas layers draw above
|
||||||
|
/// embedded windows, so a window placed under it would have its title
|
||||||
|
/// bar covered; windows open below this line.
|
||||||
|
/// </summary>
|
||||||
|
private const int HudTopInset = 84;
|
||||||
|
|
||||||
|
private static Font _monospaceFont;
|
||||||
|
|
||||||
private static Vector2 GetGameViewportSize()
|
private static Vector2 GetGameViewportSize()
|
||||||
{
|
{
|
||||||
@@ -74,11 +97,7 @@ namespace Cthangover.Mods.Interface.Tool
|
|||||||
public static T Open<T>() where T : ToolWindow, new()
|
public static T Open<T>() where T : ToolWindow, new()
|
||||||
{
|
{
|
||||||
var window = new T();
|
var window = new T();
|
||||||
window.SetupChrome();
|
ShowWindow(window);
|
||||||
var tree = Godot.Engine.GetMainLoop() as SceneTree;
|
|
||||||
tree?.Root.AddChild(window);
|
|
||||||
window.PopupCentered();
|
|
||||||
GameLogger.Log("TOOL_WINDOW", $"Open<{typeof(T).Name}> gameViewport=({GetGameViewportSize().X:F0},{GetGameViewportSize().Y:F0}) physWindow=({DisplayServer.WindowGetSize().X},{DisplayServer.WindowGetSize().Y}) windowSize=({window.Size.X},{window.Size.Y})");
|
|
||||||
return window;
|
return window;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,11 +109,14 @@ namespace Cthangover.Mods.Interface.Tool
|
|||||||
{
|
{
|
||||||
if (window is ToolWindow tw)
|
if (window is ToolWindow tw)
|
||||||
tw.SetupChrome();
|
tw.SetupChrome();
|
||||||
|
else
|
||||||
CthangoverTheme.Apply(window);
|
CthangoverTheme.Apply(window);
|
||||||
var tree = Godot.Engine.GetMainLoop() as SceneTree;
|
var tree = Godot.Engine.GetMainLoop() as SceneTree;
|
||||||
tree?.Root.AddChild(window);
|
tree?.Root.AddChild(window);
|
||||||
window.PopupCentered();
|
window.PopupCentered();
|
||||||
GameLogger.Log("TOOL_WINDOW", $"ShowWindow gameViewport=({GetGameViewportSize().X:F0},{GetGameViewportSize().Y:F0}) physWindow=({DisplayServer.WindowGetSize().X},{DisplayServer.WindowGetSize().Y}) windowSize=({window.Size.X},{window.Size.Y})");
|
if (window is ToolWindow && window.Position.Y < HudTopInset)
|
||||||
|
window.Position = new Vector2I(window.Position.X, HudTopInset);
|
||||||
|
GameLogger.Log("TOOL_WINDOW", $"ShowWindow '{window.GetType().Name}' size=({window.Size.X},{window.Size.Y})");
|
||||||
return window;
|
return window;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +127,7 @@ namespace Cthangover.Mods.Interface.Tool
|
|||||||
_baseTitle = TranslationServer.Translate(titleKey);
|
_baseTitle = TranslationServer.Translate(titleKey);
|
||||||
Title = _baseTitle;
|
Title = _baseTitle;
|
||||||
Unresizable = false;
|
Unresizable = false;
|
||||||
CloseRequested += HandleCloseRequested;
|
CloseRequested += RequestClose;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Runs the <see cref="Cleanup"/> teardown hook when the window leaves the tree.</summary>
|
/// <summary>Runs the <see cref="Cleanup"/> teardown hook when the window leaves the tree.</summary>
|
||||||
@@ -114,18 +136,46 @@ namespace Cthangover.Mods.Interface.Tool
|
|||||||
Cleanup();
|
Cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Ctrl+S → <see cref="OnSaveShortcut"/>, checked before the GUI so it works inside text fields.</summary>
|
||||||
|
public override void _Input(InputEvent @event)
|
||||||
|
{
|
||||||
|
if (@event is not InputEventKey key || !key.Pressed || key.Echo)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (key.CtrlPressed && key.Keycode == Key.S)
|
||||||
|
{
|
||||||
|
OnSaveShortcut();
|
||||||
|
SetInputAsHandled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Forwards keys no control consumed to <see cref="HandleKey"/>.</summary>
|
||||||
|
public override void _UnhandledInput(InputEvent @event)
|
||||||
|
{
|
||||||
|
if (@event is InputEventKey key && key.Pressed && HandleKey(key))
|
||||||
|
SetInputAsHandled();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Override to release resources when the window is closed. Called from <c>_ExitTree</c>.
|
/// Override to release resources when the window is closed. Called from <c>_ExitTree</c>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual void Cleanup() { }
|
protected virtual void Cleanup() { }
|
||||||
|
|
||||||
|
/// <summary>Ctrl+S handler; tools with a save action override it.</summary>
|
||||||
|
protected virtual void OnSaveShortcut() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tool-specific shortcut for a key no focused control consumed.
|
||||||
|
/// Return <c>true</c> when handled.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual bool HandleKey(InputEventKey key) => false;
|
||||||
|
|
||||||
// ── chrome setup ────────────────────────────────────────────
|
// ── chrome setup ────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Wraps all currently-added children inside a custom chrome layout
|
/// Wraps all currently-added children inside the chrome layout (title bar,
|
||||||
/// (title bar + border + resize zones). Called automatically by
|
/// content area, status bar, resize zones). Called automatically by
|
||||||
/// <see cref="Open{T}"/> and <see cref="ShowWindow"/> after the
|
/// <see cref="Open{T}"/> and <see cref="ShowWindow"/>; safe to call twice.
|
||||||
/// subclass constructor has built its UI. Safe to call only once.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal void SetupChrome()
|
internal void SetupChrome()
|
||||||
{
|
{
|
||||||
@@ -133,18 +183,19 @@ namespace Cthangover.Mods.Interface.Tool
|
|||||||
return;
|
return;
|
||||||
_chromeBuilt = true;
|
_chromeBuilt = true;
|
||||||
|
|
||||||
CthangoverTheme.Apply(this);
|
Theme = ToolTheme.Get();
|
||||||
|
|
||||||
var initialSize = Size;
|
|
||||||
|
|
||||||
|
// Read the requested size first: assigning MinSize clamps Size at once.
|
||||||
|
var requested = Size;
|
||||||
|
var viewport = GetGameViewportSize();
|
||||||
Borderless = true;
|
Borderless = true;
|
||||||
MinSize = new Vector2I(800, 400);
|
MinSize = new Vector2I(800, 400);
|
||||||
MaxSize = (Vector2I)GetGameViewportSize();
|
MaxSize = (Vector2I)viewport;
|
||||||
|
|
||||||
if (initialSize.X < MinSize.X || initialSize.Y < MinSize.Y)
|
if (requested.X < MinSize.X || requested.Y < MinSize.Y)
|
||||||
Size = MaxSize;
|
requested = (Vector2I)(viewport * DefaultSizeRatio);
|
||||||
|
var maxHeight = MaxSize.Y - HudTopInset - UiTokens.Space3;
|
||||||
GameLogger.Log("TOOL_WINDOW", $"SetupChrome gameViewport=({GetGameViewportSize().X:F0},{GetGameViewportSize().Y:F0}) physWindow=({DisplayServer.WindowGetSize().X},{DisplayServer.WindowGetSize().Y}) maxSize=({MaxSize.X},{MaxSize.Y})");
|
Size = new Vector2I(Mathf.Min(requested.X, MaxSize.X), Mathf.Min(requested.Y, maxHeight));
|
||||||
|
|
||||||
var existingChildren = new List<Node>();
|
var existingChildren = new List<Node>();
|
||||||
foreach (Node child in GetChildren())
|
foreach (Node child in GetChildren())
|
||||||
@@ -155,12 +206,21 @@ namespace Cthangover.Mods.Interface.Tool
|
|||||||
var rootPanel = BuildRootPanel();
|
var rootPanel = BuildRootPanel();
|
||||||
|
|
||||||
var rootVBox = new VBoxContainer();
|
var rootVBox = new VBoxContainer();
|
||||||
rootVBox.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
rootVBox.AddThemeConstantOverride("separation", 0);
|
||||||
rootVBox.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
|
|
||||||
rootPanel.AddChild(rootVBox);
|
rootPanel.AddChild(rootVBox);
|
||||||
|
|
||||||
var titleBar = BuildTitleBar();
|
rootVBox.AddChild(BuildTitleBar());
|
||||||
rootVBox.AddChild(titleBar);
|
|
||||||
|
var contentMargin = new MarginContainer
|
||||||
|
{
|
||||||
|
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||||
|
SizeFlagsVertical = Control.SizeFlags.ExpandFill
|
||||||
|
};
|
||||||
|
contentMargin.AddThemeConstantOverride("margin_left", UiTokens.Space3);
|
||||||
|
contentMargin.AddThemeConstantOverride("margin_right", UiTokens.Space3);
|
||||||
|
contentMargin.AddThemeConstantOverride("margin_top", UiTokens.Space3);
|
||||||
|
contentMargin.AddThemeConstantOverride("margin_bottom", UiTokens.Space1);
|
||||||
|
rootVBox.AddChild(contentMargin);
|
||||||
|
|
||||||
var contentArea = new Control
|
var contentArea = new Control
|
||||||
{
|
{
|
||||||
@@ -168,11 +228,13 @@ namespace Cthangover.Mods.Interface.Tool
|
|||||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||||
SizeFlagsVertical = Control.SizeFlags.ExpandFill
|
SizeFlagsVertical = Control.SizeFlags.ExpandFill
|
||||||
};
|
};
|
||||||
rootVBox.AddChild(contentArea);
|
contentMargin.AddChild(contentArea);
|
||||||
|
|
||||||
foreach (var child in existingChildren)
|
foreach (var child in existingChildren)
|
||||||
contentArea.AddChild(child);
|
contentArea.AddChild(child);
|
||||||
|
|
||||||
|
rootVBox.AddChild(BuildStatusBar());
|
||||||
|
|
||||||
AddResizeZones();
|
AddResizeZones();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,14 +245,11 @@ namespace Cthangover.Mods.Interface.Tool
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected void SetDirty()
|
protected void SetDirty()
|
||||||
{
|
{
|
||||||
if (!_dirty)
|
if (_dirty)
|
||||||
{
|
return;
|
||||||
|
|
||||||
_dirty = true;
|
_dirty = true;
|
||||||
var text = "* " + _baseTitle;
|
UpdateTitleText();
|
||||||
Title = text;
|
|
||||||
if (_titleLabel != null)
|
|
||||||
_titleLabel.Text = text;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -199,33 +258,98 @@ namespace Cthangover.Mods.Interface.Tool
|
|||||||
protected void MarkClean()
|
protected void MarkClean()
|
||||||
{
|
{
|
||||||
_dirty = false;
|
_dirty = false;
|
||||||
Title = _baseTitle;
|
UpdateTitleText();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateTitleText()
|
||||||
|
{
|
||||||
|
var text = _dirty ? "* " + _baseTitle : _baseTitle;
|
||||||
|
Title = text;
|
||||||
if (_titleLabel != null)
|
if (_titleLabel != null)
|
||||||
_titleLabel.Text = _baseTitle;
|
_titleLabel.Text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── status bar ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Shows a message in the status bar, coloured by <paramref name="kind"/>.</summary>
|
||||||
|
protected void SetStatus(string text, ToolStatusKind kind = ToolStatusKind.Info)
|
||||||
|
{
|
||||||
|
_pendingStatus = text ?? string.Empty;
|
||||||
|
_pendingStatusKind = kind;
|
||||||
|
if (_statusLabel == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_statusLabel.Text = _pendingStatus;
|
||||||
|
_statusLabel.AddThemeColorOverride("font_color", kind switch
|
||||||
|
{
|
||||||
|
ToolStatusKind.Ok => UiTokens.Ok,
|
||||||
|
ToolStatusKind.Error => UiTokens.DangerText,
|
||||||
|
_ => UiTokens.Body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Sets the faint right-aligned hint of the status bar (usually the shortcut list).</summary>
|
||||||
|
protected void SetHint(string text)
|
||||||
|
{
|
||||||
|
_pendingHint = text ?? string.Empty;
|
||||||
|
if (_hintLabel != null)
|
||||||
|
_hintLabel.Text = _pendingHint;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── unsaved-changes dialogs ─────────────────────────────────
|
// ── unsaved-changes dialogs ─────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Asks "close without saving?" and runs <paramref name="onDiscard"/> on confirmation.</summary>
|
||||||
protected void ShowUnsavedDialog(Action onDiscard)
|
protected void ShowUnsavedDialog(Action onDiscard)
|
||||||
{
|
{
|
||||||
var dialog = new ConfirmationDialog();
|
ShowConfirm("tools/unsaved_title", "tools/unsaved_text", onDiscard);
|
||||||
dialog.Title = TranslationServer.Translate("tools/unsaved_title");
|
}
|
||||||
dialog.DialogText = TranslationServer.Translate("tools/unsaved_text");
|
|
||||||
dialog.Confirmed += () => { dialog.QueueFree(); onDiscard(); };
|
/// <summary>
|
||||||
dialog.Canceled += () => dialog.QueueFree();
|
/// Runs <paramref name="action"/> right away when there are no unsaved
|
||||||
|
/// changes, otherwise only after the author agrees to discard them; on
|
||||||
|
/// refusal <paramref name="onCancel"/> runs (e.g. to roll a file picker
|
||||||
|
/// back). Use before replacing the edited document.
|
||||||
|
/// </summary>
|
||||||
|
protected void RunDiscardingChanges(Action action, Action onCancel = null)
|
||||||
|
{
|
||||||
|
if (!_dirty)
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShowConfirm("tools/unsaved_title", "tools/unsaved_discard_text", () =>
|
||||||
|
{
|
||||||
|
MarkClean();
|
||||||
|
action();
|
||||||
|
}, onCancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Shows a themed OK/Cancel dialog; <paramref name="onConfirm"/> runs on OK, <paramref name="onCancel"/> otherwise.</summary>
|
||||||
|
protected void ShowConfirm(string titleKey, string textKey, Action onConfirm, Action onCancel = null)
|
||||||
|
{
|
||||||
|
var dialog = new ConfirmationDialog
|
||||||
|
{
|
||||||
|
Title = TranslationServer.Translate(titleKey),
|
||||||
|
DialogText = TranslationServer.Translate(textKey),
|
||||||
|
Theme = ToolTheme.Get()
|
||||||
|
};
|
||||||
|
dialog.Confirmed += () => { dialog.QueueFree(); onConfirm(); };
|
||||||
|
dialog.Canceled += () => { dialog.QueueFree(); onCancel?.Invoke(); };
|
||||||
AddChild(dialog);
|
AddChild(dialog);
|
||||||
dialog.PopupCentered();
|
dialog.PopupCentered();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleCloseRequested()
|
/// <summary>Closes the window, asking first when there are unsaved changes.</summary>
|
||||||
|
protected void RequestClose()
|
||||||
{
|
{
|
||||||
if (_dirty)
|
if (_dirty)
|
||||||
ShowUnsavedDialog(() => QueueFree());
|
ShowUnsavedDialog(QueueFree);
|
||||||
else
|
else
|
||||||
QueueFree();
|
QueueFree();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── layout helpers (unchanged API) ──────────────────────────
|
// ── layout helpers ──────────────────────────────────────────
|
||||||
|
|
||||||
protected static VBoxContainer CreateFillContainer()
|
protected static VBoxContainer CreateFillContainer()
|
||||||
{
|
{
|
||||||
@@ -234,13 +358,14 @@ namespace Cthangover.Mods.Interface.Tool
|
|||||||
c.AnchorBottom = 1f;
|
c.AnchorBottom = 1f;
|
||||||
c.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
c.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||||
c.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
|
c.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
|
||||||
|
c.AddThemeConstantOverride("separation", UiTokens.Space3);
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static HBoxContainer CreateToolbar()
|
protected static HBoxContainer CreateToolbar()
|
||||||
{
|
{
|
||||||
var t = new HBoxContainer();
|
var t = new HBoxContainer();
|
||||||
t.AddThemeConstantOverride("separation", 8);
|
t.AddThemeConstantOverride("separation", UiTokens.Space2);
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,26 +374,54 @@ namespace Cthangover.Mods.Interface.Tool
|
|||||||
var panel = new PanelContainer();
|
var panel = new PanelContainer();
|
||||||
panel.CustomMinimumSize = new Vector2(width, 100);
|
panel.CustomMinimumSize = new Vector2(width, 100);
|
||||||
panel.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
|
panel.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
|
||||||
|
panel.AddThemeStyleboxOverride("panel", SidebarBox());
|
||||||
|
|
||||||
var scroll = new ScrollContainer();
|
var scroll = new ScrollContainer();
|
||||||
scroll.HorizontalScrollMode = ScrollContainer.ScrollMode.Disabled;
|
scroll.HorizontalScrollMode = ScrollContainer.ScrollMode.Disabled;
|
||||||
scroll.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
|
scroll.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
|
||||||
scroll.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
scroll.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||||
panel.AddChild(scroll);
|
panel.AddChild(scroll);
|
||||||
|
content.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||||
|
content.AddThemeConstantOverride("separation", UiTokens.Space2);
|
||||||
scroll.AddChild(content);
|
scroll.AddChild(content);
|
||||||
|
|
||||||
return panel;
|
return panel;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static Font GetMonospaceFont()
|
/// <summary>Inset sidebar surface: <c>--ct-surface-2</c>, line border, card radius.</summary>
|
||||||
|
protected static StyleBoxFlat SidebarBox()
|
||||||
{
|
{
|
||||||
try
|
var style = new StyleBoxFlat
|
||||||
{
|
{
|
||||||
if (ResourceLoader.Exists("res://assets/fonts/RobotoMono.ttf"))
|
BgColor = UiTokens.Surface2,
|
||||||
return ResourceLoader.Load<FontFile>("res://assets/fonts/RobotoMono.ttf");
|
BorderColor = UiTokens.Line,
|
||||||
|
BorderWidthLeft = 1,
|
||||||
|
BorderWidthRight = 1,
|
||||||
|
BorderWidthTop = 1,
|
||||||
|
BorderWidthBottom = 1,
|
||||||
|
CornerRadiusTopLeft = UiTokens.RadiusMd,
|
||||||
|
CornerRadiusTopRight = UiTokens.RadiusMd,
|
||||||
|
CornerRadiusBottomLeft = UiTokens.RadiusMd,
|
||||||
|
CornerRadiusBottomRight = UiTokens.RadiusMd,
|
||||||
|
ContentMarginLeft = UiTokens.Space4,
|
||||||
|
ContentMarginRight = UiTokens.Space4,
|
||||||
|
ContentMarginTop = UiTokens.Space4,
|
||||||
|
ContentMarginBottom = UiTokens.Space4
|
||||||
|
};
|
||||||
|
return style;
|
||||||
}
|
}
|
||||||
catch { }
|
|
||||||
return ThemeDB.FallbackFont;
|
/// <summary>
|
||||||
|
/// Monospace font for code, IDs and numbers (style guide: <c>--ct-mono</c>).
|
||||||
|
/// The game ships no mono font, so the OS one is requested by name with the
|
||||||
|
/// guide's fallbacks; Godot falls back to its default font if none exist.
|
||||||
|
/// </summary>
|
||||||
|
public static Font GetMonospaceFont()
|
||||||
|
{
|
||||||
|
return _monospaceFont ??= new SystemFont
|
||||||
|
{
|
||||||
|
FontNames = new[] { "Consolas", "Cascadia Mono", "DejaVu Sans Mono", "Liberation Mono", "Courier New", "monospace" }
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── chrome building ─────────────────────────────────────────
|
// ── chrome building ─────────────────────────────────────────
|
||||||
@@ -290,10 +443,14 @@ namespace Cthangover.Mods.Interface.Tool
|
|||||||
BorderWidthBottom = 1,
|
BorderWidthBottom = 1,
|
||||||
BorderWidthTop = 1,
|
BorderWidthTop = 1,
|
||||||
BorderColor = UiTokens.Line,
|
BorderColor = UiTokens.Line,
|
||||||
ContentMarginLeft = 4,
|
CornerRadiusTopLeft = UiTokens.RadiusLg,
|
||||||
ContentMarginRight = 4,
|
CornerRadiusTopRight = UiTokens.RadiusLg,
|
||||||
ContentMarginBottom = 4,
|
CornerRadiusBottomLeft = UiTokens.RadiusLg,
|
||||||
ContentMarginTop = 4
|
CornerRadiusBottomRight = UiTokens.RadiusLg,
|
||||||
|
ContentMarginLeft = 1,
|
||||||
|
ContentMarginRight = 1,
|
||||||
|
ContentMarginBottom = 1,
|
||||||
|
ContentMarginTop = 1
|
||||||
};
|
};
|
||||||
panel.AddThemeStyleboxOverride("panel", style);
|
panel.AddThemeStyleboxOverride("panel", style);
|
||||||
AddChild(panel);
|
AddChild(panel);
|
||||||
@@ -308,25 +465,26 @@ namespace Cthangover.Mods.Interface.Tool
|
|||||||
Name = "TitleBar",
|
Name = "TitleBar",
|
||||||
CustomMinimumSize = new Vector2(0, TITLE_BAR_HEIGHT)
|
CustomMinimumSize = new Vector2(0, TITLE_BAR_HEIGHT)
|
||||||
};
|
};
|
||||||
|
_titleBar.AddThemeConstantOverride("separation", UiTokens.Space1);
|
||||||
|
|
||||||
_titleLabel = new Label
|
_titleLabel = new Label
|
||||||
{
|
{
|
||||||
Text = _dirty ? "* " + _baseTitle : _baseTitle,
|
Text = _dirty ? "* " + _baseTitle : _baseTitle,
|
||||||
|
Uppercase = true,
|
||||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
MouseFilter = Control.MouseFilterEnum.Ignore
|
MouseFilter = Control.MouseFilterEnum.Ignore
|
||||||
};
|
};
|
||||||
_titleLabel.AddThemeColorOverride("font_color", UiTokens.InkBright);
|
_titleLabel.AddThemeColorOverride("font_color", UiTokens.InkBright);
|
||||||
|
_titleLabel.AddThemeFontSizeOverride("font_size", UiTokens.FontLead);
|
||||||
_titleBar.AddChild(_titleLabel);
|
_titleBar.AddChild(_titleLabel);
|
||||||
|
|
||||||
_maximizeBtn = CreateTitleButton("\u25A1", OnMaximizeToggle,
|
_maximizeBtn = CreateTitleButton("□", OnMaximizeToggle, UiTokens.BtnHover, UiTokens.InkBright);
|
||||||
UiTokens.Muted);
|
_maximizeBtn.TooltipText = TranslationServer.Translate("tools/window_maximize");
|
||||||
CthangoverTheme.ApplyIcon(_maximizeBtn);
|
|
||||||
_titleBar.AddChild(_maximizeBtn);
|
_titleBar.AddChild(_maximizeBtn);
|
||||||
|
|
||||||
var closeBtn = CreateTitleButton("\u2715", OnClose,
|
var closeBtn = CreateTitleButton("✕", RequestClose, UiTokens.DangerSoft, UiTokens.DangerText);
|
||||||
UiTokens.Danger);
|
closeBtn.TooltipText = TranslationServer.Translate("tools/window_close");
|
||||||
CthangoverTheme.ApplyDanger(closeBtn);
|
|
||||||
_titleBar.AddChild(closeBtn);
|
_titleBar.AddChild(closeBtn);
|
||||||
|
|
||||||
_titleBar.GuiInput += OnTitleBarInput;
|
_titleBar.GuiInput += OnTitleBarInput;
|
||||||
@@ -336,39 +494,93 @@ namespace Cthangover.Mods.Interface.Tool
|
|||||||
|
|
||||||
var titleBg = new StyleBoxFlat
|
var titleBg = new StyleBoxFlat
|
||||||
{
|
{
|
||||||
BgColor = UiTokens.Surface2,
|
BgColor = UiTokens.Surface1,
|
||||||
ContentMarginLeft = 10,
|
BorderColor = UiTokens.Line,
|
||||||
ContentMarginRight = 2,
|
BorderWidthBottom = 1,
|
||||||
|
CornerRadiusTopLeft = UiTokens.RadiusLg,
|
||||||
|
CornerRadiusTopRight = UiTokens.RadiusLg,
|
||||||
|
ContentMarginLeft = UiTokens.Space7,
|
||||||
|
ContentMarginRight = UiTokens.Space1,
|
||||||
};
|
};
|
||||||
titlePanel.AddThemeStyleboxOverride("panel", titleBg);
|
titlePanel.AddThemeStyleboxOverride("panel", titleBg);
|
||||||
|
|
||||||
return titlePanel;
|
return titlePanel;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Button CreateTitleButton(string text, Action pressed, Color hoverColor)
|
private PanelContainer BuildStatusBar()
|
||||||
|
{
|
||||||
|
var bar = new HBoxContainer { Name = "StatusBar", CustomMinimumSize = new Vector2(0, 26) };
|
||||||
|
bar.AddThemeConstantOverride("separation", UiTokens.Space5);
|
||||||
|
|
||||||
|
_statusLabel = new Label
|
||||||
|
{
|
||||||
|
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
TextOverrunBehavior = TextServer.OverrunBehavior.TrimEllipsis
|
||||||
|
};
|
||||||
|
_statusLabel.AddThemeFontSizeOverride("font_size", UiTokens.FontBodySmall);
|
||||||
|
bar.AddChild(_statusLabel);
|
||||||
|
|
||||||
|
_hintLabel = new Label
|
||||||
|
{
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Right,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center
|
||||||
|
};
|
||||||
|
_hintLabel.AddThemeFontSizeOverride("font_size", UiTokens.FontSmall);
|
||||||
|
_hintLabel.AddThemeColorOverride("font_color", UiTokens.Faint);
|
||||||
|
bar.AddChild(_hintLabel);
|
||||||
|
|
||||||
|
SetStatus(_pendingStatus, _pendingStatusKind);
|
||||||
|
SetHint(_pendingHint);
|
||||||
|
|
||||||
|
var panel = new PanelContainer();
|
||||||
|
panel.AddChild(bar);
|
||||||
|
var style = new StyleBoxFlat
|
||||||
|
{
|
||||||
|
BgColor = UiTokens.Surface2,
|
||||||
|
BorderColor = UiTokens.Line,
|
||||||
|
BorderWidthTop = 1,
|
||||||
|
CornerRadiusBottomLeft = UiTokens.RadiusLg,
|
||||||
|
CornerRadiusBottomRight = UiTokens.RadiusLg,
|
||||||
|
ContentMarginLeft = UiTokens.Space5,
|
||||||
|
ContentMarginRight = UiTokens.Space5,
|
||||||
|
};
|
||||||
|
panel.AddThemeStyleboxOverride("panel", style);
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Button CreateTitleButton(string text, Action pressed, Color hoverBg, Color hoverText)
|
||||||
{
|
{
|
||||||
var btn = new Button
|
var btn = new Button
|
||||||
{
|
{
|
||||||
Text = text,
|
Text = text,
|
||||||
CustomMinimumSize = new Vector2(36, 28),
|
CustomMinimumSize = new Vector2(TITLE_BUTTON_SIZE, TITLE_BUTTON_SIZE),
|
||||||
SizeFlagsHorizontal = Control.SizeFlags.ShrinkEnd,
|
SizeFlagsHorizontal = Control.SizeFlags.ShrinkEnd,
|
||||||
SizeFlagsVertical = Control.SizeFlags.ShrinkCenter,
|
SizeFlagsVertical = Control.SizeFlags.ShrinkCenter,
|
||||||
Flat = true
|
Flat = true,
|
||||||
|
FocusMode = Control.FocusModeEnum.None
|
||||||
};
|
};
|
||||||
|
|
||||||
btn.AddThemeColorOverride("font_color", UiTokens.Ink);
|
btn.AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||||
btn.AddThemeColorOverride("font_hover_color", UiTokens.InkBright);
|
btn.AddThemeColorOverride("font_hover_color", hoverText);
|
||||||
btn.AddThemeColorOverride("font_hover_pressed_color", UiTokens.InkBright);
|
btn.AddThemeColorOverride("font_pressed_color", hoverText);
|
||||||
btn.AddThemeFontSizeOverride("font_size", UiTokens.FontBodyLarge);
|
btn.AddThemeColorOverride("font_hover_pressed_color", hoverText);
|
||||||
|
btn.AddThemeFontSizeOverride("font_size", UiTokens.FontLead);
|
||||||
|
|
||||||
var normalStyle = new StyleBoxFlat { BgColor = Colors.Transparent };
|
var normalStyle = new StyleBoxFlat { BgColor = Colors.Transparent };
|
||||||
btn.AddThemeStyleboxOverride("normal", normalStyle);
|
btn.AddThemeStyleboxOverride("normal", normalStyle);
|
||||||
|
|
||||||
var hoverStyle = new StyleBoxFlat { BgColor = hoverColor };
|
var hoverStyle = new StyleBoxFlat
|
||||||
|
{
|
||||||
|
BgColor = hoverBg,
|
||||||
|
CornerRadiusTopLeft = UiTokens.RadiusSm,
|
||||||
|
CornerRadiusTopRight = UiTokens.RadiusSm,
|
||||||
|
CornerRadiusBottomLeft = UiTokens.RadiusSm,
|
||||||
|
CornerRadiusBottomRight = UiTokens.RadiusSm
|
||||||
|
};
|
||||||
btn.AddThemeStyleboxOverride("hover", hoverStyle);
|
btn.AddThemeStyleboxOverride("hover", hoverStyle);
|
||||||
btn.AddThemeStyleboxOverride("pressed", hoverStyle);
|
btn.AddThemeStyleboxOverride("pressed", hoverStyle);
|
||||||
|
btn.AddThemeStyleboxOverride("focus", new StyleBoxEmpty());
|
||||||
btn.AddThemeStyleboxOverride("focus", normalStyle);
|
|
||||||
|
|
||||||
btn.Pressed += pressed;
|
btn.Pressed += pressed;
|
||||||
return btn;
|
return btn;
|
||||||
@@ -389,27 +601,21 @@ namespace Cthangover.Mods.Interface.Tool
|
|||||||
_restorePosition = Position;
|
_restorePosition = Position;
|
||||||
_restoreSize = Size;
|
_restoreSize = Size;
|
||||||
_isMaximized = true;
|
_isMaximized = true;
|
||||||
_maximizeBtn.Text = "\u2750";
|
_maximizeBtn.Text = "❐";
|
||||||
|
|
||||||
Position = Vector2I.Zero;
|
Position = Vector2I.Zero;
|
||||||
Size = (Vector2I)GetGameViewportSize();
|
Size = (Vector2I)GetGameViewportSize();
|
||||||
GameLogger.Log("TOOL_WINDOW", $"MaximizeWindow gameViewport=({GetGameViewportSize().X:F0},{GetGameViewportSize().Y:F0}) physWindow=({DisplayServer.WindowGetSize().X},{DisplayServer.WindowGetSize().Y}) newSize=({Size.X},{Size.Y})");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RestoreWindow()
|
private void RestoreWindow()
|
||||||
{
|
{
|
||||||
_isMaximized = false;
|
_isMaximized = false;
|
||||||
_maximizeBtn.Text = "\u25A1";
|
_maximizeBtn.Text = "□";
|
||||||
|
|
||||||
Position = _restorePosition;
|
Position = _restorePosition;
|
||||||
Size = _restoreSize;
|
Size = _restoreSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnClose()
|
|
||||||
{
|
|
||||||
HandleCloseRequested();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── title bar drag ──────────────────────────────────────────
|
// ── title bar drag ──────────────────────────────────────────
|
||||||
|
|
||||||
private void OnTitleBarInput(InputEvent evt)
|
private void OnTitleBarInput(InputEvent evt)
|
||||||
@@ -429,7 +635,7 @@ namespace Cthangover.Mods.Interface.Tool
|
|||||||
if (_isMaximized)
|
if (_isMaximized)
|
||||||
{
|
{
|
||||||
_isMaximized = false;
|
_isMaximized = false;
|
||||||
_maximizeBtn.Text = "\u25A1";
|
_maximizeBtn.Text = "□";
|
||||||
var pos = DisplayServer.MouseGetPosition();
|
var pos = DisplayServer.MouseGetPosition();
|
||||||
var viewportSize = GetGameViewportSize();
|
var viewportSize = GetGameViewportSize();
|
||||||
var ratio = pos.X / viewportSize.X;
|
var ratio = pos.X / viewportSize.X;
|
||||||
@@ -437,6 +643,7 @@ namespace Cthangover.Mods.Interface.Tool
|
|||||||
Mathf.RoundToInt(pos.X - _restoreSize.X * ratio),
|
Mathf.RoundToInt(pos.X - _restoreSize.X * ratio),
|
||||||
Mathf.RoundToInt(pos.Y - 4));
|
Mathf.RoundToInt(pos.Y - 4));
|
||||||
_titleDragMouseStart = new Vector2(pos.X, pos.Y);
|
_titleDragMouseStart = new Vector2(pos.X, pos.Y);
|
||||||
|
Size = _restoreSize;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -452,7 +659,7 @@ namespace Cthangover.Mods.Interface.Tool
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (evt is InputEventMouseMotion mm && _draggingTitle)
|
else if (evt is InputEventMouseMotion && _draggingTitle)
|
||||||
{
|
{
|
||||||
var mp = DisplayServer.MouseGetPosition();
|
var mp = DisplayServer.MouseGetPosition();
|
||||||
var delta = new Vector2(mp.X, mp.Y) - _titleDragMouseStart;
|
var delta = new Vector2(mp.X, mp.Y) - _titleDragMouseStart;
|
||||||
@@ -589,49 +796,23 @@ namespace Cthangover.Mods.Interface.Tool
|
|||||||
private void ApplyResize(Vector2 globalMouse)
|
private void ApplyResize(Vector2 globalMouse)
|
||||||
{
|
{
|
||||||
var delta = globalMouse - _resizeMouseStart;
|
var delta = globalMouse - _resizeMouseStart;
|
||||||
|
var dx = Mathf.RoundToInt(delta.X);
|
||||||
|
var dy = Mathf.RoundToInt(delta.Y);
|
||||||
|
|
||||||
int newX = _resizePosStart.X;
|
int newX = _resizePosStart.X;
|
||||||
int newY = _resizePosStart.Y;
|
int newY = _resizePosStart.Y;
|
||||||
int newW = _resizeSizeStart.X;
|
int newW = _resizeSizeStart.X;
|
||||||
int newH = _resizeSizeStart.Y;
|
int newH = _resizeSizeStart.Y;
|
||||||
|
|
||||||
switch (_resizeEdge)
|
var left = _resizeEdge is ResizeEdge.Left or ResizeEdge.TopLeft or ResizeEdge.BottomLeft;
|
||||||
{
|
var right = _resizeEdge is ResizeEdge.Right or ResizeEdge.TopRight or ResizeEdge.BottomRight;
|
||||||
case ResizeEdge.Left:
|
var top = _resizeEdge is ResizeEdge.Top or ResizeEdge.TopLeft or ResizeEdge.TopRight;
|
||||||
newX = _resizePosStart.X + Mathf.RoundToInt(delta.X);
|
var bottom = _resizeEdge is ResizeEdge.Bottom or ResizeEdge.BottomLeft or ResizeEdge.BottomRight;
|
||||||
newW = _resizeSizeStart.X - Mathf.RoundToInt(delta.X);
|
|
||||||
break;
|
if (left) { newX += dx; newW -= dx; }
|
||||||
case ResizeEdge.Right:
|
if (right) newW += dx;
|
||||||
newW = _resizeSizeStart.X + Mathf.RoundToInt(delta.X);
|
if (top) { newY += dy; newH -= dy; }
|
||||||
break;
|
if (bottom) newH += dy;
|
||||||
case ResizeEdge.Top:
|
|
||||||
newY = _resizePosStart.Y + Mathf.RoundToInt(delta.Y);
|
|
||||||
newH = _resizeSizeStart.Y - Mathf.RoundToInt(delta.Y);
|
|
||||||
break;
|
|
||||||
case ResizeEdge.Bottom:
|
|
||||||
newH = _resizeSizeStart.Y + Mathf.RoundToInt(delta.Y);
|
|
||||||
break;
|
|
||||||
case ResizeEdge.TopLeft:
|
|
||||||
newX = _resizePosStart.X + Mathf.RoundToInt(delta.X);
|
|
||||||
newY = _resizePosStart.Y + Mathf.RoundToInt(delta.Y);
|
|
||||||
newW = _resizeSizeStart.X - Mathf.RoundToInt(delta.X);
|
|
||||||
newH = _resizeSizeStart.Y - Mathf.RoundToInt(delta.Y);
|
|
||||||
break;
|
|
||||||
case ResizeEdge.TopRight:
|
|
||||||
newY = _resizePosStart.Y + Mathf.RoundToInt(delta.Y);
|
|
||||||
newW = _resizeSizeStart.X + Mathf.RoundToInt(delta.X);
|
|
||||||
newH = _resizeSizeStart.Y - Mathf.RoundToInt(delta.Y);
|
|
||||||
break;
|
|
||||||
case ResizeEdge.BottomLeft:
|
|
||||||
newX = _resizePosStart.X + Mathf.RoundToInt(delta.X);
|
|
||||||
newW = _resizeSizeStart.X - Mathf.RoundToInt(delta.X);
|
|
||||||
newH = _resizeSizeStart.Y + Mathf.RoundToInt(delta.Y);
|
|
||||||
break;
|
|
||||||
case ResizeEdge.BottomRight:
|
|
||||||
newW = _resizeSizeStart.X + Mathf.RoundToInt(delta.X);
|
|
||||||
newH = _resizeSizeStart.Y + Mathf.RoundToInt(delta.Y);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
newX = Mathf.Clamp(newX, 0, MaxSize.X - 100);
|
newX = Mathf.Clamp(newX, 0, MaxSize.X - 100);
|
||||||
newY = Mathf.Clamp(newY, 0, MaxSize.Y - 100);
|
newY = Mathf.Clamp(newY, 0, MaxSize.Y - 100);
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
Reference in new issue
Block a user