From 38ed8b68d6f68155a696cbed5d2f444be2aa727d Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Tue, 28 Jul 2026 18:40:00 -0400 Subject: [PATCH 01/55] Editor icons: all 304 of the Mono set, at four sizes, behind names editorIcons16 was 60 icons in a 32x2 grid indexed by raw number. Only 29 of those 60 came from the Mono set the art is drawn from -- the rest were foreign -- and none had a variant above 16px, so a button could never be given a bigger icon than the one slot it started in. Rebuilt as four sheets, editorIcons16/24/32/48, each a 32x10 grid of the same 304 icons in the same order. A frame index names the same picture at every size, so a control can change size without changing its Frame. 16 cells spare. A 24px or 48px cell grid can never be power-of-two -- both carry a factor of 3 -- and TextureManager pads non-POT bitmaps to POT on upload. That is safe here: ImageAsset takes cell positions from the original bitmap and divides by the padded getTextureWidth(), and createPowerOfTwoBitmap puts the content at the top-left, so the UVs come out right. Frame numbers are now $EditorIcon:: constants from the generated EditorIcons.cs, exec'd first thing in EditorCore::create. Indices are alphabetical, so adding an icon reflows them -- which costs nothing through a constant and silently draws the wrong picture through a raw number. Six concepts in the old sheet had no Mono equivalent. Five were dead. The one in use was the particle graph's zoom pair, which now wears a squared plus and minus: the set has a magnifier but no +/- variants of it, and the squared pair stays distinct from the round plus and minus that mean add and remove everywhere else. tests/shots/iconSheet.cs now walks all 304 icons across all four sheets. All 19 smoke suites pass. Co-Authored-By: Claude Opus 5 (1M context) --- editor/AssetAdmin/AssetInspector.cs | 10 +- .../ImageEditor/AssetImageFrameEditRow.cs | 6 +- .../ImageEditor/AssetImageLayersEditRow.cs | 6 +- .../ParticleEditor/AssetParticleGraphUnit.cs | 21 +- editor/EditorCore/EditorCore.cs | 4 + editor/EditorCore/EditorIcons.cs | 321 ++++++++++++++++++ .../images/editorIcons16.asset.taml | 2 +- editor/EditorCore/images/editorIcons16.png | Bin 9726 -> 52811 bytes .../images/editorIcons24.asset.taml | 8 + editor/EditorCore/images/editorIcons24.png | Bin 0 -> 91069 bytes .../images/editorIcons32.asset.taml | 8 + editor/EditorCore/images/editorIcons32.png | Bin 0 -> 118357 bytes .../images/editorIcons48.asset.taml | 8 + editor/EditorCore/images/editorIcons48.png | Bin 0 -> 178293 bytes .../GuiEditor/scripts/GuiEditorToolsWindow.cs | 4 +- .../scripts/GuiProfileEditorDialog.cs | 18 +- .../scripts/GuiProfileEditorFieldRow.cs | 8 +- .../scripts/GuiProfileEditorStateColorRow.cs | 6 +- .../scripts/ProjectGamePanel.cs | 4 +- tests/shots/iconSheet.cs | 89 +++-- 20 files changed, 457 insertions(+), 66 deletions(-) create mode 100644 editor/EditorCore/EditorIcons.cs create mode 100644 editor/EditorCore/images/editorIcons24.asset.taml create mode 100644 editor/EditorCore/images/editorIcons24.png create mode 100644 editor/EditorCore/images/editorIcons32.asset.taml create mode 100644 editor/EditorCore/images/editorIcons32.png create mode 100644 editor/EditorCore/images/editorIcons48.asset.taml create mode 100644 editor/EditorCore/images/editorIcons48.png diff --git a/editor/AssetAdmin/AssetInspector.cs b/editor/AssetAdmin/AssetInspector.cs index 882bd8d04..0b38f4d87 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,10 @@ }; 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"); %this.tabBook = new GuiTabBookCtrl() { 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/AssetImageLayersEditRow.cs b/editor/AssetAdmin/ImageEditor/AssetImageLayersEditRow.cs index f4fa80740..58f1226d0 100644 --- a/editor/AssetAdmin/ImageEditor/AssetImageLayersEditRow.cs +++ b/editor/AssetAdmin/ImageEditor/AssetImageLayersEditRow.cs @@ -91,9 +91,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 { diff --git a/editor/AssetAdmin/ParticleEditor/AssetParticleGraphUnit.cs b/editor/AssetAdmin/ParticleEditor/AssetParticleGraphUnit.cs index 634917e0d..f579f6b00 100644 --- a/editor/AssetAdmin/ParticleEditor/AssetParticleGraphUnit.cs +++ b/editor/AssetAdmin/ParticleEditor/AssetParticleGraphUnit.cs @@ -11,12 +11,15 @@ ThemeManager.setProfile(%this.graph, "graphProfile"); %this.add(%this.graph); - //Value zoom buttons + // 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; + Frame = $EditorIcon::sq_plus; Position = "2" SPC (%center + 13); Command = %this.getId() @ ".valueZoomIn();"; Tooltip = "Zoom In"; @@ -27,7 +30,7 @@ %this.valueZoomOutButton = new GuiButtonCtrl() { Class = "EditorIconButton"; - Frame = 1; + Frame = $EditorIcon::sq_minus; Position = "2" SPC (%center - 13); Command = %this.getId() @ ".valueZoomOut();"; Tooltip = "Zoom Out"; @@ -39,7 +42,7 @@ %this.valueMoveUpButton = new GuiButtonCtrl() { Class = "EditorIconButton"; - Frame = 2; + Frame = $EditorIcon::arrow_top; Position = "2 18"; Command = %this.getId() @ ".valueMoveUp();"; Tooltip = "Move Graph Up"; @@ -50,7 +53,7 @@ %this.valueMoveDownButton = new GuiButtonCtrl() { Class = "EditorIconButton"; - Frame = 6; + Frame = $EditorIcon::arrow_bottom; Position = "2" SPC (getWord(%this.extent, 1) - 66); Command = %this.getId() @ ".valueMoveDown();"; Tooltip = "Move Graph Down"; @@ -73,7 +76,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 +87,7 @@ %this.timeZoomOutButton = new GuiButtonCtrl() { Class = "EditorIconButton"; - Frame = 1; + Frame = $EditorIcon::sq_minus; Position = "26 0"; Command = %this.getId() @ ".timeZoomOut();"; Tooltip = "Zoom Out"; @@ -96,7 +99,7 @@ %this.timeMoveBackButton = new GuiButtonCtrl() { Class = "EditorIconButton"; - Frame = 8; + Frame = $EditorIcon::arrow_left; HorizSizing = "right"; Position = "30" SPC %bottom; Command = %this.getId() @ ".timeMoveBack();"; @@ -108,7 +111,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();"; diff --git a/editor/EditorCore/EditorCore.cs b/editor/EditorCore/EditorCore.cs index 97f14e468..e5f89b52b 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)) { diff --git a/editor/EditorCore/EditorIcons.cs b/editor/EditorCore/EditorIcons.cs new file mode 100644 index 000000000..78b59a339 --- /dev/null +++ b/editor/EditorCore/EditorIcons.cs @@ -0,0 +1,321 @@ +//----------------------------------------------------------------------------- +// 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_center = 1; +$EditorIcon::align_just = 2; +$EditorIcon::align_left = 3; +$EditorIcon::align_right = 4; +$EditorIcon::app_window = 5; +$EditorIcon::app_window_alt = 6; +$EditorIcon::app_window_black = 7; +$EditorIcon::app_window_black_alt = 8; +$EditorIcon::app_window_cross = 9; +$EditorIcon::app_window_cross_alt = 10; +$EditorIcon::app_window_shell = 11; +$EditorIcon::app_window_shell_alt = 12; +$EditorIcon::arrow_bottom = 13; +$EditorIcon::arrow_bottom_left = 14; +$EditorIcon::arrow_bottom_rigth = 15; +$EditorIcon::arrow_l = 16; +$EditorIcon::arrow_left = 17; +$EditorIcon::arrow_r = 18; +$EditorIcon::arrow_right = 19; +$EditorIcon::arrow_top = 20; +$EditorIcon::arrow_top_left = 21; +$EditorIcon::arrow_top_right = 22; +$EditorIcon::arrow_two_head = 23; +$EditorIcon::arrow_two_head_2 = 24; +$EditorIcon::attention = 25; +$EditorIcon::balance = 26; +$EditorIcon::battery = 27; +$EditorIcon::bell = 28; +$EditorIcon::book = 29; +$EditorIcon::book_side = 30; +$EditorIcon::bookmark_1 = 31; +$EditorIcon::bookmark_2 = 32; +$EditorIcon::box = 33; +$EditorIcon::br_down = 34; +$EditorIcon::br_next = 35; +$EditorIcon::br_prev = 36; +$EditorIcon::br_up = 37; +$EditorIcon::brackets = 38; +$EditorIcon::browser = 39; +$EditorIcon::brush = 40; +$EditorIcon::bug = 41; +$EditorIcon::burst = 42; +$EditorIcon::calc = 43; +$EditorIcon::calendar_1 = 44; +$EditorIcon::calendar_2 = 45; +$EditorIcon::cancel = 46; +$EditorIcon::case = 47; +$EditorIcon::cassette = 48; +$EditorIcon::cc = 49; +$EditorIcon::cert = 50; +$EditorIcon::chart_bar = 51; +$EditorIcon::chart_line = 52; +$EditorIcon::chart_line_2 = 53; +$EditorIcon::chart_pie = 54; +$EditorIcon::chat_bubble_message_square = 55; +$EditorIcon::checkbox_checked = 56; +$EditorIcon::checkbox_unchecked = 57; +$EditorIcon::checkmark = 58; +$EditorIcon::clip = 59; +$EditorIcon::clipboard_copy = 60; +$EditorIcon::clipboard_cut = 61; +$EditorIcon::clipboard_past = 62; +$EditorIcon::clock = 63; +$EditorIcon::cloud = 64; +$EditorIcon::cloud_rain = 65; +$EditorIcon::coffe_cup = 66; +$EditorIcon::cog = 67; +$EditorIcon::cogs = 68; +$EditorIcon::comp = 69; +$EditorIcon::compass = 70; +$EditorIcon::connect = 71; +$EditorIcon::contact = 72; +$EditorIcon::contact_card = 73; +$EditorIcon::cube = 74; +$EditorIcon::cur_bp = 75; +$EditorIcon::cur_dollar = 76; +$EditorIcon::cur_euro = 77; +$EditorIcon::cur_yen = 78; +$EditorIcon::cursor_H_split = 79; +$EditorIcon::cursor_V_split = 80; +$EditorIcon::cursor_arrow = 81; +$EditorIcon::cursor_drag_arrow = 82; +$EditorIcon::cursor_drag_arrow_2 = 83; +$EditorIcon::cursor_drag_hand = 84; +$EditorIcon::cursor_hand = 85; +$EditorIcon::dashboard = 86; +$EditorIcon::db = 87; +$EditorIcon::delete = 88; +$EditorIcon::delete_server = 89; +$EditorIcon::disconnected = 90; +$EditorIcon::doc_delete = 91; +$EditorIcon::doc_edit = 92; +$EditorIcon::doc_empty = 93; +$EditorIcon::doc_export = 94; +$EditorIcon::doc_import = 95; +$EditorIcon::doc_lines = 96; +$EditorIcon::doc_lines_stright = 97; +$EditorIcon::doc_minus = 98; +$EditorIcon::doc_new = 99; +$EditorIcon::doc_plus = 100; +$EditorIcon::document = 101; +$EditorIcon::download = 102; +$EditorIcon::eject = 103; +$EditorIcon::emotion_sad = 104; +$EditorIcon::emotion_smile = 105; +$EditorIcon::expand = 106; +$EditorIcon::export = 107; +$EditorIcon::eye = 108; +$EditorIcon::eye_inv = 109; +$EditorIcon::facebook = 110; +$EditorIcon::fastforward_next = 111; +$EditorIcon::fill = 112; +$EditorIcon::filter = 113; +$EditorIcon::fire = 114; +$EditorIcon::flag = 115; +$EditorIcon::flag_2 = 116; +$EditorIcon::folder = 117; +$EditorIcon::folder_arrow = 118; +$EditorIcon::folder_delete = 119; +$EditorIcon::folder_minus = 120; +$EditorIcon::folder_open = 121; +$EditorIcon::folder_plus = 122; +$EditorIcon::font_bold = 123; +$EditorIcon::font_italic = 124; +$EditorIcon::font_size = 125; +$EditorIcon::font_strokethrough = 126; +$EditorIcon::font_underline = 127; +$EditorIcon::game_pad = 128; +$EditorIcon::glasses = 129; +$EditorIcon::globe_1 = 130; +$EditorIcon::globe_2 = 131; +$EditorIcon::globe_3 = 132; +$EditorIcon::google = 133; +$EditorIcon::grid_2x2 = 134; +$EditorIcon::grid_3x3 = 135; +$EditorIcon::grid_3x3_2 = 136; +$EditorIcon::hand_1 = 137; +$EditorIcon::hand_2 = 138; +$EditorIcon::hand_contra = 139; +$EditorIcon::hand_pro = 140; +$EditorIcon::hanger = 141; +$EditorIcon::headphones = 142; +$EditorIcon::heart = 143; +$EditorIcon::heart_empty = 144; +$EditorIcon::home = 145; +$EditorIcon::image_text = 146; +$EditorIcon::import = 147; +$EditorIcon::inbox = 148; +$EditorIcon::indent_decrease = 149; +$EditorIcon::indent_increase = 150; +$EditorIcon::info = 151; +$EditorIcon::inject = 152; +$EditorIcon::invisible_light = 153; +$EditorIcon::invisible_revert = 154; +$EditorIcon::iphone = 155; +$EditorIcon::key = 156; +$EditorIcon::layers_1 = 157; +$EditorIcon::layers_2 = 158; +$EditorIcon::lightbulb = 159; +$EditorIcon::lighting = 160; +$EditorIcon::link = 161; +$EditorIcon::list_bullets = 162; +$EditorIcon::list_num = 163; +$EditorIcon::loading_throbber = 164; +$EditorIcon::lock_open = 165; +$EditorIcon::magic_wand = 166; +$EditorIcon::magic_wand_2 = 167; +$EditorIcon::mail = 168; +$EditorIcon::mail_2 = 169; +$EditorIcon::message_attention = 170; +$EditorIcon::mic = 171; +$EditorIcon::microphone = 172; +$EditorIcon::money = 173; +$EditorIcon::monitor = 174; +$EditorIcon::movie = 175; +$EditorIcon::music = 176; +$EditorIcon::music_square = 177; +$EditorIcon::net_comp = 178; +$EditorIcon::network = 179; +$EditorIcon::not_connected = 180; +$EditorIcon::notepad = 181; +$EditorIcon::notepad_2 = 182; +$EditorIcon::off = 183; +$EditorIcon::on = 184; +$EditorIcon::on_off = 185; +$EditorIcon::openid = 186; +$EditorIcon::padlock_closed = 187; +$EditorIcon::padlock_open = 188; +$EditorIcon::page_layout = 189; +$EditorIcon::paper_airplane = 190; +$EditorIcon::paragraph = 191; +$EditorIcon::pencil = 192; +$EditorIcon::phone = 193; +$EditorIcon::phone_1 = 194; +$EditorIcon::phone_2 = 195; +$EditorIcon::phone_touch = 196; +$EditorIcon::photo = 197; +$EditorIcon::picture = 198; +$EditorIcon::pin = 199; +$EditorIcon::pin_2 = 200; +$EditorIcon::pin_map = 201; +$EditorIcon::pin_map_down = 202; +$EditorIcon::pin_map_left = 203; +$EditorIcon::pin_map_right = 204; +$EditorIcon::pin_map_top = 205; +$EditorIcon::pin_sq_down = 206; +$EditorIcon::pin_sq_left = 207; +$EditorIcon::pin_sq_right = 208; +$EditorIcon::pin_sq_top = 209; +$EditorIcon::playback_ff = 210; +$EditorIcon::playback_next = 211; +$EditorIcon::playback_pause = 212; +$EditorIcon::playback_play = 213; +$EditorIcon::playback_prev = 214; +$EditorIcon::playback_rec = 215; +$EditorIcon::playback_reload = 216; +$EditorIcon::playback_rew = 217; +$EditorIcon::playback_stop = 218; +$EditorIcon::podcast = 219; +$EditorIcon::preso = 220; +$EditorIcon::print = 221; +$EditorIcon::push_pin = 222; +$EditorIcon::redo = 223; +$EditorIcon::refresh = 224; +$EditorIcon::reload = 225; +$EditorIcon::rewind_previous = 226; +$EditorIcon::rnd_br_down = 227; +$EditorIcon::rnd_br_first = 228; +$EditorIcon::rnd_br_last = 229; +$EditorIcon::rnd_br_next = 230; +$EditorIcon::rnd_br_prev = 231; +$EditorIcon::rnd_br_up = 232; +$EditorIcon::round = 233; +$EditorIcon::round_and_up = 234; +$EditorIcon::round_arrow_left = 235; +$EditorIcon::round_arrow_right = 236; +$EditorIcon::round_checkmark = 237; +$EditorIcon::round_delete = 238; +$EditorIcon::round_minus = 239; +$EditorIcon::round_plus = 240; +$EditorIcon::rss = 241; +$EditorIcon::rss_sq = 242; +$EditorIcon::sand = 243; +$EditorIcon::sat_dish = 244; +$EditorIcon::save = 245; +$EditorIcon::server = 246; +$EditorIcon::shapes = 247; +$EditorIcon::share = 248; +$EditorIcon::share_2 = 249; +$EditorIcon::shield = 250; +$EditorIcon::shield_2 = 251; +$EditorIcon::shop_cart = 252; +$EditorIcon::shopping_bag = 253; +$EditorIcon::shopping_bag_dollar = 254; +$EditorIcon::sound_high = 255; +$EditorIcon::sound_low = 256; +$EditorIcon::sound_mute = 257; +$EditorIcon::spechbubble = 258; +$EditorIcon::spechbubble_2 = 259; +$EditorIcon::spechbubble_sq = 260; +$EditorIcon::spechbubble_sq_line = 261; +$EditorIcon::sq_br_down = 262; +$EditorIcon::sq_br_first = 263; +$EditorIcon::sq_br_last = 264; +$EditorIcon::sq_br_next = 265; +$EditorIcon::sq_br_prev = 266; +$EditorIcon::sq_br_up = 267; +$EditorIcon::sq_down = 268; +$EditorIcon::sq_minus = 269; +$EditorIcon::sq_next = 270; +$EditorIcon::sq_plus = 271; +$EditorIcon::sq_prev = 272; +$EditorIcon::sq_up = 273; +$EditorIcon::square_shape = 274; +$EditorIcon::stairs_down = 275; +$EditorIcon::stairs_up = 276; +$EditorIcon::star = 277; +$EditorIcon::star_fav = 278; +$EditorIcon::star_fav_empty = 279; +$EditorIcon::stop_watch = 280; +$EditorIcon::sun = 281; +$EditorIcon::tag = 282; +$EditorIcon::tape = 283; +$EditorIcon::target = 284; +$EditorIcon::text_curstor = 285; +$EditorIcon::text_letter_t = 286; +$EditorIcon::top_right_expand = 287; +$EditorIcon::track = 288; +$EditorIcon::trash = 289; +$EditorIcon::twitter = 290; +$EditorIcon::twitter_2 = 291; +$EditorIcon::undo = 292; +$EditorIcon::user = 293; +$EditorIcon::users = 294; +$EditorIcon::vault = 295; +$EditorIcon::wallet = 296; +$EditorIcon::wifi_router = 297; +$EditorIcon::wireless_signal = 298; +$EditorIcon::wrench = 299; +$EditorIcon::wrench_plus = 300; +$EditorIcon::wrench_plus_2 = 301; +$EditorIcon::youtube = 302; +$EditorIcon::zoom = 303; 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 1cdac4710527008335220553719b87fb8ba40062..f334115f5f59ed53a01e4f9fa0e4ef36153a8d8e 100644 GIT binary patch literal 52811 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 z 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 literal 9726 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 diff --git a/editor/EditorCore/images/editorIcons24.png b/editor/EditorCore/images/editorIcons24.png new file mode 100644 index 0000000000000000000000000000000000000000..bbd178c9d4133249b5d887488ab0785d0c8ce38d GIT binary patch literal 91069 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fe235985244628d8902238580b3545b21cb41b9c GIT binary patch literal 118357 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
v0V!C5Ou!=Eh>y4?hQU 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≀+1O; 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4c4cc00d604ff59ced14c5c1718d3043bf10a3b7 GIT binary patch literal 178293 zcmYIwWk6I>xAqwrN@kik?hjK^B$qmg^Hk!`23vGP#~IiOPR|9dhGYwM18r+SUjD=amnh zA}_`jb(KKk)VZOz2O z=lXi(KVsuu@NmhAGI_!Z&Oxkf!E-FB_V#4_~h6ai|R)by{O?45y)sih;Gnv zrCx=X;{PwlkpN{+!V&{+O;=b}Rwmp6zlK5ky1Kd}v$K7X{Mm2PU$C=8oFd`{2%)$g z9tLLemjxspJaBC?@fv2_)uvo$c=Fwa)tV>4i`I)K8 z{sctYh4x^J|c7I`Hxtx zUtWVX7_Fxg8c~aVv~%Ewrj`4w@>sb8DD_6xhHoVcIgL7=8q~gHOQn9RT0)X&fJ&1| zskTZLzx3cyBDQMzJ9&n$lLoP%JnKgZ|3clzel0_*hz=lJ9# z3hQlLj*5x1g9F#8^UpHKlD^oJnB-+i0y{P|wpw8NrdFMSk&!*`^Yr9oM%=tD6IxZb zro4K*vr~zIIgPN-KeyiF!J+{JH0NCyd+**ofs)vf@$p9(P{ib<0jyporky+%JSz2~ z$AtDKCns6p{j6I-Y1#8%upDxIf8WXQalPAyfiQ-qBhUypPhwY?_0abu{r_A4`{A#z zU*q)K83zRg!J}?;u!_F^!K1KiQk&VIKbwSjapg`euy16o=L%`{kFXd>){2eQ`i^ zFf4anaSeXcf@ivtf@c^VL{DaaY(@nKs6E-on+s`fZ`VyF^|!UQySloPrB@7MBP*4~ zuB@!2Ly3>afvvV7nmjxi9_$8hZfFEzTbEbp#_y$B8- zkdu|wSWo>h$FyesAu%!03H&(A5B(rV`D+_sH$(`S3!DzdRCJAEpS_sd#Lq2U&h-_7 z_hw7Upd!J6fn7Q!Q(WLc@L?EbE3Rt)*t*48|+ZM|TB|D;Tam_)J8#l_`0u}rbT zF%#Ca|2tKdb5Z_${n`_(J7?9@hc#pxpBoz~85tNzmETdn0IK=lGv4k0C*?E$>(?)1 z*?vhWDZQTJ^D(sG^E3|jBI!}%>&p|1S1n4-qiJWSd#pGlXG27K(TT`5j^?<&`uckK z^Y*!4zuuB42GClny{oHBy&WyHY(E(`OiiVSda7LbNKs8EKXV)ilaDLgEl5RCO|9Fx z3-5=Z-C!6!GpVr&T5sKWa;a*;`?>xX)$+!+X;!Fuc5F=laGb#-Jh#?^fq_BPXL6W( z4Jg|^8_ot8vKT&Z6({z0cQ=)up1uW-`A#>L8&d>TZ!|F*TCO-MI~N1XlB+|knEwFC z>m{R<07`aYVF43$Dx}zhKL4*$$eDyPPA$}I==rk~ND81dk~j$=THq>rfu$JwOfkxZ z*i-ABfj`KL3u(%7;kE-MW#S%u1m*eK<|f`nf^z%UubPKrytKr;cE>Wv&yTX3s!sME=FOwsot;(I1W0B_ z$VO?BAF2)s4Q-zu9o0iOna&DgxqVl6OE29CHWq`b$e<+uz`$jQ$*ELRPDbW!!_e;C z&uVi$Z2$VVh?3#U{kahEJ8L=DKvAcDZftDGdop7|e8^CtAxt7MM<&MxKW}eb;I6n* z4wQ^0n|n{Wt1pNvpZu?hx1St{Xf`%BW=xfR{CHbLJzQsg5Xz{myjOzD4Iu_z6EQZ~ zbDVb*={?#;w(;$#2$_)yj&DXCXc-z(QjWY=SF0sQDwW~GrD=p&a9Q3zRm80+B-99` zpbiZR`iea)gDYreW)}JL=TkiA``$i2Hd-5=y`MgPDvn%t2XD+ou?pMZV?yjl>4$;qJ>q^Ww?qdB13_(+U(EHj_IC!n==N|0vIDt z?QRCNsOjf-Qh{f7f9BUOvmec$;gH4GuV1}tcMYJ$x=}B4sVbxw9z917Nxr@WV=Nl~ zw9c}s>kvaa^I^mC4lz_HU;M)uIwiYu`czr&G*hEC$Hd`Gw=C@02_vrCl2?pTI*24g z_!9**KOdc#h^+r~yIK&5^pe!XxPUoOe*N;L3z0Z}-14ngO-*Io`J125K#qRjCj167+@JKxO4R+Dx;iWDi%iagtIMGK zvqSR-;g5_(NCL7wwv7X_x3`@dod)d!0|UkR@|s-5*)Hxj<_bS^_gAy-%@B**$YA;W ztBd+mRh4nXLp+G$%h#^~)N*yu!*5i4hlht~UwR1K)gxl$E;Qil=XaZXIP1^mCJD>m zhijSK;)aTkceb`-u$ovRq5DV0BV!Fa)2yyZk3`1$+P?~Hi>G=QV7M(R$Ixk5K4GlAfljrBne?+V?`KL@c z_K)zTYZ%-ddR<)|9sDB+kJ9O`kRm7H)kHWS2C{etH7IMzabQAJZ+tV?oY8C8%zX%G-5i6SVRJHMU-rpgG zZeml`le3?9XEKd7B$9SOq;fQE#D=2t^YgLN+tZ;%C*bw&SFC5h489X}g?u{Fxq>Do z^9iHaS$?nJlzUrL@Xeq9m79MaJ?%rDOO^zIjPQ#%7WN1jL;@1IYZ8^!s<5BJgiyqY z^TQ4?3EbBLu6>>?|C9eTO)ix8r1ke}_OCy%uwZl?6C{@Ib!~vh+zPYA0jkki%ZtEbZ{g|DNG0osG`m5~uQ4V$EClu`mw_3%Z=xslVKKY4G{Fnj0c=ZlBD+K!Hoe?PwZ3mh5)czFAtZ*D1nM+K79+@kNKuY>ICc+41(fOR!{KX_ z9#toX|M42Hj{F=427g2D)fbq$wWit?IsN z&0u@s&sdDCtSV!e;Y1#vKd#s~-RI+L#l8+5d|OqO==JoKl*p^Xq@3N@?GTbQ2XFZyNazI`fy``XVLUBG}-`UeTGbyYc|L-(s%dsZQ5qV zU8je9osps=$J}UO59-Fvj7c_-@?xkmi2~fV)pw-YcuH=E;WX-+^(u65o9OpTIs4*@ zCSlYx7n*}`m}!XRP5<3qDfks8v>JMKf#s~D{W6ZY%be?f`mUk1w_s?FykY43Az*LA3 z(!Dnkne1&1$PlO@qfSsd>}U6hkDr{JK#*}+;O@h%NUk^!#XTPb>N~nDyK8z2(H}p4 zeC+1t*71xw6N2^zbHrM(iMdQoO>v20=jNmIA20u@W6KvQ6};srkJIYO1}9wY9gkIP&^~RyXRP5+*2_n{73qkiX(& zbZvis^i6B7Ch_Dt&}JAEM_=-9Ypag{{krC%PT%H6$yg7fnxw;~C3+J5)gc6~@M#F4zV+aD{yHR97kCvC&<<~>qng@l=-Orw7eqgcJIK~YQ%1&L z<;*O<`z6O?YVSh0vn{ho@uQdRO--jqZub(O-4vI2X&dfgJuu?R(5~yzH>@~_Gl2?B z$SWYgp?tLqNb2e0uo(X?bc#JRs)bDgJ+GHHc+(O3@xnbI zAnPGd4!UoZ6x4?x3gamkH1_xRM?^OCS(uxLP3(&_m<(3c%6;%EpK#>o=SQCPXuh&S zr#d!d@9uAHZD~+m_^~>g?m2hUKQHAdEorsz_7_PIm~UxrHuiq{7aV$pWt=hXiVffW z`vySA>XwPG1)(Gmb7M1t@T^v2S7f(p!9-fRAtvEA8c%uA{Msxqdi`B&X3Xd;3SI;NI(3a26X9$qthA z(WY`)!7Yo##R&(hqNkiJAC00`8CRo*Y*|oa4JLoS>UqvM8seRGaPApJqi3b6eX>||AcuDlp-u7(6(WqwWDXKsLB6?yBUsiN zRG?Gq?=bnkf8?A6k46F$eo8;DX?(f;uTEnaw=7Uw#aAH>1|?%M6-Tke(18aK3l2ic zYCMXnKeo6>2*1&P!Cztm(pFf+utt!dLl~|`GRTdi? z+m$2c7E?c!ofLEy8+&`QTu@3-M*Pp>yhd#C=@ZRe(Y-iw$eOF_@-{U2ae|vH_Pg?I zjlst!ucYmg_azpVY!0l}7Gze=nXI9x;y3ONZE>72c(0ztkf2CGNPR#E;l9cYC1WNx z6t#{1{`|J*5;g>qCuLl!B3vlebBR7)j;4KPPYJjc>tkMZ2bdMrO1xu((R`KxkS#?Nq0rPsiA40SLy+qQAyNWKZgDq@ls&^I}lgsD>X_N>owvv zz-Q)L>t7SZCm`?$X&LZr-Z>Acdd!X2PID!>h$O+r!HIQbXBos;g{t+crhYAtuLZVV zC4BCBOFJV&Ze@;%C?$~9AfeWL8{YlAMzO)<4&IyLtY~vJ+^o@~+YyHP;o;#}ayB}| zg3yBl>&D+F)SN={EI9JYXcIx`!vy#3t-}qwfvO6;>cVyS z!;D-YA52Bg71h-Ky*-b=tE*jiIFP-)cyaOZmox{xz3L{r5>{4L!9s|21s=@(TZb&z zRxQji%f;m7REn)bd;20aMioZokDV`NyIRuZBZNL2uo;#=rh<$no7l|DAMXPhQ$H78C2*v#0#5Uje?xvq+``h) zaMFQvLz;J6(faLR`G(b($R=p9L8UeHApUEkkcMTvsPVMNPfb z@@9QzChO_>ufthl{-fBez03yRV)ZTs-K@=Ipdk^Kk#!=&=*8j;_rs)G0jk2K!a@{5ae$}8Vws-BSl z#2oAM1p5mT6(?E=D7zg8xwyEf(QNuV8l%#-sEV52}#L5P_N(k87ZlA_+40qXLByD z*J|i=H>3V@eiZGRrv&wL<21g^{)>?jVxp>USW?XvoA646w^st_6n_#4Zmbn$Wo5kp z>P7nC6?+8Eg|0@H<(lUiRlxd}F z*W4}u_rZAx`|&@DQ76z|ODbP)<&Jf6Qk%8b(}Sz#-{Zi(xOGrG!M7}QyZw4(jHHr3Q$hw-UDqP5lhJk@% zcxWhuXShT}EpD#>ddM#%x=**XvlEAd7}f}w*`bEIBvy{v5-^d!fsn>xUQjHljJDiJ zVSprFj#P4VR@E|a6&iWsdUkMdz$U=!0E@>^A%5X|v$gs^p^vAsFMNHE zPkMSD+(+zprwRmp*8~UoS$+|;b-Ok;>;>ZN;J}Hfk@W-XPq0p1eUb6@*}Z4XdL;Fl zMJn)5nZ(9~XQw%DqraFR`l`-w20J?s9H3TxH%?EdMbHc1W-qX1b+xq6uV258_03iY z>NvnHS_NTSFec}^ghSkgKj1LKpMCsEzba6WoBP;-fD$KdjDS=`9ZL~7 z$M0cr+xRG&IW(7BClbp&OOn+;o`MFhW$6>hJCB@RFs&FosK_)YQ}h#>QFV z(x~a_*DPSwNm*~+EY{PcGX8J0#;9D+ep^z)I)x17n6mZstZiykS(cXmH6A>kItMOf@#;orPz zz`Dlk>SaaAY|7Q~{l->Y668qm4mq=B`yhw|S)C5n6hujO>kY>{0|WR~Uf5Bq_(nw4+Qx9icM#Y{BT?--KXsj;q*CWAo)Pie z_47Y_dwXN|r5&x(`|#uAO%gw^5w14?0gE!$!$0aeh~4q zG@bIFLArNNe#pKjorCt(~8 zhcmZonVX(Ax*H|Evi8Phh<)2HfI83PjN1zr>cO&6cwR%r5mQ{Hg8-|%0;WWgp9c|t z(}F-KX7koTByFlV9(6KDEXqwe?~+*Ya<8l3ZqwD#@dT#gx|4U&(E-!a);2cT1r>oI zGW4rc355z@S)<#6XBg8d%diM?kgdN~`a3lfhW!4<24@wS@-n3d$c3nHzb(;+EI)TG zF>;@86Qw5V&@nQWp}MI*(3GO|92+OXtL7%BGBXi-Vv=kUI>d;`4YZuOy<}mMjzgCkO`xMZpl#s5kFk zUG4Af?J=TFr3~=D1qKR$g2yvBxB{;f+|n+r2h=<>L#C#@yO3*|DO%$y zey2Mq@up5h1O)V?rKRy*B{M^IhlX2V^gZbArZ@5efH-3@eAjn6jc>D8# z;rc#*uK2L87(6lv90G~Ktoqqx95qxrr7*H_8*&F}?%?g4Ob$4lcP~gJpke`>qySjK67@?S0x3M&i!STrn z)~uJMqvHqrTEj3WLfX^e(dsY(+ZS^SJ4;JTF)t4ky{mWf?e~Lz4kM}TTE_0Q^gu}x znqjIQy&YuFc>fz}!1xe&m;cy_Q6*mv3yQkLhCgM!oU6$LniiLq9?;U!p|~*8<-5sL zfvsoOBMyOWY7qyZssyA2GJy^KU0j5MvAxUV{4+s%Oh+ug0)IcX-6jdUsfAlxTSp{e zY~6;qxlpit2X#_!M2V2=Sp^4__6DZxftTAVRS5tXNZWKu5w3cu_1*zRBlc;E&7% zlfF67t9V~SXaTdFdC9(ZmxDv4p&>6fmmG|rB;4OsJW_~Mi?JrG*wKyVmxM4v7`kEd zQT+N*Xi4H=fuxitXI7@~ST~St&JuvX{C9rd47#llBkVLQi`v!CPpn=C z>fN)6TqjXf7b2T5UB=VyOQx4 z;c_f`K|R*pOIdO3vGp&CCCqSH55Kc*gvO5Qq;{rx#B>+M5ohIVbsrt%JwgicZeQeQ zhJOYAhB{85>Y&2LQ&;zNArVx@Ks|ctP;XMroXAxe#X49$+Pj0@-I0NEQCrq%e%OD4Y|{e z#6IDT1~@kjhspYKpy!!BeO>s+pv;Z{Z6gh?7!-B-c(ewjmt5#YG@-gkwZaCa$xDT) zqTOX=7~505va+-Lc6N8K2_SwMnLMGGBr#aZK}cc%xY_i=f}iz(&hYn@vHMv-D~9Z{ z9tlgmDwKeM>55$V?ajmbu3BEO0;Y=im*M6^oQ2v56_8+nl<74(Kwmg; z!zNmXStEE~UVCyNm~hoo&PW^?c^42(ZJ45g}oQwCoFfo zsUL@)>IHK@f1YDjq;D+Q+S+;w*Y<3#H~D|M2Zo>(ZK2oZ5HvkA6KxgO&iQ-ohls{} z0Od1IbkNYH|R*T+0KJTSlu688UTk}%PD5&Js?27Un@(z1ybZV4eF)(;n| z;5M2V$amP&#%M>GORDy9F9D1zo2h$nl`DT4Te07QLPDLF7iVOq*vj|PP2ClKvw=4J z_%p1RuCA_QApq1cI5gx5hJ!`-aM<>OiSwvn(snA)1F*(?qK|cO=GB1SOxp(Cdy-H< z#KROD<57jhnKUwpIqjKoMtD@NG>$T4G&es#6h0WZ&wU=5latdg{!ss3q=1e%bb7BX z=tPI<#CnMusXOySMSpj`mB&6;k9zsNPEJL4fk*&_NdTdkkWgyPNN1;GbCEDq8a-6R z{xt84s)Xz0TkJvKAu9*`P~Mmy7xzhAsqrBocHjD zk1;rbfu)hklX8~o&$EGuZg^v(Yufw+6qhnN4gz zjH$HK_tOH9HW=s2X+kfHv)H*Xnc*9zoY_kO=r%Y7!7wFeeLeX!$1`wlCHz`1_FI;Yxfpvbp+`&x||&TbDE!9gt3B%meNlmD`E=5t-so||Yw z$zC!-LPC#ld4PeE$|s0J^KCD9ng}-3{=ZguLgL>wZ!x8nzzBQX#Oo}}XfJlxPFZo} z55yt5ioG5CRvStiwYJCHktjSczFns|b(;WqE%Z_+PTQzGuP{x#bIIr;Z9MkrDVHcs zyh@$4F6JttQSVQz_5F}f?7H)t-@rWllz7{$Ej`%bGXta%czq=^7|@ZrLoyn<{zw7_ zF)5*Fl4B2-I$xWXjP?lA5Y&v?M^UWiKakBLVw7FYrFvBU*hCNJu67VvMMfpVP!85PRqcK-go zOMcOrWKL?qk6Ku0dm$AokN|)$@IniA!V8woEUS0z35Q>wcvi8Y#8vb6@}z~dQD~mR zKoAY~Xy~9@eIl`s+8u(RCbQ??xKz}M;n?|7vmBG=39f+2Bz56_uES@ zhK2~?^mJbUhz6(m$53}7qYBiMOcc>js{U~DO@*Cc7`aQoj73SCg%bbl+#FqFL&MuA zgi&g^?CZaOD^nKmxw^PGWMC@d+JM=4lYN>0-LG2P`LzqET)KYn(cuDD%g9QqJej}B z5^wmxlwLxHgSuC?C#-j0*4Bn3Kz7(ra zdQIt%3NcTOKgdL7yO?YeSiSVmuxaI0;bDoWWwVOfYlS~dC&mWgts= zrwMZgOaJj%c4jRni=*Jy9`c(PUz8)z8!&F(1mPm=DDF-RMWZOU;a^|BxmO=|n zYgMO{?^JgY|%TdrGm8{VA?R)?dVMH*IG4rA|xTl=aHM(9kV#-YtgI5W}Em#QK$lG~Up%=39MINOZ0b?(aj0 zz@3gkUDHfHv$Mldb6Q?7{nmB`rt<)&5OrO06iUAyt0MnH4?WoFmv+6kf&<4A1_%TV zHF@DvgE2Of^O3$*&Mn$+hF9M^NLgb?bE0}8qXG^7Xu0dlFEMrwRe3hzQ3`M@l}%{$j}-XY`Nc zGxPYH*?wM54q2G=BG40?%B}x{GPO@K=E9W$f~UbzI{fERhK#J(!S&U7)#=_!cUZx0 z-Kc#j3q%uq&tZS1JFy3kShiJ?tn~eE&v7&-i~VujdEpZQqgU%YaDF@kKBZM@%y`xH zx#MF|rTJlweT&I5W;`^bxP|am)$Z+rCeb3p=jjzK?orT>7Sga>^LiEn@ zvG1+8Je>M<2iR5rmhtTT{MA?Le_!oeSV2*5qSJh`yKn$P>nCv-sD9vuYAZx1lH|Es zp@(nMtSo$mL`ZkavTLbU*8X>2*N6(aQ{p>+G-e1=Wa&>v&dA6~NfhJMzNnL*;cT&M z3**m6g_Rg;28n92jIjRX0LIyjjC_c*=DZb#j}o`pTQ3J7CRA)bJ@L?heDX9lmP6$d z^^c6c6aM>YMfXYb4&D6kOFR-j58+3t#CY6+Ssr!1CQ=C(=>cfER!ju@%vAG@$Mz0+ z1R=fwWLTJZmOZLiB-1TMCT{^~Em^)0(fWgoD@a;@7C~Q$mrXeQuSWCw_fiN#Qz+Ro55xUY;2S z<<2NihFF2i{4yD6z(Ve&afJ`O_m&eGx4py~_0R?Ch)pvdsH}MSE=uV`j@ZN`N``FI!Dae_?;*SGCjXuu1AlqxShQ0sy3m&De|f*IAZi zMvFu|kBpzS%Y8o}!!3eC7wX=c24&^h5VRnG3ckS;y}m4LH1O>?@=4${st2VDK;$q- zkm53vFsJ2#&f8(gqmL6eYm6Y)bZ*kJu&{tDs*&5$cg9@s0TekoChzdCcN;BHoDUBH zwwAw3=T!OF-4rwK&SEn~@KWgYc>xvEFRfasHVNjZEHOK7jdpUtTzD+u@>2|7}2$ec(lJnan#{SKk0ihdvX&K6I*(IhIUAv zj#=1A0mcP`5(TbkxL&;T!9ycSEY&}Q)QtdBsc;&6vbTMn=?jDD-KqvR*kYY_cgl~Q zL80*Y=aoB2E8BD=@EKUO0x~q|#ZBAsLkn*VODyrMGOj@Fb|k1fX6!{+Z_#cr!q0YV zrTub>4_H_hHWxy#ZK7Ueey$E}$B_RETYvsEn3qJ(P&5lQ1#+x#KqrbZ{hyf+PsZ-JRZn6pWXyuv6dwjaDaoLHzN*L+SKUxuZ|JW z2|9IFGW%AN$S$McK|~0pP+92|-Ikc7>Kn9+yu4Cos8(ih<~*)^Q~(FVf6#LUKgm9I z#_w|qq81ybUfM#1U}Z62tb#r}lenMR-=tq|nP=@-=M4%~XmHa1_Svg)^c)?T`n&Drgc8;i;bi&%w)>%)=YssWN+Voqv)7;JXuU zQHwMy!2{wuP#4IZtbnA|XE?ASf&G)>f#^Y|h)Ixxjmd7>DmpoRLFr4AHj_|0STXLle zKT|o%w4Oetr5qG1e!mla{Z_A~5D)?Y0*P2rRsP=%AUDQXiS!5Scq#~+DzAq`NFa*Y z*;#nOqt%I_pQV&*I`-ap=ppK7Cc6H?!D&ysZ@}u`KvtQHPPk{*vwd?EpjGAo86CtF z@S}i+gCs$M@(?oA3oP>&WWja9V;a=e)QsIa0s7jbaPA9os?1v|QP@KTj{Cxi2B7G&&%lC0%pFNL;c@;3x z3Zr1T{j<%{ih|FLSQ1CZlRrmpsoU&m^{0z|Z>Mf7jxznixAM^1_ZB3ZgVw2wAjvGq z&%XtsD;pX_ycK02E(PJVP=9}7hqRi)F+k$zi?Bs)Yd{}@OX0G`i3SWdnBTWsUC;F~UK>-WVltbsK6*NFdx>T2lC^AOPYe zD`Wa`3aa@oD0Kz{(f4Z@LVW@3o={FVahTu;KC>I@#LanqFFs*4O}1p8x7?xV@4qt% zN)8m?xlFwD+EO!#X;Dduk}$KKK7@``oOB299qPuDNMo7F^7AC}WuJ(L7)+1tLV>8@3@{uNKbff*}O!BhNA!;#Ng`_F%fpQX}#fMfIs{?I3d*RJ7)t|AIkDrx*8 z93p0411K-V*l3IHyHD+%oP=D$L51(!_t((>`l|JhJlA)bSWp-0O4`>^jRIs~_bn{c z=KK5V)Xck2Rq*0JQ1aCt12`Y`*ofF#&4CGoa0AYUhoK7ZtDa}masQ=cIx4dfK}4G4Dm}br0Cd-tzp>f3x1<(AcSFFMD3;R?CjvjlP&)J zdtXN*i$5*dNZo`OvshnBqx+UmUrsSeoz2X1 zL!;uRam01j(2&j$%L7wE_r>gT%JvWI@}IYIeGx=Qzwsb`9u>#0r?L&+(KFzx4wZnf z4hFhOtMx4!t$sFv&y;y=V{hXOGuJ0AX!@%F{rT+xr z9V@@v_&;__`V@eu6kmPQGnEIiGv&~92A3OM^|l{G`0PmuxI#7jiWWdSn7ddbQUHFi zSkq32N!@cyZN*QU(F_QEMV;J3J>b3rmioh<(B}Eix1dO{E_v zYjquqtJT{&fw6mJ+K&Q6HU7o~B z`W)c$M`1WnIB`n*`6@WrQRbZQBZNS}rbTH2%|k;&6`H$FD&Iryz?f!0TQg`t%EY(l z1D$t!d;0>kPWvUN=!Vf#Y?v;O3^uN{25oL&5FNZBI!{oazB`k}6^h7}V1)oEt|9o* zgj;)lCDFbGO#+qBTy5r6F2zoYrdla2PJTBTWD8k_L$`@B&;{vmCM?DSsukP|F=I!y zQETB#Fkt~;@UD%Egn+=n*@wcxzL0#z~-BWiIL z?(pMI*))#?&!SwiSlG}DQeI>tjap@R z^X>sZ=d}2o%(DG2CKOF9R;?v1D+{W8%3=?p(xvxz^1?-xhIqgKf!jC#d3X#xNJQ;Q z+mzxief}&;!r8A)-5XlIcNY(y1$1lm5E;s~Vz|w#=P<8+?>eU(kDqIz1J1mE!|iFj z1*2M#BbfJ))MBru{hToF&%5%u0Kq|VzSyZq#W-ENmcYopl}#5-96Py0^5M6~G9m|^1_Rlt28BE-95 zFyL7zzLfbi(~Yrt2lPonkHZbXeDn;Enj@+$t*zSMyjJgOcK*0w4Y8)T6asQjLv}zR zSqEv;SrP+5O4v8%h%RV~HEoJrD_qsZzu29<{&WjWjn(nf-+kQOnV1IscB6=-TJl(T zFR!|3K#bw}WYg22nbx@e<;#~^(W#dQ@3qKqhVrcVikASN2TpovI`XC@x#+NL^0X(5 zOE=x34KQJNF(eW7TFhnR3>nA8FDR5y`FPW&J8_Hv96c_%j~wMi=H$={t(|3n7q0(q zZ7mlz_iaSJxah}az8i61$&YH^YgY!X5@&Am&G=Qx|2 z(|bS~py*MO_1K@|)qYiqvx~qqKI1Q`bz`2tEgkJh*?V66;SXcA4ARHj+kIb)CmR_K zL{1(l+e32M9rx)<#ih#I*Y^{c3;gb(DI-|DJ^}P6MY7Dtms*cz5NP&Y1LW+lU-SUb zERWl|fvHL|y?lO(H$ua4{-kjXPVGh*_Bv-Y!%oMj>4oBNe0Og?<_OPP+|vl%{*(9~ z8kvAU8jPYT#_wkic7aQD(6^mkqE}UWZ75SmtVms65Vo@$K+8T0P^hyL6Ai`n9(b=- zCqnu#NeQc*#yrtk3r9r^V`SXi9keB*pgbAuE#(PX(&>zBk?hed_1^L+>#Lu7SvTMto_{PI09|;x(Q?lBBfoyNzGv|=63pUWbCZ(w1{|1hT~>O!Q14)eFsQpN(t2GjRUfWt9`wc)iwX-9GC@8lRh{fT|gdMcxx4VaMEk2uD{{ddF43>v;W;m0Mg@<#$xN4@gTV~0gaGr)@S z*q%p$t@isDR{SWuh=-%PmuiDW^D{HOVD+MpMDfdRfR0kJ!11*tn~@~##d@CE-=(Fo za!%V!0%WTt-WJ1qt%o1KVXuMFssc`RIt?D5s3vy$vW&xJ^P?LHsa7S32q=nPQyeWN63(%7(@Cb)&V5i9)jD{U`aDT$%d)(f-I|fOF61TDP z7OQ@M94h-4Z(!?~3zRfwtfqjqd$_D+D_qM?mAPgdV;z8vMY)_#?X0~9G5Nsi(79oq zitg%6eXk4oQ3lrd2LJ%ld-8r8WF?fFmY{%iad0q(W#-<{>FO6vF90WH;pb1zeSvTkUYUX*cHXES96+`B#8@cU?| zt$ol|Pj6=d(55(>_?&D9fq1=K(|bHwF_p4litZ#FeGDPt>oKAZIVXJ!b93nZ#jbZ- zFnRJsI%pLvfQ;}Sjksy&{!@@7>9OxiR-!4?^`<^=rG#GwZucn#obB!DRnN|KM}Y>% zDG-*)3fl|7B2sY2AVwFw8_L{ZD})igO9bB^K@jd8h;79?k+LEar`1C%thEdRwBTLo zHEL3kcRs_1gM$Qb?n#VSSjJG8nVX}c$9kHKZi{WFOyAoS5ZhWeK#(-LSv_@8{DuFXg(TUtz?gEI809))2cr z-FZkNBXe3hWR>-rg=&zTFD+7Rm;@?+i ztKRwlSUL}Qs^9njzmHA!Udi6FXR^tNA|oRd$E<{sm7R>EY}s20*+rRU%UY{Ue1HEQ507-5ocH^EU-xz2_jSKs&%2iaZk3K;j(gsl55v6UDzn(l}xVs4aa)4u&5#J4xQ5i&ZwTIHy_QjaKH}f5PS0Pmc$d19*WBmBq!d3E|8!QTMmT1*ojP(`v!WBCAOZk-}cO#yXB80AWTY)pBYgn6xePv-6O z%Y)40V2Z1T_&JaUN}@mK$m6Trn9LfQi8kM-J5EEhX(ITNF8&vfOPKyceV}mEu#F6l z*20N^P&OJQccLSqX=pF|ThlLkjfg@FPZ|ZsL=N)f7G#ovj@ag&| z`aPZmZ649@Iz%CX%3dCc>wKq99U^MJlTT+(v%GHfk{>1qeuk1!g-NOdHHx$JZUO4I zj7iDfr&BkTRmO?6J(*%U1mPT;pC9t8PY6?%%Om)zHy&F-x#olNI{#G4gC1r79vhQ< z>OzfDU;Z`~!$}h6{9kFco_&Uf3JGPuaZ77!aQmlrob^Ast_BjMOkX>uXQ}CPo_~VE zAwg$`BmwyWx4aKb?cq+lw1fkCUkKh7v1FF{5Rq0?{Qf6K+Ex3|XEkeNJRAEbaj5f6 z&X18mTTHEOrG?opg@IIviLLRf6{sMU8@o&T4mm+4+AY(?!AJ{xQq0-1J*?RQ*eu)H zDx$Y@sxBjrK7;cx|0`$Arb2o_n9O+Zmnvz*aOBJhESQYrYg%S|7DxN?YSf-WPSfse zm^4&CF+%nyOa4&FwiJx|Ky%qG`rT%N1|C#-k+ObK7@oT?;GAsDCs5L`Qx^JT1)(*=Io+uK7o)p>@w1t%=z(<57Fc18sx zF(dAH1;wdHS68GB3d~k11eBZ>j`RKK9AHv_*s!2)Gv%L+7YOnNkB-FI9(Z}t`k`wD zcef~rH~P+=-X!g7oH^Tz{mFD9jn{9hs0-%PEs?fGBkvo@`+tQ3c#OFmZ8=nh6Pb7?`M z>nzb*df6FwVT8D;^&OQli|@IYrO(O}dwY*&JIC`Cx_rZxW1X%R5R;J+BP@N}Soiu2 zfdL=4dMh%AtXJ5PwoqJJTADj}tqVtxOBXPnImv2x#Fra%-20PM$O`a<%weBWva5;1 z?v{;BQF)&Ni7r9Gx9SNuS=qy?nfZBD_d72}16!=Jcq*k@36Q&BIS#BacR=?=Zb8?? zLe`~L#5b4hBE)zmcww}?PJ|`>sT*@St;OHjR<1;^4<+5bXy86SH6;-59QrJ`(%eA8 zPuZy7KX%YVrT?%~oIBumE#V1q7(0d76S!y%&H000!!zk4&b-6Bt4XRjY&dAC|^ zE4wAT+*L5D&m?m;;D1Fu$?61G@7Gt{z!#udPwi&zcFlFa5!of#m053fPg|r`&EZR@ zRmHnV$vGKmbBaT%+oxYsjKP8Khw-$sqAPIkra(T}C9Ke|XiXZh3B2BCN;t;``&^i9 zB~r3#vUn-1t&^`EZ}EGrOxb~D%DabubFt`^@wQ;RUtnpetZ`Qzo1NeIW8itA%u5{! zvBXq!9^EfBFI%o4ayC(x_Vn~jcpox?6aR=;kplP#)UGQ7T;nP} ze!TSf$&(XewB?zB4<^&gzI4+t36$QO3fYzE@uM$pgBH)b8WUmh zQ0tc}7a~Y-J?w7UraE#d@y9-NDfJS2^*Vw{nKgTp&y-?ADVy1PBOJAy1G@6lsiq9vPE3S z$Glo9ed`(TUzdVBQ*08+5KT=OX(KO0wRYax(}n#L{PfUW)(kP?B2tMfQl|u;h;5Uv zi_kXjz7HYb#y26Fes0Mkk@4QjYGm=i59xV>{@CpIr{sF&l%MZ&7mWK?xu&~A6}K@1 zME0w_uX&Z7am<9xVqlUbMWnnwQaNOq%(!5)@BT`De{%2MLt!S~3|G6%?AXZjLW-#I z!4akfRl{6GzIU|dEbh*CO*q3B*R3lj+~Ai^XtDkS9sHitp_CXP11^{a{-e{Hn9`*Ped{ zt*X_;XM@?9TDjjsV4{BgHn_q(JTx?v{HcQ&1MP{hS>;sv*~(&@JXMgszuu~XlbE_k zJqbS~i@j|Gk!UGpc_c;6&8Phj|JK0UFOc0x9tx!ay}`!T_M+0n)%~3vDL~W7OF9i@ z{)WbSP#}1!UabRf0PpX^&!n6&i|nkUB!uW*R#CZ#ul5eMDi+=ySD2Ui zE4P|gm>=F0?Mk{#MyZG@?;}MaQU!u;iVw%M@@b%n zw3O)e^z;j&9pDOBjIluZK+BlkkD>7JTJ{kGbEIXRgbyjTgX8n$$5Je*57IBxBpTh_ zIi`o)ma4J;@d>V^05-~^VDL-84>@ft^E%}Y{4>Ev9HI-*tWhD9eS#xjW$Dk6)`^q% zH=lR)9msaumQCDDza@&a;YHWjnU{O3@3Toa4`UrB6Z5sfM2%Fz6D1>zVjvDNG^V#; ziL~VMVJ)lSC+(}ZLNhx(&2rbKXgpeRpSZw#$)uZTh|XHCjzlGsC@XUW#Yrgj`fE{% zC#5lcMHnuNSRs@y*5Eu)&f^&W?5M?^; z!n|wII>bw0`8PlQS{qc+nz_(w;GW`tj}mFDN(jm0Mgq~yC7jX+G01gZPLmkH`(ca^ zcw+95AuaY8I{V9HiUFRU+w#SsJbu&nuB6d<b8yt#;Qh7a3iUhY66frRbYDP((RY=w8)>ix(Ipu?B zFd^;gg)~L=v6$vmOy?bCU?S98)vpogevRu9@KPd16piWWidby)zmwZ?Uw}(fww%AU zV>hDK((#?;Wh~pe5Dfxy_)4GiiCUpA0(hks3j`VODF~akS|oLof8L64W^d;ALauKz zw7Y!E`k;f(DvvDu!g%0O{M|#8ajgHh@Ed01^1;ik!b5i4Q{!qU4KsdA_#R|#kt(RM zNk^RoV45@zik=(2IcdpMaU?*W(4yQJJc2#3W8jKVQH*cgC#}P$eF<9)l2<7a+2*_U z_8|hHq(;wMK~dC`O;I3Nf>xLIjJ)lIJ$!0a!7odm7LH|cZGzV~oR%|~gHc?&nLI`_ z&!DH#ca{fMLKiNNS1hM{5TuPVd27jDJ864*G2oTRgj>CuWo2-qRYg#u)Q$E#cBn&3 z0EoQ(j@a8d5WNtLai?!$6?iww{aGR`S@s&KiZxzV<^bw?g?Sv_A~>=I6rQ;_;;ad9 zpsSy3m+t==kg%gt@Ra{Exa~-=e?M9s=+|34y(#J>*CPdm@}h?Ow}$ey_P3Olk9s4n z6X-if7VmX4<7lLa+b&d2pth|2^4#i$b@`s~-qvrUGBU{1rzug=Cx3Z$`undvrI!fy z5AUxGe0Q2D*V0L)jgtM3S%#vF{iebUc@Oc*!3lv~SjgLm+SV`rq-#klL-u81!QP&L zljWn(mYqsarm8`3Q^;WW8cnVz>U@U$77!((p_nF^<=k#rS zYXvpSUUpy_-@G1;F#x?D4};(PQMd;X0y=Ms@`KUa)vyKZ=UyrL%OA(33&*+w>_X%8go>#O;3YpK1?j5faA$BC#+J>hVO&**rrc}QsQ2$s8>{~qcUVUF6kq+(k8gk|NNsbZCCz5g-e#LNC?Ke#&hr=u z-(d*LSbF}oMt?=C1>VBF#DvPhU(zzpGD9&=8KznldLmn7A72oMJk>AO$fU5>PP0DG z&TcajMe@|?F>?J^k!znm;b?&1h{C*S-^FG(seI`wo&9$ZH(%+vH3S_5*v7)3;?{R` zT;b#MbQV?R&gp?5qbd4?Ult5HP&rRmIL{T*F1?}TOY+K53J|boL42lEIiw`%JZAXY z7*#}FTTI`W3LiqNyOG>@XHtrVwZ7NoHAjasDD^xqI2?^7D(2nkxGUqD@h-Yzd|gPe zejQek(#dg|OF*IUYP{RO1v(+8j2{GK#{fWZHk3OyKHnNGi>v<)(o4~S8CIDHT0I@q zW`0t%K-}&6eS3B!Xh}pw>d%iK{1YG&q8~_6y=7-7fGlW#Mk66m+|Vi)wKNV0HB%tICa@-6E|8z(d0xJOH@mne9-A`G=Q zF!QDSp_HqxVNa!Jygu8pUMZa5uZs|n*BBLTcrCSw&aM_OL+ih6YPJ15CY$t^>Jgoh z;t9yUJ!$J-Ih)o%0ms0;d3Pld71fK^#=ojZAH&~vzegV98G8HW*)WCu+&j3&XCfz+ z@3pCb+%+$ofJpzOi{i&}G!YCY)TVe&pAOu{FeNVpgC3mbvV-38oDlN0+*H`p%X z+UBvIJdNz#Rt7}k8~yC-X;DPn<*pj_k z1ko+&x4hr~W!?D4mMzvnnpcv^kB9LnlMA^Y8dcYae}dQQ_({evxwtNK#D@GcJuK05 z3b}+sR4T5un3NV7hFo0*Ce^DZkc$`zGhAtPb#W2Ie>kAAL|HUhB1CmP6eB_nJA;fs zu*^Vk-u_D$SXuk1HNz%JtNeU?oa*kohHs;@V+iC|9Tz3LESnF7q`Aul^WtUjSThSRc*oRGSvskPOEb+)mgAqxKq zKcSh!3_qZx%0cueK;H*9>zn;l{oP%o<&A7mgnGC4xmCLFE%J}xV5GK%vl4P8t1#Q zNXuuO?E;hZfA91(m-jFkk?qIyc(1D$wY^In92%nI_lp;tz=iM z`Cskp>MGW?Rb!z{zDu5ws zh!VWq`7>nw?(ja#U%mO&O&gS5r+C^fUC*ieFc=Io#{qbTr@?E`D%5W(iBx&|`)7bF zzkXa{YUmMY9!t1MzdYZA0;QNis4W!#2B5ah7NKLGSJ zYIUOM(yhZTrxVDN*n-e*U5~(x&cpl%_MZ%%V=!9)#h5)jY@F~?!&9K5&p2Tr9x4`5 z$-E~K5ER4#(ybqcTSKf7v6FTB*wu1xR%A3npziJCUl}C{Yxk2?!_o%75mvtS^coOa zPI+VN;N@X;{?2PQZKcNCaj8qQGnH^Fp~BxE1qIztC3R7MLc~?)2}BKW#bbqzf;tVq zGVE;A-!CZ=B2Kx2c?PA$%X_-BjoM{L&VqG?j;=GkNa_}bhb=EMkd*`8fCH2 z_^=}VZ>#ymG*wjF;_z_%&5LPbvL}xYzE}ZRZu$3Z?bi zn=_S6e6=u~<3-VBWra+(7lnbu<#Yc6w1FE` zs#AEoWF28R5FY^s$TP_jvCU6lB#Q&BYI0}&txHfoKEkS5N`JO+`rA;R{Nn4>R2n|= z6Ux@G6(I%M%t;p+_^VL&tx#fc_t|SuX~t_*5$MM|a1_AP#v7v#Ok#ls?U}d4dmB}x zMf*bXN940-Nn}CsS%{}sGfU8+8Ihx}@687{CGACfRwh8;$K7>MmSUpAZ}3QxE0)vY z6D3Fz`t=jYHbVX0@P?Pe3)tbq5O0QxT(>^_9{e-6waM z+)-sTK=jfVvci`ZT%}haf7vBpykA2eLQz)ShF#VpA-XNQW5+9(OL7rx^R#LixvGv{ z_W%Z?5Gk3%_ikT!+FLu_D79umMl9p?VwF=!s9@O@`i*iw1UyUD!N^O(5MJcxTh;3M z<-h-QCjVugZ%K4+{4crj&61}K{sI7eE(6a-)|e<{lj8@h4S=2^>iQ`&6J|L;_!+@0 zeuH>$?Ni(WB1unA6bW_D&wb@$i$vlO?9ZRCEBqh{M8R&5iJZ=CAH)VI5L*j)TzGI0 zB!-uCySlm>K=vw;Kw1^_@L}>9_v7kYF__FMC@ShbkBjqL!~k0WT(I)g8W^L9NsmA@ zSB9i^(2eV>gxq+DKn(Y}BGRg+{qj!7Ws7$30J$}p|q3~_v_G) zGR|*_S&&l5laC6LqL8Y)gn{`@RIlnjLQJ25SDF$w@}*9P;5&A|N!H>4Q#F#4ILXuU zb`ea{0A;pPE%bVGAcn-%pnMC)b_TLeHby@7B>wqyQ9OzyUt9lKu+`8Uc>qyJFNXaV z`KKC4KfRaHDtbyXk3;ilQP-cUMC zN%>4bQkFb0BF-WsZcLY%jjdn34f~#WN7da6_fMaoNa~|Gz%4<{WXjn`Z@|qncF|_( z-z7lG>Cx~R&V-kNi$J8NwN^`o-?Yu;xsCV-QhVSXyq2X~h7VypZqSi5QrEtJeB0dC z_!qujS(jMXLAH*mOqsM1KlwCsO!AMc#k-6~Mn;K6%IO`SfiZMRPR@MzC(HFBF`hH# zOY%3>i|5pOp-V8B{l+O`LbBG&UlqU(;tnG55sZZ0%;$iy%#DBpJu)uzyCQR&z8_-? zD1%VQ!uRi?e1B=dAwm!(NT`Hjj3C>xTlmBjx3>422gu4+N8@-6ou9mil&lud1`h=3 z@T^+(QNt5DSHiDJ$6sq7(zE{*h)m`Al{ahLA7wS*#npabafRfneCd*OxSq>Qo=kcR zbaK~Nv=)~Z;y73^>l4#ZT`fsDTh9VCC71p+h!9%ED{e8ZCqh%z?93$`b9na^wluEe zMS=1`)+V@A^#b#y@1vAgSG3h6g*MThqYqqMZN;M)p3XfIf!fTt+?okb_;2rodYup|sV2U*GAU8J~C{OZ-KBp1=ws)#nt zwMh7&4G9FW;pFtXuaO8hBI8f4(|n4`)EaLRaIC@d{zKY+*AUFM_{snvWFC8Y-NDNA z5f}%36vOGIMj%6KHr#n##cFe#?-LSeM0S>#T*UF2TZIQ@UcKE8n`~D&63X8`q5WQ3 z9!u$M{#otCktYRT6s^41urM~kJpt!Mt=rikgBP-hMExpSUWNdMYq!SUe$soEQ%uc>tJq!HdNyXm}Zs|4Vl&B~9CjwX-(V01%DTFeuI zMV$yYmdtj80GaCR1f0}1G)!I0_9$eLd2{{sm-)@jm!(-aU}pyI;=FY(H0vT*6IdK> zyLx!MfUWdi!8Uxr1@-ihRvEF*MN>LolOQr8r{OBf47D%JzfFU*tTA68?Q<{hxrl!E zxiaxO3`}kma*D;?z$dzzyg?ci7^tkd?{53ONzVw#TPBBs6asC(4i0=rZcn$zOaF@P z-oXJ9uv!AjtS==@Vok7YC{wzczy1FlnYSYT##jxC&(xb(pX0u9zO{?z)o_?;kmdkM z>fkxWYjiU?97IbF*#%#gvV0uo>|MQs2H0EPjUn=wJ6U!^@cKM}BKn2cZ2<5TIycjWvR_(Q-~@fba?~d;z?)p({n`WAoE;+$WfLN# z>m^u(>`;3oa}1@#Ax(88f>~8M2~<`NH^#G%&1m)U{-Y1ZrEqDfRZ}1f0{r|^Fiqt~ z9cVY_Ac=JLFtYGWU(IX4#e{qDppaa{b*fhkvGhn0@QKd6zjy8wAV0F=@Pz0-_l7L- z)x5nwCX8SK6{Rgc*(Xlj{nnNn)eqrSFet0t+1d%COe<+{)&0tA!)h{}vf30uMtXsj zJTG1kULxKNP3~%n@ehzJq73P1Z&wd;a}&Pa&IzgNP$K*u<$tP}bH>xroUC(z=X1%P zkHyX!q1ptvlUjNwk2_fc0t1^0l7;G)Y81{iI1CQJ$X;{k9Y%S90WvNd`vLdi4@Q zkA2R;J?|GjCzLm7pv_{cDG^Gq@$*{xEf_!M!9iBxrJ1CN_%fYGS_vm##Koa|rtg{= zZKa+^U_vWn-0f=e=Mo(nQ80iOpN5U`){RMw4hb6QM2aMW>E-UZ@Zq8%^6AV^!Tn>k zS?abC@XR++j@1Nm?|gH>N`^NUg5VhRqoZ0+oOHt9p94;?zdIXoNF4k&EJ)#9Y9G=v z9Dq&k)&naQSZRcr({I5M6ZnXbA63ZNWCEd2o~H62h!ARP$Md6_=RE~ZmT6h_iHK&D zqk*nFW13@A(aoOI!8a+MO3KQ|WJs`J7gm={*z&!M^3(zvpSz0|i3b`JebjbtZDPAd zh{If=yIlwGK^0N0@|Py_2Gol*@k&6{J?FXvQ>BCMWk{8 z9xg8HU9x*+sv>V<_1ekr7x7^#t0yNN@!w9(KXd;L`b*skX$3I|Q!Xa;OhMj~!5Rt?-xf7H46^HD8_<(XuW3ZVa9z{?G?3 z@AVak?O)(F+8t~OCja}w{10r$R~Cz?C@FXNIbQOQT=Eo;C?C5ZzUh&A*60||uC71$ z6sG`lDgxnj-=i-Ls4BLJR$^pkZ|LmBZ!YW+l8h8NV)mF@gg8Eb0PTQ^I??%8oF1>? zi}D>G@HuWnHt0`ypc;JY2+jwr?YqBG)0d&zbLu9;3e}#kP|M)M^R3ddh0S+37b$^r zinqu1H3vI87P#k@HCN8ehnjB>``5ohBQe3mGih`VsHDS$IH6j;adP!WI(y3m#v!^Y zqdL;lR|}0YpIGB{F2`JJcQ_Om{@t0_XaSC`KhQTZPfuEYbSW0?SbCRXx`8++lCO|Eq``cua^c_6dgdv*3jheBRac%UKz(*?Qg;_xwJ)e3B*ZW zgc3*veDQ%klGZj(&mRPwbF5tryowFYqh-xD&at~Jv)-mhcY;S|$4cGV-)-9N8p720 zYW&bYSmD9~?fWQ%y?fq!Kz7Sh>|XEgrW!)4`f691We_mj>*>BMDVdNzp3B=Dbo|H5*&9*Z`fPNLhU<$iDaKt%n= z;LpOiS0yD<%npN{e}@8Y9m0Odn&zM8|Na^ea}(4Zb3NT)6L8l3w2&FM8t zPLE69zw`9H$dFv8F#ZU?+wnJKoUB+&Ww0pZnDRUOd3aD2g_{y(#X9DMl~-P2MNFWg zTLzmNc$c4^@uQ;elE5-4181U0GCjZK7M!+0x(#EJkh?@OZ%D8$KKsL>EV}4%v}qRA ze2>CY5Y?^cALmZ=>geLPT|~-XO6XcBi;8@L_$&t@R)s>7xvMWF+3?4?Qg^&GR27^S zw3*&9l4I?|y#Vn_!B+0?7Z>yW)KDxhLLvX=)3eM1t>XJ^t}K;uXW(ZuBthTd%u$x| zGE=X+q{spQk#1vBJ>HNb>eRIS(*Gr5XVF5L**|Pvst_Y(CS8mu>eMH0KC3^QuusNh z@Rk;lDjyh0@zr#M(x4PV$C0v&pCY(kfVZC7uLK&2w}SQQT!HjczTLsDX#1o#8XZFp zx8C4Yav*DoXY^`$-KnIxWwF^D7^}|~{)TR0YwNlQT?n8;zvz?^IE5)o^RkI==zI&V zT%p5~BeWr@5%H_ytm;zA8b8^pD_Igfvr=#I?2TD*I+(8oH<01>p8AhGGv2?ntipr} z(Ik12R&@@u?K~}uDUXnzzseU9wJyTEd+ZrA3O$nZwm8foo%$mgV5;1~*s3gW8#7F0 zMT8tPJ-WqdJjIQU(2?p{z`?O4StPu|e1ZP{N^5_(vWpGP02Bq6XMVgw4~SGXoovO1P$F#YN#Qa8|Z)mX-;D^qAB!!R>0Bzy~zzhs_6g+-G}l# z=(}Iw&m9boI~b>PDu%Y`sZ5j4s+E4%ko-xYBJ^R~`o@NyEN;mv7!G2B^X=xZ;Um!O zdYh_eB1-_=c``2*um&ql^aDrcmd_@rv!!KaWiQFf)=R+~bX{{edU4$8kMFy=xdE;8 zHLbcphJ>!=BKyux;0ZT6gFun&b1BJttP{ru*8+!u5P=dP^cP#>*yG2?-oqaL zUA{LAQ-AdaTTWs#r~(W{Voil`v0SN1+VYOcm}|bB(@j51j_=p{4&d?uf&fdGTK07`>j_1h{E7hzpAHjmaF6@u*wPRYF2WE0#v{J%hN9^7Jz26^J zX=DLCUT=KzNN6X6>X$NP1tC9v*MidqSW;ZQuDl0bUWP*f6c_N5RTt?wP%1nCjEFsY z&WZy`gj!z9#1{Cd!w;6asn9Y#zhIxid9*!+hCtop-dM2-DG-H9ee+F)S@y)g-%K6w z<+=d8E^BH6E@C+H1+)ErdV0E`$+&9$ACTkzMN%iSB`Kz&&-rxA!NdkQZX)^vs{o6u z@!GOW5ZiX&6gtfTTS7RbA0v)>(B!|myV!%T=H=w1^Y8)HTL>`*L@g^loknI-eOLGN zTue0sk_T|OZdw&aXJF1baVI>9;!mZocdXUf_D?~n9jM;hn)$*2qGAQNjx|xId-hHi6PqLKv#boB>4E8Q(g*K-^!Wc660|ZA}XuBk< zO*5Z)m|fN?vyQf@7Yg9cK!NkF(l#4!SD?Snw5i-oIl*yzCh}_VC&oZ=>-``eORnxa zyHU!8;~9UXZF72yB{4M3$6lfFOip=xlQ`mf!WO=VG=7MH(uGY^A~ohmZ(v`cjy_qS zm#2b^j(7OCwhk%ak#!>P>B!}R4oCR3rKqadE-Tt?f)do5Aav(I;8+mF|4fy{R_%mB z9-k18LDX?pTt@B3Nl_D&zCvRW7jB2ulcT>tyD>K@ER?50(i&}%K%1LS2$2R@3oB@lNOT2*wXZUv zx}R!&_UP|stx?=PCnx^WNdfiM)wKPbu6a8d50KY5&=2BsaA5@x#Mx9mlKJdqwTG}r zC)N&ucrOKycv!}i;<9H;I^dDJoza7btvpHc&3e@V;Mh%@C%f@UHuM2QJ4aW>thO>At@)ZxQ#LWY1 z*`zcg^Fj7Nj`jRR)(C*_o36wSe|~6)%3+)N1pnXobG5+{<&FF6mt&3?u+N=o8>)?N zdlbXLP^YPQjCCP4qXJrZCj-g%lCZ6|hY`mBI}R=3eU<&_JggIY7N&YX8*WCi_}pUIV~())5h+;O6GSylrO^qJ&5fW!AW;_>U&D~exjx$9I{q6``YsttN7q9{OpZJv@M*6 zc#8-*Gv8IiD-n2{ccA_tPtRhc_nv$Ker8@cq&K_j&VKU`xe!D90pTgzPcBd(7$~MG zs1->EsNK1lCC2I?uW2WmqwcgHxvYX1t|Nj?PmX*>+N#-ho4@oIDxpK!L@Ow_U}kSj z+6EDo#21+Em8WW)>KN^|u>uRgi_9sN^}=Idw2W)R&0|#?#-=I-y_L{#DPz2~nf0iD z7iUN~b$hZo7M|GKe{w=1DwpJcN)uB$eUZfKCH*YbY3`7! z+nN6^o%NYn+#ci2c&Jt9$0(tGxlltYsH^vsK(ya#gESsMkR%QIw0{r(QcniqJMG$Q zw<`PPKF17tNU=WAY0VtomaS$}r#890iBhXJ4+;tjXdOsJRgnzYRt()AxA%JRgaEiL zlv>*EBWJWl`Aw?9VmDZnPynuJt({W%K>RTtNf^BU(HJI~3Z_xU!YPTcEa291BuI<} z7)>l|LVMn$Fe$*|jif9tYlWdWruO|loZ;j~D zxnttRRVD%yBk7CdNZl04)^B@V^a=(1HNuIT_p|570EZu?TbPd1YilWrJFisK? z`qtZ-e5~WWmydpbyX3g_mmA?!y|Hj&Wi9g8?|+EdPZwB6^jbr4qWwyQr??q>)}{Vx z(f;(82ExBk3#1jFf>+n6YkuO)9;p7aJKZGc+jI35IEFLF2t5ua;*0o#=4dY6!Vqe2I+yGnSXC{XUDi<3P7p*lMqE3 zRz=pDhDUp^{DqCUrNFdUUC~ULB+1&fN#P|=Ax@ru1E{aG`!1ZzT)pr(`fF-C@Neu2 zehS;a!E5`~QfknG;sA!WCWKa~$)cCqw~Eh}UY9Xax7`7d*qm!LG|Z`uCf3&XgMw5J z0aI#RDYOkIV3^S8@!Y{AT~G;JC`>V1UIMbau96GivutjEj6lGRWO>WflW%`tlgD{} z6v8caR6Dz0W6Qd&jl4{0gJnxm)GsEiGu7dVXlL!+E4P#N`OKHURo%B)GIex`=c#w) zzP+*aj?EptYn_U&rKhNidU3?VkP5s7RH`y>malQgfHCeKh-p zh4!dRN0be76>`rb5Vf2UFG1CAurhS!qo%bRU-t!H?7!$&O9CIX&pt*Qt2LLmF8cLLl>Cf9XvI;bLhdPd z+hsT#Rcy{d&*ueq)AO2`FiiKDQ;ms>0q%P8dl+}%V3cP#kDQZ<=44^YV>K3mSu zRI-$Hw>oo-NyOPdeOQPxiFPVaCeFyr)T{}x7eNQDr!uQ@%YQ7*cMWlID#D2YHd?xP4h%%kIz|itBJ9fcVSVm6F5ZzROV#w)ed&frlN$_hs!`nd%wA`+DjRGg{&y zg&pVE^nefUvn3`>F{GB~4f86_Qz9Zr)!Av){I+ET3U>dM0iIG0aQoUNv8$|(d5aSR z5iG}P86RO1?+h$B0Y9YzwVj7I`mRm0pX>N4NZUOtzGb)bJDmLZ@7Q4@K!T^s>|tgI zbt*Zt!WSB+&VUDwfU7UswK<>)oQ1PPN5zcMc`jRZXm-KAJ$j)W3zvdsPPvhsiLA81 zi_oC!v$I*$YHb>}?v_OhpUvMl1m0p7J|z*mq-^Y&xf6Ms+eG@>8J zNp!KJN?*oj-LjF6yRe?pIRJS9pWa#fERV~4B?Z|;ZFeec)pUY3Thsm@kmsNp(?1t5 zBEB{M%sV1{+9>oB?ek>?EGH2K zj&v0i4!)7ix^}=sB{sOhhzeU$zXe(M{eTu!Oom0NWK>mOI8NQJkWz)ZiLp#def>3e zt-s5kd_s%|EyRk=2tw9jP$xLmd|kMA6XWTE70_^LcjVEK4Kgpu0bbp%HODEOP?28n zTJa)x_ztUneGZ(RTlnsAZ(~hQ({9aofJwwmip$ipMpUN}@A;d{6TRYv6xfBY%_ z{RiU{?H2oT+5=x@NL<;ux#E$@Lr=zXdA!VC#fP)W5hU>VfTs5}65aRdJ=~Ly z#%#Lm3w+mo9#Rr%!&{mPc8yhxsJ3dII-1aV${Y(mRkRch5+d-I0l%tyRO))JK)_X* zcy)LE&nB9u{c&C8b8{zvuTC)v!7#0Kwc<`QIfu+l&M?WH7Bx(-50rTWQr&DTwNK9m zb@e`llDl7^sqi#@W}aVom3|;lV82RBl5eOeL4^Q9HCFd@;I#tr8A&vw$);?=oLZw_ z<_Q91LINqff{R3rF6&wS|UZ*M&tLeaFaM&g?#|eR!2ydSW4$?ac$;hr@azv72=LC|3(?| z$Nfa??Ja$u=azh{y#9*6zkB_TN3e-v009f` z+(6T*Tz3{;h-y^1KC^NraY!pFkGeNGDsRMv7dG9BCLiAH%IwA&v;K4sEJnBX!-0kb z4R%u%ya7%^iw#j4{1OYg z@ZpbsC%luWq!9O;$$v~k_J5aFWW@Rhira^~yAizt=eM@s_H@13pU!LOj%>edzCM`~ z0OVJp7K7Ky0{IR6d+<(rPqY^5KW}&cBR;GADDE9>mUXl_e?e3raR4``bitG_KVPz3 z?BeDck&oHKRPFIENB9AIl5G!)uI+)G?XBdb!r{jq`Aoo`T%}&WLNz7r;T^=rf$c62c~qIw}_$i1#fMaX5ZJ1IP_f#w;#MvEq}fVzQb$W9g{F{|M@>2Ef1VUP_UCFj+G}r{HE09y8-Wz z5`Cgy%YF1Pj{BP%zC)8vjd6xtQux+#;#?5dQ6krBwpnbc3!AaO?}DS4rhain(%)M3 z)AQ(yjjj6`0b<F>fDuP^BNsGiY)&7I@MIxb3oxHk7ixf%O^513_O?NMmVTCxx` zNM0ntpLyqS1$IP-7X~3aCVG7l_I~|lU>J+IsF?y3x3(L51?Ye3hnFXlz;!A>FPqG%YZRGZ8A~{PC34eXF7Xn<^RX> z+mg|eTe_%u617IH|GY6IGKJL{)AnverwJ+N;{M%FAH%(bCN(_&trGTMsV!>xTsI7} zkNA~mho1k$^_6=tAvhTp=ZQ`!YO@_oT@ocKWrr7_^)h|emrvSEW{(APtc%+;7agy4 z3rWPkfyXhwrZ*{cq3v&u{LQ60>p#2xCz@Lq+1Z?>2hM4~N%ZkM@B2JMB4GH;D8;C3 zvXk7$dWM!Ituo2rba&GNuY3uBKM4>x=MR#f-$C=o239_psm3yPrG~8xem5Gv1G@rT zGupp}PmwC3*2f>-YvLh}%`>g{!*Gbb<9aMo6+#VYd?W1-JAf^R2pCV9JE|2XMC`8S zSSTZbgdqUhaNixgJ9=_*`5Ypu@iVs1(yW6ym`92T5Kb{8G3nwC`Gu+^Fq^40H)%tu zovYG6yV@ri?ioLo^P>3s>fXKcqLIHT-(}kJ1L(d|hVLvihE)3$?J-aOd%-o1vOw%D zJtxC8RH>tzJNoYdZPiE1)dLTqkER?H-ZU8epnycdJiY1jk-}4IFB@Dpv-5v5Gh{~F z;z(dYo%OdfcW7w;6?gK|tRt}ij zhr} zJME`UiQFU99FmZs1%^e4J#50bg?#`lhB_fLEQW41pvaxA5^9aB zYW*xxTUq+8Goz#23RO&tTjc*JvEpa)vJBc;QglURTh7F_eUgiDE&_NYy1%bai1{r(xhSxpNKrIz}~(L|_IZFiP4N>+?DNUCc*kKPGz1rGj9T%Y{{zZ%=~ zm(-W?;T?MWlad6A;;L`vaQvvbWAKfS;)TPKEq%cbp17Kb_pW<5vw|T3dP}9n5-OrLp9gng z@7_Cc=FH5QbAIR57<F-6vp%rREF&+zkv7g|W4QND?j9ckvOv%;4V5KgSg{nT;OtfDu?YP#$ z%U;#Gbo>a^U{?ZW@K+)I=Dm@7kK_xvjO?#QJ|KsReb2D8$Ic9B+t*9&*Jl`0&awk9 zLx90jDOqM`I{)$F5w*aOjsVjihWjro?KDC5z@u1f=<%l`3I0AzyzAc!#%HYL*W?>rvw8U?wH>2 z2LB_V_f(K4Zh;99Pu*Qd+VA&!>Y~`^vR(IxUFH2RCWTQC11GKl)0)b2i)WPg_v54+ zwSG~@U!?qCaIgfsxn&OZ^n+cHCt-)w)%aZ!TYmH66*@BDC6>yywG*Fd7S&)*SHey2fZMhOlN4Sr$~VIstYU`XW4H~k@yzU9KgQTVB(MSvVKlt| zMO@VDE8HyIGfm14M5=1LX%v7WTbeh(SQr;nEwX2f1ZODxyQ=k`X9?F7CI5th{UCPO z9*`WJ4Tl`_ei1&4Nt;`@-;zz1`E4Mxl1zj3ttCN~NC6u%HdNtx7g2x(g~H@Jt0n;R zRNAmg1})0x8aL-rMm`CQ?}>Ed2Nu3kY+E;a5hO8V0diTm%Bz(OvjY&|P$Bo&*`XQG zFhX(jr#D02p58a)Wlce61tX`rkJ3;TiX6ycQjCoGYp_Gy~tddd^1pmdwZKSB{`tn+UFyc@#DbMF@#2P$4`FyPyO{mK$vK%ZV9BqU>^2pQGTg=5iVfN0H<9Y z#|mB$|4qhmVmT{Z#1*)Q4rfz7_U9KwBT!R+uL&+1bbO}ifin7n4u1SGrGr*{x0XQy zvN?ZHMJ&$}h&&qz!4ue27d6NH;YfZxm2!`qWNq|a{fydyHWF~bhq8u-b=Z=CIVKOX z2(b2}vg~-x-U68z=O&;U3MEsYe^!FLBFA~1rUd6lcDe9=w2NgQdiNh z-@J}hVWhx^Iw&e~(g^3?@oRUD8be61x{CIeQ;jxvU4Z_PQsl2=&&u6YzIE=)fV8cm|wCdozYoA+g~;_u66WRenmR(ZfgAbLCOMe z*fehMni>N)X0&x9r?$#YR9zaH0*9*g8$fIf6;1$~p@f)`XJPgUx6;r)h!s)pUW~*z$jV6ao$ zN)uK6gHv+-(C@CPsd4N;FEi58o}7+t*H<{U6EO_bc1n!Grp?^gWK!idT`UmkElsb8w zDNFfrRTH$Vqa8z@QjVV-E8^(N{Hp;kuOwwDIdzEXb4ulN5arWFD4VkISMkXcf|`1x zl*@Lk5lRx7+MN|L=JYj~b$)-aVV|!BNzfPINMM%d#Ygrwgypg&&D;huj-tZA)q!Z= zBoQyQuXt$UVt%x4f7Y~da?-ek2VKw|&W4Cc`{r_#{l_gSp!2jv9fWMdfd|hUVZYq} zK&vXtotylXO>Ws)|0?jZAfa9_IX3LS4a%V|K=0^C!K5!U9Z1B_f+0g8|Q4MMRkTTU!j~4sUdG+;NxJB87uJW zV0(MJLZz$#1i_n|nFTZ7-6n~syt7CGkj;kyfq|0UXT|>Cmd?yu>>X5^o0_Z|w-?eS zvsjfI0GzkIsp-$l0LvpFnnAw4p9%BN(gE0UNR0b#`hVb#SdKBg#3yBhBR@P9u_R&U zMS?vl9-p9*2Y+MwA1bjN#-%75;U$EIdY>Z!?UobuF#}jgKf~+#3WiFoHL}d9?VG0~ zPGe)5YJ-}0&LW7;agA#Sv#_71r@jH|AAz1NfG2p$TcD-NzdD!vH}5YR$wat2Fy&iJ zgeYS*{X_WZzeZR1luq*)1hrBPhSL+>O^;tW45OX`mO&LV#5>V`b;c2}7H1|W*+AT! z(CRBkTWmpAzq|AE_uqguyt=QSi2w#IEAgp&E`lIY$Pc5eJrSeYu4A>m(A^68^B&6w zS~9<9rg{Cpo8R;EyZ|apZnKjDuk6IlQ&PUW<3e|x2Y6JRk~GrRoTp=sWPOynU)tI* z{3zw$x3{&egE5DOGxH7waSm+eTo6eM2WG`X>>MVU?u=g9+5t@9AO+OO^*UNYekA9q z_N0hk>B4yHnnq0kIARpNqJmd^zD8gck$StbQjsx*s3qUmW$b z_U2}dXI^-c&E#|CV@|#v{26D6z_9>Vcx_3^kF<;{fO0!j=|QnN&5s%3HrdUz+;Y>y zGSSpSQdobesd;s}y{%D?M%ZJ#yR#FsHJGV@_0*e=8Tj}N2avubNAuvS8{w!Ae^&=a zwB*dAY!1=o%eJ=WW)H6DF1OGbJvYWeN(hUwN-D+?GfT%V_C?|Pb+~03bXiXN2Ypzq zS?nqI^xC8J(P#;F5g<3*k+zGv$r?-QJ^Gd#N_CeFbr3lBX_ag|MJhESuG^-zHgonO0p-okCr&1%^<}MdNc+UG` zM{EN%q~g0I!;Zjq9iS4nm9H`!8_GYv6!Go^(nxNvaL)yul{T!=gXp_~4|!ov@Wk=m z{k!nY=8B*}ASF(ey&2YGZ!LK{JO!SQ#rDEY&|Fb+A{O9w_2b$qxtY;AzDEoAIS+CW z9!^)whlk@jPtZWtIuMB6GIeTAv8(Zj=0}f&cjp~QtvvtET?gRgMk^;<8dg8e_(luz z9iN==I!V?woHlEM*zeZ`tX+>Ll^^+{a7@uRO}P2~tj+^;xys@Zuv%!?75d11;m`up#yNR8>1AJu7&@jnI0a#ODqcFA}#p^*ngz48|AbO z=nRhKpj`QwTZ{wL(wK$|J!qB&=2s z$4=e=b1Jz(wf~2j?Fr)_cGY@Gj^J3dp3Tp!Om*b+_7AF^VYXS$B8$f5f(lN=WxZ08DL3J1zL(Q)KPz>X{|e8K?9{ zP;j=+Hb&)34(Pkd&Shy3bJ5-gm~9Sr^pe+nJb%QU4u7);Pk6E;f^L30{Oki3d^8Fx z(!ulCiRYv+NDZxsI9yht!+@KFpyR7Hhm2i2Tyh@e^Dx3-@?w)5#me`3Bc|8rKofb0D>SY#mcY$!GrV6YlN{rh^9O=S_a=SN5la5 zV=Uo2Nahxl9yjlAM

HG{wp*;8dLT%Ru@w^RC0TG7xN6s$Jzau&P(!U7HO%WNHsf z0i$^N*RQ9Yd>xwOZMb~6TZ7%*=+nB(O7!}oMxcC2`ZQiL>Z&?d&xJNJJ8#N#yo%a``s6>)6GM>D1mlw;(L zm;=)4ngRw;KRi7WAM^b>>>bl&&`x)%ezk#)?fvj?a{tM-Uwiim|i4Y1nixtz(Bm&e+qc2~+aP+UP)R}S` zBL(+J_$kePPEGwFZYA>)s+scIy?m!xV+yvn-4u7&J)IFcn|JPq>gbMlK%X3f`lu%r zImp*2#YV8EDn}-8oj-9od;kG%Zy#v$$2rwgI}i8y5*FX0X+lYPG^#FiVbcc_QlYKp zC7dxVDS>0$zq+9gl~(g7z#aC;d?wyX&X)(7go$A3oHbZ1X_=swpnqF*;li&8b%xpw z!-f1^cffiAS=upg*c$>QO8DV0<}kXQ^8yH-cSo9XFbR!QtUT=T!?p%a@(QO`z=eil zWgIX&w=XZHKx$}?>e|bw-&PZO!Efv8@t{l~Anu6bEztzACNh~=B0%6H24VqTeu5WR z&u~0mkFkJcY6H&VTdpZyn0%vo%`3nxd;pa?4L+-!+Cdg3fL}peo^2qLdCioO$okiC z!MLOy5Yz?ujZe2i?EL3rBJ}r~3SWoL?b5N2$yK5T{^O5zzgzom&bH!}g7pL>S+EBU zaKXYI!}U5BxR8@UCOlL{y?P59)Y^S%z<*-iKT}{Z5{M#51o{fkI;)f7h_8y^$v-(T z7i78x-SD4eF4Fqw_A%Jl*nL*;o1^6Tn;eQ{*2G`&r*4V zMap9QgVYuBcjDc5l?8@Vw#P2^J&=Xv+n2+H(q4}*!MJ-RQJ3pDbxeN#Bm_9@wZp6L zFeoF6TNRnEKrq*MhiszsbD3KDuJBGt5uKbC+_B#Dm3L+;=(k`0!`<4P32DLo(Zg^h z%bJGDw`o~^_L<=oO=e(>UMv&Roz8j?)YNB$pgBW}m9LqyNA5k5 z7u_RtZ10F!Y2!JQjlIve$`z4K6UcN=8HcWPY6qlTEwb%Ug_+N~}p^ zKo?Xsbde^`%`)i8A9MH^?VG?jVxhZfl8aitIE#Zww0F{lhI3*mt+Ncc1K}0FD8fm5 zBZih$T3fKH^R9c$u5@i*V?pV{R*R8j2^S>}g=aUwXynjo=0rW=&S8qOt5X)aT z6Xq8P7{V4=yDrh!8pFnfBUHChLc{gs9htJAsDOa(guZN{WP-ml7k9uJwg(1OC2yo+ zyNM97lSQ9>%SWA?{4?i8I-arKzC1?GPpzSc1)nXROLlmxXxU3 zqa+G>28J2npsmcwI>^7-Yf>G)(HCA~hK@ln8=w)D1D!O@joASdJb*NY^?B+mxC0l^ zcEbw)bu@))OsQAvmBM4%`a|waR+_@85DtH+1}Gt=Cn-l%?5J{*P0XlSycM}N89{$y9xt#|`t@6TT4J3B`TJ(3mtl)-VHs}(O3s+fRJ5&{AVXb6*U_A0p_ z;d8caMy>6uX!{=dFQwib*QqwXru_g)EOvN_ft&_!=qM->aM!&dwe!MVHpsHow5p#J zO_nTX3nvF~S#?OdnPjFuY9LN7e^c97k51^!K?wA0ss(Hu0roBd?U;T)T1l?`pAb&cCT_XMK``mJANZ8X_yCa+1qu@N zfHa~h|2&SH$V<*h7dCCY0mt-?+l5>(4Cp-HxM0`0lvE0cA4#rw-lWM#p6KP=)oV_Q z>`Yk{r^!>p*`1_)5(2Yg?P9gB)zLQeMU&RbUU2>mily2nDn!hAC9>N)xX z_4xT5)qTg9P1&`@qc;GLS%>pmHNaJIWIeB?CoV{h6IEr&c2aOf{*?f$Eye4^OL9Ku z$dH*ll@a!pwbd|XXdfR+MG44S9X#a&N@K&h(R93%txN$$*Vn1OlmksZejQ?IDNmxTXS(<$XaT@DsoMY8wp*>r*M1*x(t*TxRWm&@ zttr?ZBSs$(w@R|FC2)q2(L$?)*pYHY6d=q0hi=*YPCtu^Lhcdkwul{6+6>zJ_fg0BB`jH2;& ztACKkHr4Lc8l`80-O51nfO_c=c2^gi0H&R9ohx2GnP#tTNLj^b(Od<~jmd>0fBZ?!f>u8)im0YvrE=N?N|r^mqp0*q+hln3dik zfVa_+B@07rzCKbSQSQ4vANXgikosWEK_pqqsNB3J(bk;NQ95WvQvLz>Z7^?5nMU_pgC|;@a|Em9)^BR(=6y_Q^DKMB(=|(^KXyftFoyl#5u0eVQMC zRjEA?NyFOR*DH1*m{5$3+4xe9tnMi4U2K1U_=EjLu5%i+ul>bs_GyME0-F6BQp;!p zfuXOhBpA=F+B~{LLNg`Ja0iU?i0N^5NQG$h`J`lL~#$^U*q-!y#h{{Qd!iYwA z*|m0$sV*efe-V^2&#hl(aEk&Nw#ibgWX$u9^57$YLM*JUNkJLo3l?^tuN$)0i~S5Q zYk|%Hj8jfi)pv5s~BHvfOC{WsnWj0!=@5)B0Fw zGGp3kx&|$AKSJ6b(jNe59BiYxCJs%P(Tr3z%#y{mECA#%=AqVw9 zkMGGAvf5;2XemrV@9?9&;g4qW2?&+&e)8HXAyYvVFvMtJAIuWO9oPK-v7GP8ugJ28 zIy>G}{Njosyj1ieQ_+*1*g7b&O_y_qI;@cGrrvE2*{3t2@e=#L3}2SLB3_cJ!pbsD znR!{+-+C)TJ?0$F@>}zxF_pr_e4S?CXO0=CH4l>-ydC~WdTST%wgh0fnlhMV)(hFE zSp?X@Y~nrwMy+kN8`&_PIZ1b6Op%WHTe-*t#`7n;i~u0X;?2}a3H1URt@2^qNjS0a zJ8g?HgJrHSkvW6vnw{E)=gANP*KSvJndNPIP+Y-jfpSyTcVCvdo?h&Zvg~@Y4V!hd z;3=-PUAr0jN0nS2&?ejt;6gn&n?PtmsR38&;+vGKA1?&s_8S0fmaHndm_clWl{1d@ zp(CFW;d{b~1^o*5!N^j7!3aTf&?U`y>hC42C$dy9aO$FazJ_NcfT;dK4yyH!YaZ!> zrB5&NF9e1#Kvef&UuUQ~L&aQtTJjH%rYs5?8lK+H(0qg0mkw7ZFXPrcBpHM0Q|X}O zMRuAceVt+nkYd2Q%CySWpyF3h5!}jlWSPd#Fx;bRlJr5z__!=nP`=BHq6#RXRbDzo z;lJGZR7q#55mVfPtkW{e#95PCG^j|U=pDeERzr z{zeXulFVQh0nVo$U)oQFS3yoCwP|T2G2PK@0g$_^olT_j`lgM|dR3c!Ne1LB$_8}t z79#3(L!!CMLxInYw>rN@KYzWwzk8j%rqd&dIg0k^mK2vB?|S=Uj%e=HM*<-OkFxFi zy~`DO$TR^&_XQ8x%8T0RN>j4{ahM}qI3z{Mu3{z-Y53Um&2i{#^PvZ|@GGLnYNd!F zYH(XVWe*{|dXAkj1GlD}Hnp6@PKuHHKZM=cXnpDkMCtuHPmJI?v?xn^fRE4j4uTp3 zL9PPU|6*Ae2`T^pBIaG|Q@*r$mrW;PHC*lTOu{F7>l3tX^P(YL#0+oIp#=X-S~uyVhUfB31&?b07kgDxDbaZAcTY> z2csx1D#DmT6OtrlEG7EItcEE9VpPYE;APkPjHb8}I9 zB;p98Gf!>_9z3w^r4ttFsh3s4DNGp)2Ip3s^qRfE9f6jLYPo++h+DG}y|Fju!w1Gh zsNUIp@{#BP-}pX_`*&+(JKz_(`{bGd;b74a{%41#UIaG@-zb&n64?Pb;%9LxZc->l z8$3$`Ylq?UvjvCm6ad^X7sddP6oc)jJh(WrX5t)MVp0%eB>3VdxI%D(Pwf5V-kL8W%wv~UF8yl`{-xH)mo zof2(+*8u1{YCazs)c`A7KN`z2Y!jY8c)DL`6#%A=k(j9j-+WV*?xm$Aji%>(?_AJT z=#)f6L=4b2MpR*tAxFWre}u1DLaQ`8r?89+58+Kvl+fd0H=a7Sp-eCVkXmOYxjaa) zwSFgGC~5FZ<8P|%haoZS8hv^I$#UPpEkN@bXpQK{w9TDD=IVejfdjNEuZ0h zY5oyH-`i8rQU`tluV#>Zq-&_si2bkXBbpxEtFdnv)b|8r0QFO*tyE6O?x4)BSZ!r} zUW)duVrJR((E#@TU5ftj>MgKZhQeD0!1q@`2zgoeSJ$~vprx3vMD_vz*$H@*blA6M zM8G}-feK=LFi4E}Hntbb|1I=^!D3%BNn_W>9zcO)nRT13N&W6=rB+!0w>+CW4`riIC988}*>Bulsgu*VWALw>P9cPG7@K8tgI zk|;6wNLZ=!T6-pCGQ`2*t+brnM%14{jD|l}Sy>UzO0J#&jEOQDQO5Hpo1ag#7M{7n zP8>KqxN!U(Chzwc4bcrsh2}n2+}Xx#xdNaal7Q9t73zHelPy^LDEasR_nH=+x+e%(Ku6XII~wfi({rXi|IHhT6`r z50-QB6kY=WyQufn;N8+-YhDM4hmu06q*db|q7T3w(>$7fdK+ zRz$!vZ+7)tPj`ml5}d6=NQB4=d500Y13!8<&PrU3{moCKDCA;y7pYIY479;SI57m~lYT9v=bvK`vL87wgII8j*Rc)f2uy0%l^|2qp7uX`(dIRU_qR`bj5Ne&`&>Nz{ zrmUPCu6pHYA!&Z!fPm(8-B1*NCC{AVunmB{)3Flf$;7hL>=FiPPde_TVWR$TI$x;o zofAWFK2bJwf5Nn%M58)=cIgY|<4rDlQGy-yNAIvl{k6J|w!kCuYEMKa`%`ZmO8zzF z5F?-_T$Pc;nI5F3%vttwC0XB_M$}vBva@zLCL%AKQi^u-DBO3xtsRB>y36spP}?xa z>QSVaCQe=%o&ckRH5DZ#CpekqgF1>8JJe^GxDR*0sdpn4SGV^%&~Ta<&?NYs9C(qT zr+a+S`I&C^Bf%8lcZcrbWpvboOc2kwtgnrYk_;eBu)Q10r!=Za>NEO1J^WY2$G4$@Hug0AQG^PE_zgdy=QmnuP>`tE8*w{6s3kL9I_P|JJOjsy_ zps%Zs4%y_HFmmdTO&~Qte?HLkJ)Ibv<>b&k*m`1oW(XP2mb4A1xy+$tJ=67;^l$$D zI0jzW$hbH=p6!}cu!Grfd4Afa9OL|b`lZH8R#b$Hc=bnG+~oJMvp3|~-;2y^Q7w%+ zr%?VlyP*~WpqX}a!*F?EgeBRq6a-33fn!B!@WQOL>m-8~S`=ks_zv0=kLXk0TX~B_ zPK^K)mO+4O2?7wqzixNsx*jW#E+FXsY3zT)5ZUI$;p`r(osNN+0N2>LxKu8UUM+Ky zZ0&-IGf>lO%I73265V>!>bp=-VNiU3#gIuuKtW5phF!Yx zmAC`@YOGMlvMv8@Yy708?2xX0pkKYjJ$-47PTT#9b4p?3NF7old2VO)fI4r@q zg&xJS=RG656g2%r&$~=X`5AI7ST6xLLPnkKVx1F^3F8U&gmMW0B<`M@Y^Ar-k{|YW zfkpaRno|c=8vqDlKoNaBbIbvQGQl%uK&K3^OFxs8m0e$wG?lZ`EmnJ#U3f?kLpAp3@G++_z?YtH zZF!fW>5joZq+z^EQS2YH;ml8@;a5Nit{VsuI;4ED80NQ)SOV--24(3`-zB=x8OQwc zt~ZOc>t}oTB7xwX|1oPfT@cDQbpJ_;s7mWbz>vy0N(V&+-q>vaY$7_a^5Qi})8`82 z0211G2+xP>=*i!JNL@R?>ODGW{pgCht+unL^iy2==-2s66}kyU#vNgnOmFNQ;nyYv z1y~^UJO(6LPgD$WYv!kWfV=A6sq-;{l!@{k{ACak<9EktBuMDgqzP)g2!3#-DBtJg zR)BYoCKF%s?0h8)?;=dG4QGT8@i7!hNLR=ok3QCGIKN${0}B+8l^`fcE5$4bc|vI^mWaoOlsE!s5>GQ59mEYWMf5B4z3<@GO+ZAxL|R+9}*Oey3u z{4XDjCR{M1!+$cWRIpM0SI=AYCB#TOJw;l#;q)Kqe*6!p2w%jR+)oM#HY$k-(l;|8 zlF`r0%jRj6R)r`w+%qc-b*RN#pTVoIDpmV;&=|0D~*>0Xu;QmPA*bFuMO{;WAJtyppgv4~QYM zw9}q^ZHU}$RMNiT)+ZanmxhE4QS7kJ+fX2G7RyayUP8w(dkLnU+d9g*B<2UsMFBh1 zpf>5CmQREP)C!wrD7GsM_kRFQ`VX{3QlYdp@(5h;?fZ8$`n)4olj4eC;SYB;?}WGN zyK_P-0K5_f>bR#CDt1_hA;qg7X7yAYrdaXaMaMlhU+iHVp0{Q1C#_=}KV zy;t#=u;)hV&Mu~l*2p`brP|7C`AIzo6U>~G&#))^04qxQ(TF<^6H$2BiWxb+pUez9#Naz?4_1d$~K@r!^Wob0AG>0<;LwM5B`kYT<_{YIC>G-fRYi#LCPKV_CXAB={7>WTwv`%*@b8VBr%;n~b+6{%`atHU>pi*HT&ldiVdji~ zYB2Pq57H?@zTJAvLV z8@_%87KAP?E;gJ+F?3E-&txrX?({Y4?>Ve&a;p?u--HGj_?n^^=4WT&F!$;xDH%-5 zsDow0Tu$LD%~$a$tr!Yo3K$o)^thlgo_^0n`>cb*hU>|gwoLq|g-_aEzeygZTN^L} zBC=3WI4*n#I9J%~zcWklE5mNzqZJnVFeQKv-EJTmLA+<)Y+YR|pcH9JncKjtf*O{H-RxK40Unb@D6Cl*{yHKHm$eUoOep z_tQZ{f_{YC#ntr%*jK+-nfUgSlS8n>Js=6ts8%LgVl$iy#wZ#Su<5(d8bOJy^k3_| zJs6GRL&Eqvgj7F^t$gx&>ub?lg(`YE z){myPH^9E94(eh3CGOjh6Z3yyjpP9g5muG3mz?@Q>W$sFegm)Pm<+WuylTz&BK0J1 z#gY_2OaxE5H?omp?4cC)ldX<4)NiX+bPA!B3|JPnvi;(J(!i(Ez35FdKX6)0?bn zm;etHnmGTMhKlN)%c&`B%C>U+q5Lfe@FVVXJgEanxhKET|7{QZJaYp!C62nQH;uBR zT=)>}UE{qC^MR@1PZAX>Dyn96bTk3`qu}YYrVCkYd_-*QgQt||0~7#kNe0}5ZNI## zOor($f+aR3B_&a-Kuc|HqHSi((Z{$oAQVKmop=6?6Gb^AfwlF;F__6c5ZxRk?O0)aiI!cz*~FJD$UJv_HFu6vE{ppNHv-?4r^ zWI(*`2z3eMh&nJaWcp3zEgryQD6WlAxSSm}(I}{>;6PSSM?wB+OpXH-~A6(z|+Q64Q}PfJdD4eDD<`p4%DO8RS-! zRx5fu29tmF)x7IV==&wP(Exm?O59!H=(9xVUf~#D#xKav+M0QUEN={U8+UL$Txb&a z_D-hk6PPSx@lu;p-C@baZs7U>p?B z7_WIJQnAiojm+#0-M7>fSU%$9lbIuW8u%G3fpKqr>_cJ66r23y4rG%9LTR6Sev~Dn z&?Q781zQaPVE)pAq5NoI7^s_VI4IER*_wKPyGWBndx5xAF0{$hQvLOvvJ*N(AEHW} zI9Z(t8$#TP^L!(QDK!Na5s=|H`UD3*x&k9h5FKCN=^zK^A=JfjpI_6_+WK+BH2bTh zB}l%Gw#ekBy%hr&T>*^SB9MqXRJ4dn2*joyff+QcDgww&i`eB|cMLhooHUO{rIMZk zE(F)mWVGcm4ilW!!uV~81O}N5o&kl;JF9ntM?phMpKPEYpuBnXHMqe|aZs1hf6-Yf zgj1egyon{+Th6vU)pIiwGDcx2Gx-xhm_jJzOjLN97WdGy%R-r0ubm{B7-74hW{NrA z)AJe!?FfqY*~v01I6Y5j-00}(QR#f#{#IbsV)w5q{g5|reF?>R`}f<$OSbWw2Tmj!WBQ9f8ejI`7i}rZhT3bgHB;P*9y6DZ{RhoZM zEzr$mJ*kyk6dGcAcKbNp#KL0Z7YEJ|MP{)lwM#J%097{%&+MuEs6QF*61Sn*!Hmej z*c23m^2Ew9NNGL@x&ULTb&}3bzIE3=gjQ-~;Y-vDd)qh^)Vp+AzA^?^ z`LlJ1r>drA!@R|h(b1jw%V3XH%Y)4x1~QNt7GMH4;uvME^PHV)nY`Obll%=tcDRWN zbF=#D)#Q(;x23*z<3muXP|L>`MIa%b;Y`((z(4vF8Yfar#5bwAif`7PmF$S2GpofP zn}MCS(ILjib}wK4CDpVZl#rNyu3hWZ)f3*BZV2#qk-65QFkihzBQ@Y%a+AxgiTnqNEKa+mFc zvMN&Fxzol@L$@k{-9z+EVC+w{&z$G{#7}Vou7QW|FC>bnbIOr`{RhMy*Yxo2T}Ku) z8g{my8%&J5FQ0Ub&-F&o`Eqa7P46A!}Sa)B%>z5WlU5=v|5AEbT%$yW^OYix*MYMEp^ zh?P=i?q$!}Pp-<=4H{RaL%_6H#}UFJ~&5L+oRv&KpSWTK!73Ab$QB<46q*yt-6(!K_T~QQ`xaF65S3uHl&IddSL7$Ahj1ErXr0x{f zuu{F-$>k{+@`Rj#6o^K{*VDwRQ*l4i=qA88e@RResn^p#(p5;c-IuWt@$-#? zH!EQY29)68YV;k*)O`6eTtPEDQ1=|42~tU21Ifv2GNuWyKfxz$AN=ZA=dQ;CMMrt*gE5dqFFlsx^40Cp_Qm?B6?pANgZ*%dFB*9SA%YgvFm?NiRk z5|NaYoJTg%(bA^an(%$I;Bvb!*T~U3o&id7y%sxz1~`_%+E8bpkzWpft@EMQQYL3Z zcw!w%B5K~py1GLvC#U{VLNgPSzVj#p?9E$SHtr@gmAf?W6~V4a>0C(H;Piz-Wp%aP z`?_Hh*nd1nsIo$6-x;9fx5K>YePn{=-9L_6jBBR2Kk~;ug>->OZM8dtNUci1yJNid z>l$glH{Cmj)1gDG>a@^@L{q+)`d$>mpC`l_Ixp85$TC`boM+7;g}I}FG=3AaSVy$v z1JU+RpV+pJ2f0v}BME!hY8=J51KmZC@$mx|Z9(q&ceqeL*oD{qzN+xPXb-yh)@8E) zbZlFaZuOa%oyW6%>~L<#!|=QVsC1SA4?-P8-c(ikJllKpzcanopF4x18t5PW`>#C< zs^h6Jp*1WL5{7QwP$XK0ow4(jncqZccF0v^XO?>9f&QEFWM%u2%&}J!ey11rI54Sy z2u(SkOv%dZ--oE-_NY%&94y6P??Run2kgAa^RTCTz-YRTI*nJOxO()IIPwPt0x7Hb zE{xl5ElvnO7K5_m7>tShufCI-vAZcLqzkNAIgaa$-;aW{Q`hQZkVIWlD-esu7Inp= z4Me}8Uf)STa3>>$`Q$(5)PFqG9N%#e`6kWoN^e9kEgfxbm3e)OP}*W$YjaxCLgUFn~k2YO2^X~NDY6 zV*zXxZeWmzh7A_s<(*#~ZJh0zsBBm{f|JJ686(*>wM5BFTyt+zcIT)K|1$HMtFf8? zYHMp>C|!OF{G5)|qHFgRj#F=>5ALl~Y&G9_nmM9;bBl*43TVxtrl!_6E=$%@3o0MU zs5M31eN-3H|EdTdF)abH{I3%&YNj&bA(dvt-tW;LV&0CFnv-B;loFBLTk!K{vg*Ya zak=t{#)Gx?=&dmkDDo_kL69!4(_>>1QWeF1k!uB)=Sl^8p#F(K|LHcoKL7ID@2=L z78?g_XDR#^ZKbdM*tYE5zd7dy4*75w<0I7iZHHzjGI@)4*aZUYi=LFFPgP;mMYhU` zbX155IKt%lO6@0hyn~CYmNr6wAuI*_3-yvJhw=bDp0xDH37Da5I$3ZpH?+02pVACk zJiOH76bsOC_xAn;qQPMa)b#Xe&o}kQL6To`b_7VnMQ6+3AP$1x>ZmdK2RwpTmd~O< zXZF|eWQKm3kQ0Vd4ULRgH5_GaWQ8Hws{7ExID0ytZx+u!@&3I@a=?_2C4}Qifvi)v zE_dQZXKGhKYosucBG~vomFo>DU@mg=h3=io#{G+j!i@M;I>3}cc={OJw4N_tD~9X5 zwYjeDLAJ5rYCdg#i8;?sQYk8*4NbW24q<41Nu+U8P131PKN$8glvi-m16Y9 zhD}^Da$*G>^l6n%?Fc82KiREtNf1lIPry}r^*$qmi;tiGF2>oi?fh_UVASR5x&a6H zOQ?4K4uqg$b z4|6hGr`AyNfx8(S6W!hBVxoqIY_;1yS(;6;SCL!po69sk3= z0gvS=N-jwbhjW$o<5iTsZ@jq8-kyK3CHv`2JbJJoS!35%FDO4+Y*=DR`9m^UV>B~$ zJkwR?{7E_m?+|h*FxqgBF16zJ0PR4JqevKE{;Q;D`OGr0#)O8(XS=`L zU|0e3^LGl*$v3<;&V!}YK>7$s+@kiioVg$>_7F?@i&9{O92U6X!W1|~8Q_VXr}^$r zXH0mX_mMi}$3ov3wjkBwvr5i?M;jwS%^@g7zUDA3C32a>g5-7cwxD#}%@Cm?FC<** zFNxP{adyOan}+6)MYJzZ%pLz-F?Q5e>uUV?hIZ#YLOgxM#dm7)AKWww{=@yE49s0d zG#D5p-p=0#RSwZ^opYFh5f6X2S-10!ZM%rqQLg9ZU-s;g!uZgv|D)+FprY!cJ${Go z25CX*?rtgRl8}&2X%LW@Aw;BO=oBQRLj)8Bk?szak`|Oynt6xsy|-q$);Po5d(Pgc z?m2t^{+noX8~$qu^X2I)&)GBfCD;MrB-tPW5&i3O@Clq0mkY7*FvyIQg5H&pc7l0Bec%(%Vq zDv4WxEtRo4Zn!-07BeK+JTsaljfi~zZF*-jf6deAz!Tq$HVyz zf_hBlAMhR~okbgokcZ{?N}o0z>KTiCz8wrzn)Jz#j=Q$Hdf3g?H#-z=s3!2{a(-#~ zryhl|vGG!rg{H|Xq2g$R3;yaMf*3HFipB}Pt*Ne#5wkn;q@UdYQ|qkKJmrkPprqv6 z>gs9=*!0Mssiy+0j?t|IOs8Nrllg8Kf@sOaWvUtT)DF}a=jj``;d~WzS{>+J94EjK~12AW90aRWFr&^-S^poz>wTl`XL*~ zIzC*c4xO)NE7~>aEBd*kN2 zYh~!5ln1I=xn=Qmwv%)3Hrz?#+G0`@ehm)djs43i-XQEmD;4QzY`phv^_|s{2y;J>9IWo0JifbDhw>4rFFlhYo-Sbf{;

zO`J}sZTzykh+)(DnX-wpL3!kz!^Rqh{S`>co9;wc&3=Hsq#!aOyPy+AAYlHXm^=Y# zti=xty|>0<$D#V%KnhX)d<71&0dc48&WJ{)1rV^Hd6mEqck^ytS8@n;9M9Wi)}V0o zVY<>KrnvhlmgR*0+fD`a02@3LJ*UY4=%{I3Pj7@Z^7Dy>Gttj z(f{7uGZ4VP4!_?#I7pLYfJdNK2mqVBsx)-2n20X4SYX3cJSOdR#pRwIGsb7hqS}oxN{=SPcv(JS$cL>dtnC z;}6TnGwWaK3kh)IS3G!r_iIIN0>ci^H6+6cNkkeqHr|^zaLQ|9&Y$O3alAx-mFSzZ zg8yX~v8uvaT=Lv9m z6*z#T09KAqgspapR(>8`3$fxDuB;$>*z;?ID2f6h?rRAP*hqo zG(gbg04*a4eO>#Zt?et|AVemTVl^hUxglZ<$Ne8J;o}IyuJ#FRE6)?3x@{ zSgg}wf)|}b?W+!=h850`OK*E^tHOv*fy=_!Jf;B!rBOw+ET44M$_kJtk+-YrZ+u<* zm7cIb*CthtcY?fD#6G%_WQal}{v*Nz2bWG5ag=`S1KLGPz13JJSP`dOG>%eqI2V@n zE!<~TpCMu3Xr3E zh6+HXKz4!ORCPer&l_Jo2)UI*YE%?XGs!M~Z86`KuSHWAH{zUV5iFa1pj|-Ox*n|T zr-FUB>k%(w?XH^~$xnCXJ>|lRoI{~kl8$**x`%mHr)_uL_gT+LkZa7b;|dHFe=vOW zK0N^&^Nq+)+yGBb0guraJy+h5;u*jeE3ov~Hv4}xD`W5ed-up`-w?5SvEhMM@p7j@ zRrj!b5F1p~TRca5TF?#zhR8?m?$oJl3Wm39N%e?v^M}!@9I2W|3%*B zjc=N}gQ!&O7`{&S3s;{vu?-uzK0rB$l0r zBmk--rMWgUAIY+-kD#zg%I`w|1m*=}L6q;5x~BsVP!SUwTmEihj#K6n-3xwf0@F(j zqukurzVa^lhjny_F+{MXDs@~@Xa3qK1Za+(G@j();608PKG^}C@uB3hyvh}1z3CaS zThPvPC7Gfi&9v@?v)f=u$>U$QY@GR zP`2acV3D@rT=4-KB}3v}?%0#9bk7t+IwufnPyG6|GeOInG)}9R4cC;!%(b7-VID_q zdgi{m#Q-T&|Do^|Y!%k!Z7nBZLK+~w{I%@(!jx?+C(_~V=7bI+QkRZ-*fslwZs!6Q ziv?gG`i|D8f~b%)o`urVC4n%*ky7)4&`^BW^}P?!WoFO79wYX{e~z*pU0ejBMgR01 zp0-P)6}TT2A#8Sx1aRwrfbeDXpHLmBqABkui?p$#6kxDOVoIgyf-B?+c!@8jK1609 zUBbBb^Rx@Ia`N&u*h_)t#C211^)r-6)7!m``3PVnl}B?xSh?DzIfRRYHHcs#OUG%s|)w1;=Q=GIcP> z+aEaSI#Dong;u|S8d};J1t_0I0T}}f<93YxuP&dOY|pB*varmHwgo~SYK^ovwS4ib z-pbfqT@lMN_r2u%YNixaXg))0KDC6r8%@)wNXQE^d|6dDTRpD&dO8hyJd+`%v>U-U4< zy1k+8FF)+ks$53rvV~QvnE+o2mH-B2#h1ccnS@lF13XPG5ybeTuA`20DUQh zOuHPDsJg${oIHXiLPU$w=O|-H?k*A58gVIfOHLmlz6$F*5L5u*LpAl0F03sXei4Me ze*Ib{mb{R*aqlZ(oC`M`M>?hc?!j+dhTUJBGcIs z?CPhtohTlc=QusQOD6x_<k2phH(zc>FT?y(wXJb!~NI2I_sFoYu{W&pVIu070_Ywy1INl7SU;v~| z)6)K)w)#lw_>{l<^ytd5$p$pXM2^Jy*sqD8Q35IWrIy3;pMEZHa{xcFP(Bj7`Ae;k zUMpC_>Z*hU_nW3T-mUTG%4{-0_x9;kfG`KA$e!R=<35tDsg#(?=`x>Dub*f@mB4FR$BY!^?esq{o#> zlAdtgPd_PA)S?x`*pX()MbamDoblM09wGd^dqGh>9uOTb)TG}1Z19`sal3=E(kEg2 zX7_z}XGas(ip%jDecX+pz+Hykd>7FT&ms0$pI&NUX_L~n7Z5ztj*5WfP9*Be9~4^y zWPoJkeWjc3hfI;hFXvAoC(0+PbLmR8bBj1J;sW@CLTKHv)`5D<9{dtaFiLtq3kx&W zpO(9_OJ+sof+cX{gmo(!OeX(JEpF&TS+EbA++$uJf@R=1x~zP3<=zK#yfEFS@_$@u z{!Wn>PbjwC3X;$@{d|u2s`o=JL0?TD8a$?V$;sFE)4HzsoMScwGc!IG?o~u^JlDUq zfVNwPBg8o$3)da?Pc(~`Fp(aVpf(Z6PVCyPdOhd$uMS}r%`AlZMvXjJ|uR%)ve6~N9O zCZmrhL>{Ex$ZRO^nIRzA#t3mqoSiT#%If(5&%pVh0^s&o1C z#E>7sZcHBR{HxBq5s2U6=eyZv1gO&pj2`U6Jofc<#t?|0Hhdm+ymg+aLJ0RCJnl6t zGV#%q!v>II&X@^MToZ;?pdK$WHlTF{_Q3@Lu2e{rDk^&Yn)Fo`u|U^ZI?!Z?f=Xjh z_|jxacZ>SiuV02Ulwp64@5_h-SfsL^_ctpZ-e6je+rvfH!2VCPp!OzU8E_w(;O(i^ zgA`Ta;5$Rg@=?QB!=hCW7-O~B<+VG@`>y1YPu6VBva{#9UQh7pPFFAPR;)RllY7;2;_#3N}xMl%0H~4dc+;? z)Fl+LSE9B%4;v6?u6&%c{dcjr!zg4OIStKVAEgpOK!x~8uv;}CBCXtuuiFRj)FZ}M z@mik$7YpKuaomh&@f6G(biSwBw(;5IQ;Ibdf3=ZG zYimL&fYybcP&;!o!A`pDzpaql<}PeJL+~LM(OeVH!2|??38TngAJ6MgABZWZ-LW<4 z3uiHlO1zx`-9-}*u_8_B@9pIi8p(z8tE%b4@ji5q0F4$@hk#&38Ue!cOPdHg3#%=9 zj!DXHVGhNjo{^D}s0@a(`~N=F)O@>!WFk?&&BY5GFv95+Qxo2U1${ye8gDshhW z+Zr~O07>~oV&i(XJ+vT;7;5vjEyK!#H9Huv$#08vR7Km97~2{p{jeXzSsEQ_L1B}^ zm(LXbAYrHDTelQ0gMsShw`pZG9`AkenwHlPg!~0=Esah6+l!~S8Ux%ZPeJSr+iC~l z;e-d^Jmg$s>TGYXRx<2-D`NH$qWfmpxdNhCaq&w(CqsEweP3eNDg}$oYB{&&kRXaa ze#XoY9G=or!TtT@QT8IP6gCD?AqEJyQu2F>2yxl{??n1*>$WQT)8UHLk6j9&?eh~p zIMd6cgC&Cpeb(iy4Ux4k+XJuxpMqa3cUW%f4D5vE&>n4-wyh0{{E?d z2-@GZwmP-1-L?%P_vhx4hZLqyiy`cnF~&cGy(lQGgUgX`JbhYHTi-rmiTHfg5>Jjj z+chS!F8#&SkZV6@`{a+aNqDScJW%BRh9Yg!JcwVCPXUS`|B6a#^^0xmVk6Nurnp6( zRq3n^{NvFML&z+Q>G-U-p!1AcUNxQVf@}-J2AcVQ;Gp)D<+r(K}|{i_0E$27icFOh1OgWIMDY(1NpSHB-)k9${b# z!9f(sC&Ti*2Qu=OcMg5OSm=aSNxc8-`{gUlP?ts@T32)hcKrkc6nL(XyY24ozOJ`=VU2NrP0VU`7h$Xp{cm~4mvGXqSm0v88=v*5XgN2c zFu#v)zdfjW*u9i!H~#nNoxQ zvZzR9%N&vR<(C$1D>oG%UfEF<6Yog%3a0LgZwd_T_V@AdNDOr}a54xeOZN~I07dD- zE5qjNc0V}LVFRCG0N}r{ySbnZ=|eiwLoz06=wuKs)1es~hx@j8LB9SdeB)}sD&uoe zA^ZTN2hwFO6FqEtdv)vZ8Kzl7igRP1x&64d;N$e=Jt09>aIW(a1PdFj&ja18*Gg+o ziIL*N9ZJ|`?%we|scA8ia6&Y?E6X-{5`tT!+9dV;^cuMPJ3lw6L(qaxVk;ylsBxBA z%Q@U9tF&|iIyX+r{s|tYbXE$cmQ7?v#Kb}R&;tt7Ki-3P!h!grH~jyIdJlN2|M&m@W$(Qy zD`ao7H=#1hmc2uw>~JXi*ekL^BrCIlP!f@mP?S+bM)p3O-{t-Je{cWWxg8GYoY#6@ zuj@Ik>v6wqMC*)U=tb*sxi;rRf^dKO_dLk@HJJG#Zr~<#{WyS){}K7ncFE=9{=0V8 zMFg~-`b{(|9g4h;_+P)yz0JhRgcmn_E~b5X{>9PWhl{TAnWCHm*Ag09@N2wL)~%~bd~!| zfu|HZ-)JS?$m_)T#hZ?MW(GH=rce}h`rW#mw0QV|_){3qUKjYQ2AmdI$qdci6R(E@ zmmQ#h$)_)cDXhVpWdCV&aGOM46I0gY;Mk9R_ImWNGGKFy{w^Vt?A3z zYv`cYd1Ds`+psM&P?W8p>BNH<*c8gQw+Gg_tWa0g%~gnfLn2_5h3>C)(gfromcyUv zG(UB_IVr$%CV7)kaLPhiaVp%(dW6CbC)E7w=5N9}Xg6D3o5Ho2k+j*J$cBC|I_>j? zH}#sgnpsmM3?#li7NC-S#H{{v|MC@1L^rox?B{k{;?$`of&e+e)v^!w2ZisIE*qf) zX(MM+u7{-P^i`{v+%!XF*}1 zX6EU35mr|eGVmn1^HY_e-*&nqa)|*+gh5BT-dLPMz0&!{2_Z;#1(RX&%^rc!p=qqG zk*hH&d?MQHM;&8>_bVpuca5XM#K6c1keHjXJKFi1@te`2iFdMQ;IV;h0~bZX=g)(K zW_UreFIje=8VBajNTiZ)-X4_)N8{%DmEK(u9^Ku_8JBfZ`Ta3j#ss@PC9S6G;-JFR z=chEO8oOQwIel9%M$uDU8zkCAuukGGK?Cb{_XtZ?u+wg*Rm{%@`T95?27Q@}B4ISD z8k76-hHvdFrSF{LLmdT@j0 z!EN`6|NL=fMW$W)9`l-!{D}wq$J{3Iw3-|QcRB|AS*r+{DBHIhKMnyG;dX$$(3!YI5M*9dcWfvA^ zxIsABpDiW`3IiwO4bB>fCynKlzmhbBqRplsaAod|Heo$HHj2(2M5!mz%YSkM+E%rO zmA$U)y5T$K%alqfAPOdExcGp7(LAaJ8^`rlm+N>x6hRp2Hac9qNq9>FG)nH$>GMC4 zo1~(x7puZq84#>D&uC<|dP^}(^l49(qlEO-^F_f7tvz}J*D-@hn%S5aZMNv!VXmg4 zLxY%LFEZV0UzpI(=OgP?x&Ei3N?fKE!2fv5+_y zwGaJEl|sI33p3*pNDRHPA~gSwz9)dNo116t^1!n|2M6`dc)M`;WsrZ-S1`(j5xrEpHq2V`zpn27PhGpahf018laH>PuUgTD0_%~Xl3ss z+GSH0m~3pg{{J0SJEAPu{P0O(s?Z6OEbq}JXtGIN|UIA`405f z@m9<;7JTdaF^`CluUv$*?hP2=g;gpc#R&^2HWI|omnrVyxqfQHD5Ng_oG)b~I}~aG zrM*^wx-`9+|*O2PyKjyZj1B%GptGuhFqZz0`Y7kE1p|do@1% zDlvk91+=-Q8(G>|SoCTe{npwv5b@2fb_YbH({(9wzFhhf;qeQ!O!=e96r_|_Bby-Jzv$Z zeBS~6;fw;%x6BKDr${*!zyVC@l%EkcIwQ|68&kpXh@Fex6~U{<1$|07-3*0O8*Wd{ zYfKrfhWm%PkVTmY$j>_4gdR;%XnoHs((i_XoDcC+hS=yI@p3(66mX8!B(A}kGQe%( zaYWw*o#J)oZz)w=sE4&P6yG10MCS|RbV?&_n#6rvYuDKSS@Dk1Xt?C8$6Q#CzD&G1 zpZoQ0QlMp*$2=YC&s7sFpG0bn#Wq%Lm6sBMgt<$E3Tn#RGndEg(27{<2?T094A2oQ z?#)u;%$|Ul^5MMce`5mPOzMJBLc~WYO6yknEyr{Joh;7j1AieLA-j2CGCM@Q{p>pG zo_yu2E)I%(O#$S#Mpo2HAv?sze07$elX|rdy~sVim5t8W1VZez6rMC0MZ2k;*wFUf zrIAwfWko;b^5y9}J|I`)eYG-HGq(1$cu&G|)TQ7iJ%@6LNjbjMSi*pG5K(U=PNyht z==lDcGob~F~-5DS)fWF*UG}s^d zC>Y-;^BnE2Q2K)eSsLn2j{G_wtckxk+rn?T_D1#1gb8Q)Ij!D~( zvnUU$HBws!ary<)>D3s+Pmp+A=x~sHV}7+TCc?V*>AmmOlE{60)bpQ5G0iRPdO@(q zu7#tyxIV+B6~p&1yJwU5gqg@VgAr(#x@izUC70aN2Iy3*RIo)q93t=7tD{i1t>?!l zf(Shg4|JojGtjYn@Ba(uT*!&4}j; zjA)XIbo;E!A;WTK!*=wJc}-3LO+IZrCf?X;s@VDBz@iJk>XU62ms3j>MUofZ-!NBu z{*$%g>x3v8TE%@|Tl)YP^9uV+bagnjhJRA>9Ze4eL2ovdpQlZ@C4U0J(^_;EiyoFWLIz4&Lu1#D+$09P^%fH(N?1mI!#DrQSe<*B}9;Ok&TUwH> zVpT{dXnwbdxeP_nfZ)h*PhU)5yTl$T(ww9%GpG9Bqa*8pYI$|6pey>KVWCOJuHnJi zBZUkL4ZcD4kQyo8neU_fUe}C)oe*?iIJV%l=W}vo9jCb3P~oY?CCYydKWC6Iqan#9mn~5q&YHi7n%}1i zvfx>19L$8yH;Y69CR*fW1|oQPZ;DI>&Bp3Ck7pEsRR=Z>A|%TKQ^=U*Mw z7ySQDT9p!m1u5o0!{inymJ;GIJeZ5{_!x-=gZqkERRk#yp4yqlJ%K9Od$m)J)lRs2 z(XrS1Oqqj%$sq`);HF!0?)8y{#AjeYrj?ua0 zMNOWh>Jw9{l?R9_l{!rE6Iwe<&V zF*4bdEzD?=)m=R|^)!9oL-z7hxR6bfm(z!`ukAUUAs${ojEKW6d{gqXRPFn1Dr07; z;9?@IOZw;S5w*%~AP4_&(GHRv28W-Z)MV`bno)`H zIg6VZyOVG!g&j4e_8R{0-(L^0AUXR%i9<-Fj<6u4pn3-_3I;wJV6VmUuR*;F;kjWg zwn@!7a^UDXv@o9H6nt{VE8xS+TqeiBcfUCMY^V&`J~GBLVwAiQ0@DoAr#CG@XfRq~ zI;mW4b9bxVMTmN$lKg<{3j4yvd&8qCZn5^Rar8S4E}a~P&LY}eKEZ_X&vCyiHsEmd zldo>RgDXwY8tAZRK*AxAG`Q$)y^r#p)s9qbGq?{t2;B=88QUCU-P*_z|91~YvlVUy zyqtW?zvkKz^vsGMh-o!y1HJDmn$r7cHm;oiytEGsB=|{{es`GY=km?iE!oe(5`R-q z6Oal#D1N~*Xj6`Q44k*-?J0hIBS+6~?{q`$>KMqWXTFpcPj|IPb~Pkv|NoZ}mLqj4 zIhueh(5S(!N`mFuE{bif-Yz~6Q-Rx2cU;vLL)YOVgNV0*bt%L6Oi!j0q+xr{AHWPD z_}9`~6315;lf+#2Zr;YojZbv6w_n5QREWO%xE2dJbqt-eMDcJi{lhA)NjG1IbQp~P z_>}D{h!@_Qe)*6@8K5DJL~0*EW0ZA%XCmDH^U^Ol;^Y7z!&t1`bLs?O+ z2@fESV|55BbZu67-{*AFC@fJ)>;1n)hT{Shfi;O=q>mTCN>@aEbvMDvSmIcIPEWI% z_GX^R%pA8BDgU{8ZWu&7-k^F*gn+P0_RSOf`Tj@j=ODnd*cWBVqC>0kY)O{x&)%MF zl0-nh#3ttOZiOY@VX-wcjvCFsl8h_iE^pP^xKo76gc)&f^ZFN&WNZ}u_u%k9?TEm# zyH0W3AesZw>IbcG+EY4s{CAPd8u{v=iFQ;Imf zc`rzr{U2>jLpXuA`+bST#pPXACEx`p5+O{sm~LoJKvO2YqhdJY1Bj>A=?`ZyAx1%< zleBMXnRI)ZCS<;z^XIMY+jW!deB0PD8K7gNhKf64guv~u`#*a>QBzoeui{C5?=Td3 zc9!_PC-Fj|wBzlmje8N9)6MO}FkRv&1AsrEhYS1WBaKGP5ZNj2hw_#3g`$C5mQb=N z&YF82&LfG?`NsMO5Z8-!**HdLcSAqsMtjP&I)5wqTgVm0mYtH8uwomeQ&f@a?xeb@Rpe2itxEGf`(C z2&xOO$q}`12=QH|MRWl{Pmajp{&bJJe-;Dut(^I%9${spj$36Lh4U27y>2*aRi)!X zG=2`RmApg!#|}_hjpP3gKEpZ5>4&O$v+%sPA>$fZ@^XGEB>Y@e(#$)9lN|@88Z{~F zXP0P!aLQsd#pB1z_lSEeLiSd_bAKa1K(6KL_LUfplz@;>ZoRySKWXGI^;H^bjdk}c z8M+HmD~vi;1i-QK&N_x6UTW+d^*NkXSP(rt11%xdw-}m#`ok4B$J|l+aqi7+{jmR8 zBHihO*`oYdHqqEe>S}9ij9^v^|7WG8gB_X?Jc$zFzZN2ohZ;Uyn-KU+;;{$Q^k^oJV{4s?vE zv|h9C%c}DZxMnsJD0szox;e))#L}8roR~@C^h~`iBs?<$>eL_P5p|&F4jrrX6dfc3 zMPOLZGK=M!9qaiopn33bIJMoA8>tL8DHE6vW3*UFfP<#Ukm3tib2_6%lv#&%#jCn) z96z7vj*abBjjlQQRISZiTDCu9RmVlA`Bguc6lBdCosf>Ouc!)r=+*h*P51=oJ`sEISgoz;vZ(a z_lqkHK*3CHqclSstTN=Lefcs`$*Uq$8DLfBo@sI^i@InrM93y6^jHWR>{;=LkGi^x zD6Gu0y2|G5RxV88XWu-Wbe(YC4XaR>te9}{zb59{AE(q8tcw|H1NE-!ZZb&xmiO|v z&@PrsL{p8cuqo3+S^L7h&*}_cLP@Jn ztQ6Sw+DFsH{E3Dj3w_l;MX>ryqgO~P(IyBW&mkIP7K4VPh4Y&5j)28^C910>G78>= z?;s3*?{OGzhuagmTMlJOYNrRAc*i52tKg1xPHNBDZNrOlb^NN8pkCK#e&-hu*6g$) zd{BKj-4GjDK$^?cb;EhGe%8*A_m@!!ORa7zU--uQ;$8JPOlVpL{}#vo?tT^)=#x#B zMK=^Lmq0qJek~2XeNH`Ip;B#!#-5l(frCV55$%J(`mYLN}2_QqbT>QQ;ts$9>(Gju8ZkI#gUe^kUffHk8-H)VL`?aiagzG9aSI z<9@F{+q4alBl*E43S3wOq(7&0(KCHuTbvb!iQA!;DZFYQejf{S(U?%m&Sw9ogvnh+`U2mS)rL(v~C&7;edCsGZQ9cFG# zr>7xy-Do1Q&>ud*rLS{t6l||eU6Sh1_khYfyfemC_+=1o)BfyZd19klrN%SHb|iII z=3%h3!Rot`@8OAx*jY-|Q#`xo=SEyJGcP<9_2xf#iQlP*TnBP{YUI`1x6^9GPk$(! zn%xTvyOxo)erW#|)Wa7Z3Wn>1_k{(0vAwt^tFI?zGu<~lJ2hpUtQ0!FB6F4}K&?@A zyVfaV7teU;k@#;8F6q)hHAn-y_4P42fm&m~-R8{W@xAYr>o}NTw*|@Rzv&gxqOQKY z7r$oq7q@~>PO>v}H_w{yF&4zm{uPUHX;N?rejg(a=z543=WhC9r`cpxoXMm4RF_|4 z0slfjqStro^07k~mDZ;DpfE6`$oZsT3a@&V*DUuZZWxO(LsOo&NWG73u`riYIY=mC3msi7X1T0bG;{Hp4JrVaOmh6H&OHF*25n|VJEwL$vyt!Ti#dzm&{O-56q7VLKypw zYyPfb&gL@0r?(waeokqCE?!CF86nL@v~M$%$9Cfh^!>xZ9MDz&+-Rf^@+1;n$1)hR zjJLqix*`ZWi`OZA4>P$+6w?NWv~t3=TEmkk#V(06jkwrtE#va`jiIGroY@&J7FZz6 z@eIeF7PLa84Lp?Tr3qNt>vKep+Rii@7D>lTTeVx?I88b?1bG0Nwr(qGbsRwSn7t`ic^^Y$lU_g&vVUvF$-I&i5GWuwogbxqN zkyJjVMq8|E1FMx@xb(3(E{eRzLT?@*J@4SiCA({dhL^v>uIRzzr1?I(lCc};K=tB9 zj{D(0jtRdLn(6$KDM+J18XFWl$XjK|h>U~g`xYoSJrWmzFUBIa7@hH)SSajr6y6Hx zF7x+MdFv$emYP2otLHyeRyQ1lBcTuDy^ins z5IEgjT3UMacWKFDlMF)@q)aAw5CI$wKOiYMc3ni~{a5(x(QXO~3Lz3brlfDDLo8Zf z(ofXU+PV_^BxLVXi(kF5QM5GC9A_$`f$!?Y&*V4LsvHPTGW(uiXNH!Mlw#{G?w3lHTZhVsJ>IzvH+$VH zS~QFR^Wh-c2Es+5Fq+FfNeKhJTeZm3n;wTYjSt z7EGT^BFi{S@Ik~XF%f!w65wG)TVd1F&2k77nqY%n4 z)z)ISgpGcd_A7|RN9L(W>>2)|uR_hJYyM;rT3ax0J(?XK##TL%j_A#+aJw!;b|tHk ztUjo*E~{63+=9mTgl99TioG@}VY@Y;ZvUe>E+5nK{74DjVL)$6lB8cB0l~eg_z^>o z1*}cv%3))jT6Ovk9p=K_ z$2*yP49{J62IsAO(71@JnEF3}6|k^;(Kk2G8HNKI5zyq*skmm+@=WBl-;*7H9?QtF zYaz2RHao|{{(9M;0{rJ24Yb$bntyFwT_dPkZ{ln|H2jE>k}T6dIxWjv<(oDyE z17rod906h?8R5f7)kb{}wR*YU4z1}T`)q*~I&6#xfmg#FiqHq68$X{P#mZpA@eIu{ zc!)G>l2_@-GY7Pocu%uVX=Go;xV!W?TT+jH>4>gkGZs>2vmkXU>H(+Pr*022R&;;5 ze{Y_0&J-s~b0$TW*q(mY@+l6spv9(ynENS6tIcJGU<~uBt*zSqEz>P(P7;K9YSgMV z{+r%9H$CznO5n|#H}u@0N@b0r;Cb#C7n0}{A-$p3$!xdw_30ggGReOnEz-vTG z41T%!aWxMd0tNzX3OzyJ`~7daHpc7*|NdhS_c{rTI)p?U9zWVJ%F-nJvnad&2;JOu+RGJnjY;bpP!Y!p8Z8Ne1n7fj%#m63BM(JGmpfm zJo^ijRG47Uer-hmw1hw5!o{}`f}1U_nWbIZJ&(wjB$D2ON)P4y`qT5d^jlXGijwa@ zsj+Lvjct2gTyDDi}_Cs^&BOPr0nmKB%KF=#Y{o1EQyr=&TAoR@0i`7As8|iEoIUhp^ED952?nx zyUWXh1S&Lo3I^Z(UE+(wD!(=9qC=2Q8Z3Q0{*|V_F-FNp5_;K-osR#C^r$mC-n`N0 zD&V$|GWLotEM(_G=`w-XJ9%THNBGV7=;(v1;hr*?yif?`0$~?NG~S#$f5ONx?e)F- zWq%9OzOv`2{)5L1QIGayw*PiIUkAN#A`F&RUV&X8QlZxZ|+hjAy|CxEn^Gi+O zs2VBeH{E){SADR*@EZ4KM8^V;`zgi5&)CHc>+t&%s?1LQojhZ2_I7rJ6_n~^nfuY% z`XG`PVjrJjeD&mORedUll(OEx0`B9e#57C}m_j1skn_dR+lNf>S}HRpu<+X@0+CL~ zZMI3ASM?Ln!zvaH;U}=$gpglN0UNmIeNn+~rVbC_d?(BYvsjTg9P<5A$i(Ipa z=iPP&(#JMuTn}HNDf0a~t({WGlHU;TNT56a%Z1~xCei)hg2|%07o(K?(dU-0-`ar+ zvtKgR;fZo;+z!&AxJ6j`JMyi|^}K+(;CZ{%zkmDOqwWSHLE6&{>ld-}C*S_Ibhvv< zk*Z2uTP;`Jq0lS4bd>a6eAe;tzheHh*m0HAZr^;Qi-nr>??=JZ_u^E6_KJq1MEzG; z?w-5Y@W+rrHa)4k_53@$LGGx0D@x+&(!?r!l%tEgGnZ}V*xu>xH}e7=JqGgdmw#}I zojyb)@W-JIXc*$q6=7C%t}7h#cMk_TIQ(dNu-SxefF1ebE-i_ULc;q~){j2^8j!?Q z@Dts$r~DT)c07k_-zvsdqS&aE6`gohd2WdQ7AL0W!?x&gmCa+`;b5DNECOECYLt#p zuc65CI_br>vlT)LQ$q@yVv#Qt`~y(}sHNBj@g zk(~<#n-@I@ZQ`Bd^!bFJfD+}8-0kS!uUIJTToQA-h`zsjR#9;o?9y^hZqT3X!o>xV zB%R-H7g8o@nZBg>#QXM**-?J-{AA#!YINV(&t_8cPe=aNdPC-QN!)c{Z9KXfNYrug z@}Nvl=W(H5K)Zu-W=_L=(c#_b&JH_2T6$~=!$BQciEA5q6cw;OF&J&drt?lWi+y|1 zgGL;oG~2g#RlUc{XI41MFvBQE`ZWp6tex*2Mxi@7_58!kA1MzZ-8=h|r;DR&g*+0! zML8+jme6{-`2dSpyI~gI`!@o0C?G)R@in`$rVH7?QwXN=bQF3I6GcCmYLe!@becGR z;};%YC{snf(a2OgIMeiIg6(m(34tnbo)XChH3ApJL`4NBIa4haIe+eB9+7a~IC2?7 z?dZw;l0vfeQm=*$34Eax7FOna)|d6L&fm;C=GvrIwP(>xOvHyd6&UYoGhjMiz3R;v zSHAhJ{guGf(I%LiCFxH!MU43d_Y}GF)pyK~qp*4VznLpSiZxL==jXAFjUkW@N|>k;DHEKx-E*fB4bv@-QNzo7QBN!(|Y<%TO%0hR;oDAkG@ z1@4QgzG)FvVQY~Q4E&4AqFf-$0GY=EhU;pF_?2lM@OXdEToWa z7?Xv(StsEk?!IM%E%y)^o|<}$rz?o`9`|Jget3A1YQjkE291{Befc6gyvwTz+KQ*g zM@L$*#|O4`fUJZ8X)vnS{_++ibY(f#wc%XTE{a{n3SJ|~O<_Pliv&9&?;3%W6{Vh6 z@2m!`+i{VB&C7sCi2@x=R=jtkyW_Upkz$WrVrpfs$ynRaJ$xaAwJG4S<|$+y{oS8{ z4YU5+FpKW(s1E<=;^KM1e)Vho+D%@DXnWH%stB`d+aYZj8YS{{u2=7M{sKD#_9%&n z;~~6N$D>*gbqVkAE|0beG-h78rO223oBsNDu6ky_QXM?`WIBl+-n`X26&S_)DGA2( zwgmo`&M#XnB3c)ZMegeD7t;4IAOnYPhc|Jy)?elyeC}X5up7yosac+w;d>x@ee3tc z(dbeOGtAfG zz(@Pf2SKUt9Pd48H$waovo8&|MjU)q$~`1GEk|IfBUU2(%C_hbq=V&ck2TS%?YA?; zWDS)Rtw+l+Gewf3_)R4lOi_EQt6m?tM{{;B`UeEG{JNaI17C$C_m)2nck0E~iO-Sa zsVFM`7AZ{GcRCRJO^S?=Vt@m2#n{A^j{*4;5YIxGzNhY;XWF4>#U@QSK8 z1{O>bK3A2-^}AoIWkR9{RzDh(;K@s5x|e)NwfRFr;rNEZb8`gcS&5)#VxE%?YyVqW zvHrWVBIWI%Wxd62O5OjVvG)iS|B({~DOXEG)g!FmZ%;`W_ZB*K=j5HI`E_)oZwl<; z!@RNpIUb_Uwx3)m#CeLBM#pg|=Fu48b%1p`8C&JjM|iysNse9wj_uDx`OW7=7Im=7 z=Wsq!14SVQzc-@ivZmELX0cl{u^*JrXjt;CT;-Fi4amOF0uW_xP;Y_{KY3h_Yqi2^TOBS zRy#(eAGj@&u?sC zcXp(;r)ijYZoqi|=J5X3N+F{gM#!Ec&VYagL%3HUHZVTi-&(m`+f4pvEJWZjsqPRa zmKiyiD*OES!c;@e7%pP`6gEXet{(@4V@+%97_3|AdA6lXE|dSrx>(+2sb^+j_wGIj zAp)fGA8d+0Lr<$+9O=D(bzU_hAYiW^P!iR_hP1(kY~dSp!>7BLo9IKTn+D8Vc1Gt} z&SRnc2yj%S#w^EKmxRELNd_@zGJRCnl~rOQBqVf>aZ9}q>VM7<*(J9=d0D)IJYQKbfhK^MXWDT{f5bcH6 z(;lb$e!4?VR12La=YlkPl zXIBS1zTwA#wQH{zA^=~ruDQjvlyCCwC^^f!2p^#>j5MCl3b^10y+2$?W@ct;>Df$N zgeBcLBQSIp|ftL{{F$h=>A;PvSXjHpg z6cYy2*k)KSpnyn{h?dtJ5^k9P4G7S6NkxeWpmD%^1b#h?pd#8)M|H3q-`9Y| z#%EQhtt`mN$>l^|*Mh~(Q&7_870$-Ys!GfXg>M-ivX9OW zpJ-ZYyPtTda9oun`2zHo!|slXMN$;DbN(S0Bv}hDcVPi%@o#?K`KS6+iIRs)=ifcx zihUKy)krT=3u$KO;HcQ;xaiiuxhdguk0SfTRbXPqXt4B7%bT>zBZx}+jo_o^t{l6muflAzA@#ydr$xT z)C-81IQqgEx*bL?u4O%Qa&Yj?l&t`)s#>MKtEYA|3;V1<(Vzosjm!dBV;6t{ez5MJ z95n(W^lpGT*PMeV-P`N3iH_y`Sbbb%j-!*%L>^cp5^~ST7}M^(cfa)7fLCMPA2u!b z)ZFa06ut&$HV1kJXbLs_9vjNkRIZg;_`t4>r43fVliF!|~;zqpl<2Jt-8_a`q? z-g_=4)po(^RcPAwLoYnJfn6v{R5Ihk9z1Y)_J>ThPRTu}Bf`(~WpBehD9D0^oqdp| zIJ+lue%0y-;W;;2K#j4?^9FpnAor=Xae#A}W}vyWFzl zelSIm;-dP;EmcNBV@?+ArU)Exii^iY2-mws4zO&))fsdn+7TFGZ?*5g7nP$$nONG~ zM&=LgSnMqmp+Vh2MwIqvU2t9PJDy#;&ktN=^0{g(rb9+|i627c?`ccF8zvv5Nc|N; zvS(|(d`|!Z6A%>i%F&sgkR-Xwrq3*o+}`q1|LuC58x7R!h+1=W46+i@QT;i8#ASFX ziCAi*qh#;;&f|#5ed55U4_6>waqh!iZExZYj+Lv0zu%(8%QaEwmCe<*IO6if=Zh{` zc4YP+SD*_yoCZ`=xjOyOYecyLCo+D8q{K>#zm@PGRcF-L`aSWX*iB;EP@Uhd5(hwA zkT4q)G4ZM*w(R7p*MjONOzYFMG@3})*_P!iX9$Y5kZU(wSC0L|eVKpDG|Dm?1hE49 zr6&KD*gfFEG>rcL=K|TUR zQd3hi>Vu+QD1v2>!AV+L$~4_o(q2%AfW2Sz<2q&Ks*XWrBy!xCL|8|`Ne6MpAT zDj@5Kz49&@GYjA?`m^L;(;-AlL;#=Vn?oqPt0E_70iUpt2MD884O^00s_;`s2q$wV z4zPSK7foF}OP~eU5@q}+@=Ap`)AMgHzxNW3VWH(N-NjPyOw>uu&3t{d!8sMa zF|_&`cYel4-k}fMkaxb!Qnij{L11m0t4zD7}%nV*=+IL=~?#wzR*9gC# ziS6B5j|Th&FHy~6ws)=R(^rF@MFo93Fb*0=U8*9Do}d(}YCq5oz%dAhTalHWJ$qnK zLgrCl?@OoejADkEKcspk*2+ps)i|Ak4_25kz14<-2yPXUx{D{Qx$`r)s=E#FB!z@4 zoGqW)S3RQ;X}9Oq9=lJmy5ab!uumEhL!8t$(K8vhvd8pg4qh%_#zlRYn{yZ;RJ0Bh zpV2W~m%i)VwFt)_+7Ivccd)uk zzc&RNtAb`{LHj2AdKRl5z=&JVx_PaHg1O0F)y@|WJ91J2VK7=;_DPMakcXTF!_i20Eo%kN$&m;D;< z2JOb&etU88B>qX66uMzvGmLfHWd^UlrNSy;A`!F4`s$NUa3E^TEG!aueK*J_y2VcM zf<=oCU@mR3-2t}6kEmtO-Or*&ya2&tcAi{^Og|jM9gni;rwo5DPl6l_bSycZ$S37r zk?j$uLS#5%d0wX#Dalv;kPGT1IaqJRur0Gq&XeoBzuUfH2m7Hxd0%0UY@0|(!L()< zz3Nqcn(s7@X(Yx4h6t0ubVP`cj~ER6L?<(stqYv~qA=7KI)6VH5w|C^UMlU|us59T zNJ)%m2v*%RKgE7Yi@^JFvLIh*5&G3VVG@n%`ffpC!s=UVllvQDk3QpdvauDMup(y68)jef zRlkG3+<|RE$x%F%BmkaTEXC?ta;txdz*YCK*+-{qo;G&_Rizr8oLq}WqfJPXdzY_P z0-4E-WJrin#e?MAxA>Cile=?cEseuurc&Z^F`B3w%iFK!RSow(D*A~E>fls@6@J$i zN9SS1?;U`FjnBU|!$C@wxs?5w^?rSQ`wrT?8AfkeCnsU-7CQ**8@%f<74C!97l=kHb}MRPX!225>0I=i0P(1P(nsI^a=?uso{weNo0 zNe==jV>sGXH8PYT+mY_j#C>uFaMoK*ENItwZDAn##i!LUnib3Lhb73hyo~Tzdw>7E1@0VmxSqk&psR3L=2!7>zS#kepq(#tya62D7>E{o47sb z=xYuDoiI->kWPQw-5 zYu4Qeu%z`r-=4`*n!tFG5yhf zMu>i8;{8EkFk*4hLb~q#Rqa?Qocs;DhpnGzM^i>wYUy#VURc$aUHRk`=^u8_3I}0) zFD3{DNdUZ+(Df(8Q-*qC`6V449qyxjt}7$_H>mE{Suv+e&L#EDeg_ zr9bDJdNv=F3K0%QBHlzc1-=$DQor>w(sTDqY z#Ls-Q$kny0X`(uuQTY|v;ktkRHCeYF;w5%Awi`tg2|nNx{`C+g1-+;(!@Uat@Fqb7 znk^FuNdw=$F_#=*_@*S*ey-i4Sl=&mrRoTrw0iU;|8bYUdsC{e)i!=(J{zm*bqW!_Wc)};Q9LD(v%)@{(}O%+R26@5 z+-y>%csE_YBu87RtST$3lkq5Z>p?_A$HcBmLk22|?i2>0{cxuXnQ?sCYwzwfoEM%9 zZ4VsTx_ZBR>y8Ue4KEA7>smw^%vp2GsdC3qmh?~nIA8fD2276K9Qih z)?~nb=GI$XA{u-bIv(=?kLcZ9t+C{Yx)=fZ0rPx?OVtMzzORTQg6m@Z5X|w>O|Gq> z>joNgs@3Q?xvjG$4uz39gAljmloYaRq08buzYY#wbvvG{sXaDJ=b-9*w2rA#M@naD zeTjvPts+V|gct+nIh63!J`AMsbyUj9anPQy+$KPY#5F+?2hgZ)i{kVL+D_FQFWD6O zU7LRV4NIyWkeh338U+bLYWX4uu+Lie{n7xKm#5qy!1;kMc%jYk1F+^8IwE&rhJj&y zb8Xt`hnZk1CxUoEDULsIyJnrS)cO?C!iMz2gX&nu8Qpk>^kso?WZtBLQ*ZM{VZeg~ zk!2ANqXfZdm#YI*xBbqMqDhLQQ_Rp9Bi>J(yyUywI(u80G?;I!6}dnIqZ_ ztkd}pRmt75oj8S+;cjt8FC^=%+R{_oy3pH1CUq09A)}QvH_84@BrM<%j04gR1<)@5 zmwb{4^6{%-sQC0vZPVORKqqPLg#o1kfTn=GXDcKBO8C&t;Sp|U1AE-C+@*#h%nG_= z=A0&+6I5^PV z-K~7!=DIjPU;0DuzO~m*F^Maiq6K2z zx&B`qXb>Z$7}y%QkMr|Q+FFL|zYR37#Qme)>BTz(@p!z%q|ek&p|A-GW!)(VN*^Nr z$(^BzD{)#Z=E`zR+C7hR{c~@O=maE$6Cwj>u3tY)PUh{ZkS5BU9^u?Yngs}kGX3X6QFnCnTe%3USB)H!cS{sDT^YL+=Ee-k`+Fa?v z+KQg5C@YH`pNE>Mz(OKe*A|(2@YJyx4Yt^0Wf+Kd+e5!XJo zNW(RJ$#3qyz?liWbxZIJx+o`6rd*Bnp-eWUjBwv7{oavGbV@i7k>G4t*W-M$4a16+ zGTO#pY87UX-9B-C`HNP>n-@gP->+n2oX|O5e>9)=2D<-Uk0VdMHgT(2HNOoQK~Lsz zoZ4)w#E-DeMS`%=cj{cxA+y>{{Fv)tdzpNBruwr*l{euk{OJn-mW;`~b^ z_%j5IOmcp@`tVYNj3;ba`;Fy1x-fR*@#DO_>wtD4b-D>jrk0x={t1v-bMOOqsUV3% zi1izNz8>4TAxtX;I&IdbOS^wR{VhXwbU}d(8*}yKus5tk15$Qpr>TONt*_UzSwivy z@UlOu6BaI3HCxpnd&*qfNNU~KElHM^%@O-ln7T!D{k)GmiB~KxCFS(jOXl&la#9E2 zGU1^9!%mHw(GX4u`gnWCcRS1$v=ks;T8K(y3fgAVunU09x&A<5UOZ$avZ5LO;AwBq z#+$VRb?o@cZU_nDobtZjW8xt%mb&w9beAfbd^|VS*Dhk_@QlO=pTI%J`H7ay5YEL; zW&r7}cWWpU+e8+=)pvCK4(qdrx=j^SpR;-papj5F zG;U`C9>>RwyMr6Y{moWu3>db|pwhhuPrp_&5bb}8avemc*t^H)>pd6iojSP1V41C5 zxahL0bYZcZ70;H(OjC(vbz}fIO86wr(lmz*%;b0cHE~}@Fcwaiz3P1Bt+aL z$}jOL81LyBl!dex8c%6Imu(R4Cn#Ku2C|P|yKT;xypXip($}vhSxZ0K3>GCPR0%O` zl!VzXub&088<$Sp6;y2yhVpz>#9X2yMQLRzv*gGA`lc1(!ac7Xxx@+&7K3d!Ux+;n zX&1FzB`b9k!0YrZc`xaDphyz($iZ&EW+&}mWWH61Btjol$kY`^R$%&KaMBpDHe;}UQOMwT{ay>fUPr<5c~Ovf)=BvbjMpYTJ`#TiRyY1P;{%HtRFi7z}xYg~;q6{|W9g-Lnh ztBWfGep71%#J_vHx{>;$jZc5{}-bkw-!1~x7rC}06gmYb>{zZ_10lkZBe85qLGl0 z25D&o2?@zfr+}n%w@7zLNp~pSNVkB1bW1l<5+Wedy@79XzW06ZeXi$^gR=KpGsYNe zt`WcCWslI4cszObwnCac*v7g<38(6YNYJ3e-=qNJW!u1~ln%hQ;#q57Eo1@Mz88tX~KsWy}t5k2LB6foiuJtpy_Ani+7mRX6zn8=^qf%}X z@z34K3bQhxvPQk|qxwyTPbrTuyy%-lFX{REmB%)P#8pk2_XA?h6LV?k-S=^{Y;!3t zl7R1$Z@)SUjSUNpahOC6g9wgimLV|Yl`}SafF{~a!m)!^bMq)G{m_9wy-u^NkY_I| z?^DCht9#Vr4$~+;Ea*2|pGUrC*^!?iW1mETegjU~@xVe53978uHlw6)9lp1G{6?U<~p&Jrh(@ z&gB@x04;iHiK2n#0kS~Y6dCwgPz1mg^p1u;{S&FIx&n{~djhQoD#8a?rv{A0SzYea z=g-U@DyM+C^51_Sj@ug=wt^i%(-9Eu{t;>b>b)J=DpUWba;<$84)~i}h)i#(0oLb2 z@QYuC&L!(H$o|19TLr&)_Se-7Zt#118KO;hyMBR&|YMKl62Jvd?pkE*Ae@HLa~Gigch-VP^P8>(`^sE^N&J=v_wk+`{5w#9NgAAc6Ga*eydv z6cg>%IQz%+c~cQ?meEvjkcOa1SAWiO>0r?<3y>UezjfKCTAA%|-VJzj_*!+NRe9vtPvRbE628vVnB;VgZw+U47Z0e2{h=g!3)72gz zNu0HBo*q#pQ@`4(bD)HO${X8L&W_|8%_`z|RR}^iRNpNxcHVz5`KE+}lM`B_s4eJLUo;7yQDd zU_>>*i=j#FJfIHbG)J6ero1pKN6;%iU>ewVr4yxGEzBkXj*QQM6thD!9Q(WZ{(fIy zNAsur+%kr~6(VB7c#v$s{ZSg8o;UIfx(-SN59E0|NZQC8nV+tjJCG^X5K;S7IP4{E z*q;A;0WEYnO$Sn&szO(XYWAlZ&ZRclqso~TrOsENsJ?{Ekww*NS4d4JXS1g#Sn}c= z57LR*piMjC+?drFz`3%I+D;pMJr|z$^JwQ>#5leEt-##utcT#(ZE~DtjAexvxNL3G4|0k7hTU9PMSgfe1C|l1j zq;7qcSIYa^ecy89r3Xa_EyAY>a%T>ExYf#XWRh*lWX_e4 zMU;m?K7ru(e@DVetLV9Lt3yjQTq4U$jKL>-65GM$Q~jRy2(})k!K`3hJ%5}hpGRYS z-OT>K-4B2pn3|%pveR=j%{jpnL|S28;w*`u zv!$_tK8Ac7{Xu>pa{JUa?f5UKpcOA_Jtmvn+}x}mc5{33sG0}7w81=1?GS(C0Z*#B zw&z!`#O&k3qUIa$Vr>a;eVw^?pAkE{`C-7J&>x8`Qo4izkhbL{ke+S3b>YJ-^yBdG z`yJUrMu$ZxdUik-7rGNgzwcMFgb}d#+*hCBXKeRig!wGZo_v&jlqpfM82bg%s|7?X}9vq-9!qhxj_o|HWt+jOgF@bcVJj`fJ_HKxxn#JFovijWr z*D?2{;LAx~ChE*1uB|D+K3RuECTi^6zH7wV3Iu%BFEQm9TAH4oj{3+e(i)uaMI*8| zJ9PO3>)Z`LfWys|Wm)w3tSWs#Fc+!bVN)inJ;jW*>E}vaLjcWoa?NpO#q(O@f3AVU zHJ1-ZMaBHthf)0B?2ygJ9CAlp?kB;?v~*<)=a)7d^{ptY;rGyedpK4L5-? zm>?YFa=A|ysxgxV==qeWr!fZm(EaLcR4d^zWJ$idrRgLjKH96J=rjvzCG;eJ3d&c6a3;LS z`FDM@yDShSati!Q)Q3{!3+vQMBqi2 zS>7@jSB)jPmL`c~&uwhjtC`_8Wo{=@C~;NmbM}lLn2UAtU+b4;Bw4vxJn2=BW-jo> zf9&C&Xy(pZ&s_KaU7zPMIwS{R_VBL$u4E9~MZqys%+Wh7ql~k&vckiu0an&?zf+NiL7ZFNG)3j!E3Ur*cl%5kmro4Z% zOZskFe;NZze$2bQ+TD$G54x1Zv&y4?=+{cZO#sp20TCgg!{?h(Q|A(ydJ7if zWze=x;WhRnPmi;wA}|$U6P+fToQ#aVYUSkaP6Uy9%#ngBE-xA32M;f=+{>*^1cIQ1 zsqPC6C~C@owgRrCq!clmBgH;y>_>$n_IIXCDVw#|Cb{#CxLa)kjW{8kk2yC$3e*tE9+UO-{;lkuSBQ zSKpqOxk|NB+X?Rf`bA^hUz0%!sZ70Bs{xrg*2T^OG3iKasoUQr(RJS0*%9c!GsFL) zFPLWb=1roN#H?phS=AgJi^jm7<{i-7XESizKY=E{qGk}HgjM>QJbQfab;p6N71kW17 zZumC@R<{sz51C>rzN;u}eh*R_i}LdyF#GQ=hjDy^{Fh>2Zw`KU1dtV;VEAIxvPjDs zM#qvpMT}0YwcF5C)mET-M~H|;QPKTn3KX^8MsGBsb3@eADZIn8^*|1phUgvWsU4p| zCBPFO7}QA|s&_gF*ntzfW)rq^;D_gCiD@LKPj+?g05R6O(Lj^8Ctu+Iouk;DsVO$- zQ&s#Q1t#4Sc6Kiin(zy$7U*3`Wx6n7An4H8fZ8oMa#@m(hYsf&&A~x=$tHS;7 zJo10^674uq@P{K7ENp&qGJ$8|MnDd&G)K`zzJXz!_4pO3uG_^gX`KUP58Y72J?(U4xCnVf$er4)lS@7|x_m-{dhuL+81PGRLMgebDIIuO4 zv8>D#=eH^e8H-NzWV}X)$aXjNLDgLr2}0CAXK!!vZT{zbM}9mD;OdLUS%s?%XY_Og9|~ zw-MMog38aSP5;+aJ&5?&rvIbkRQba8Z8BlH0EuHg1l+pyjt#yKe%{`DrA5KZz8;^> zuYlGD_K|zFmXydn{z=l>))p=JUMR8+RB_@uUER-mVAjD)FJw1f{L*=&gY6<-R9N_I zf9k|#b(LkoD(C)LnwOMgoHRB=sP3>1KFM0rjV`e6l{P1OK-m6g@}8BTU9JmnQ05fx z^iB)#;EHff?=Eix3=P6TT@F2%5qsLeR=G4@deQgiVhi$8p09qf0a?I<$!cSKmV!3H zUX|Jp)X|)LUoTPQwxiE`*E(nr0H+O5bat*Q#}Lqe|M{Dkcr#_NQ8mIv*)2EI_HSJm zfeT^lCpdktA9q+@{~h3_yvN^f(Pq_7a!&T@1jF46cK{n zzs3jdLHR3Fh5q5-8mlw##exTaC3xAam1|<3bbtLB&j@v&WpDGJK~r1XZ7^3ZLnrw< z7b3L+dN|1@hkJgD+HSoBFqxBpJCnMcal%v!=FWM-WD%hMo#XMEArvpqj-!;_2bwb| zq0^vz8@pPZob0mU`eVBA=nA+vEmZkI&27X_<}O>jLEx;K*!-*mlkFOeZYE=Mvxe@S zD2TluZMJ)f^I;~Q4Q(Lbc}?%V`vF)VgxIRPRO}`d6%{CngZVAHU`C-sK`Z|&QTtvF zAqX}Hk0Xy`ZHZs*KD)rCENIEIZqh8@#X&pPa_Ll5P*CWkyL#FfE^+0`x0sA;p4P$< zXT^ZC)ksJ>GdY>}1=dKsCwu4R;Lp(h>gwL>P*uNCTR{RM7d@>1mt>sUnGsDzA_aX3A1(lr3S{Va=%*8#?7xIp4(?R{=0xs+;$U@f1r!b~I{e~`^vLjY zD*>Wte$*&*yRQcfhC}ZgfFjAG=-+zkgjWOR0%PBf4To>AAO++Qqo^oc?R@s1{jiY$ zi*>~^N$dp#v0pu=hgWj~@(n5oqW4lg^pYQ~uHk1+Ix_zsSNJjlOo zmAL?jf};$+HU7~gczbhm&F&S)PG%w*QhC^jGeu+GhQ=X9a^Ng2U8%6CB5H~ldQCbj zH`8Ho+yhXdr~$K40w*fRXqQ=)!$6Cpvt|ET>jt>6d8Sg93(#N&9!0g~viHD_I6HYX z%-_An`k;PCPZ8SYIm7V0Z!G`tTS^L1uQhq7PmI>JvTEDjB!E?|OAXMTbeJEb5s05@ z%^`d$qG`kwT=6Tv?dE95qRv12M;{#K)<)qQ*M11JK_0o}3--Omh756ddOCR)F1~BR z$tr7=UK5?UEzyVlITGq%_14gBpgZiIZ}vQRWZ1I&p9IIrt6MPyuiWph5_SE#T?GKY zvH_N>-@m1&@r4`W?tlMgz%c4WiGA||Yh=oO=*y~xlHHC+!)UHuTtkF8Y@qdVNeb8) zCEKtTPpB>!H+Se}m`Ual2f&0e*ZW(5F7L^+dkQfdDa$`@K_KGGOW%Zkinl4!@4}4& zl(IOK=|q>xp9IQtFoG@2^e)*(pK*3hZYGqzdPE~2+)X>9^&$Qqa0N7l?|VTMX%lcp zVdR+|DC*d+0&BH)4YwBO=P%jzaQ0`i=@IRR_RkLw$$aY&Cb!*G+f*Mffqqi1*wU9r@1zK7= zJ?6HBM%@6Mwd6wFzk+4mrWhV?&Pg<8HHzahLqZ9wT%9IsojpkWm^E;eOUc%SIMMva z!#GwPG~;bOF!a9H^Jm+Ju~XFHud){nk>e)FZCw(bXllbj;qbw-84Fj#alYJikntdfr7VvWO%dd7?JG zA&ch39Y5=ugu5=Ar!R>uX*qtImV<;4m3;0@;nPs893gb#i9-`IWrk2~Bo zF!-#nWbVhWa<_G|0no)z?TN17UOH@5qn6qIZJYl(|MLYP0i`0@#n~s1U1sT;UD3ZP zg8n&+h1<1~b2Po%+~Ld%14gXhBsE9S&OBsGe}8~d@#w^zmHeDdO1DSr8ymObn?p;j zn9FoNQ!CrMyQO6Qx^gedRVsfRee-a2Ev#U-mA@-9^Y58>kD%VA{AVjnleis&4zMXE zp*wq9+sK8>ktfIP6&38(uDM`5KeA4DmF`zV_IGz}z^g@X+;I9XKxlaFpaxJ!@iK&d zEZw(r@xbJsMpIxtCVwjVwAH(`qQZXAT>F##RoPAhP=%k&=1cYenY{%s1GES8tLD(5 z5=F&-!7#2ABHfmKtQ;LEGF+w7TnMD_YCYLn^-f0mfb6dnR zKGA;14U5nsaM4EQIU8X=$=wj`qDk8NcVAuD~%LsZe`S(iPKu=#E zJQFh|pM#ZA@ai$w#)23glf|~?@`m>1>DZ8sTugb`d^t~2FSzZ_|BJ;beb3tAi>!vw zfqGjtIWh4|fgnQasuyI@72xgU*HvCU7fpUuCh?9CV*5$62bc{TOyoQHf(8w8He&J~ zC3Nb_4mK-+4@Z>`&&Tj!2_RKGqHoRU57xF(+Z*-JDSk`* z3i@KIZ`4IN)v(UKQX>b8?KNp!i=N1AE7Qe#KIR9CJ5J_!ApT6SEw zTgLYEZ4T{V%O779_aH>M<; zbF#Lyh6h0hw1ImdAIQQ{C@?XVG2)2|JZgGW2p5LcR#l(t>?#S7zWZYzjEBnJE-Cz- z012th@*`i_@e`Pd1)2fX1$~j!wpI|Q(sGUx>rnzW zBq$YN+}~mQk-n}4?dvzH?@dkb74P=&N_Qo`T{rs;9`N{nSTN|M7amzn;?_8qoYEBj z3QB}0nTV4Jcg^O9ArX*1QY4&W2%6|AYFe{$GzH2W|I8q!4VX^nb4X%w)+nPROlsRo z1%ZZV*H5!5M;^5}|Jc%T>HQe^uTt)N^&kH_=geqV8ichc{f>@76uX87ZC=m1(s`NU zECJ-DS0F~W#!Iut1mb)(g$uI)Fdrx5O`9$(*1dA711CpEl5-v~?%LlV;6v_#f5x<6B>XB@EP=z}_kp1{_>i1q-K$(!ex zIUOdTA2;L#GHoJy540GtUK3vnXd-_iM0o5>r31Ggr5EBC$HYfr_CMd<#qdghcyiL8 z)rX@!i$&XD6kla5IT2Y03b;mi^kn>UjKqG^b+)2<;C>)ozL+Wr?BJ(!MUR`~TAP{zlqWQUJJwhpfH+v^FZ?f@XUy8ZedCIZ zsQ-KGt(~O@bnVT`E)w(!Q#R;xZ zKvQg@(kB1Ag%Q*MGAv227f50tq1r-)0aeRK(GVGl%PCaExv&ov3jaMYjlF$LD-|tq zRR^F`9w)z}zii9FaLD5YX#?<2eG>$YgLa08hhMbkH3(&GtOH@4G^lh(7%+r5-`IEm zoS4YfGcf3zF1ZFLbSF>G&*S9VBL5WdgX7S3>6U^bin*q&Z1~MnM?b&Osrmpv<6m&N-rwC_wX6?!d_r@bO)J~9 zG(DpkxKQ~LD5c~D9vr(_BJ969EW(9KLA)F0+4wLMCK<~|EHU76z+7fTgF>& z+Fga_?#{wplw4h12|Et+jB>;hlMhP(WWZ3!e*{2*@Z^B0k=1;4cw+^r4Cl>AWNj6@ z)A*mSuY**PF0dU0e7>4=i0Dk-%;K-J$vrPZ>^4lAh<3_#jpJ8mujS;veUhO$(%dox z%|xro!hd@?m|Rp3z5JKbr?|VmZt{8en28(m@Eb$auiMJmIui3+U>Q97Jwkp2e%U!( z|BQnkF=s7l{-nfLGHI$&a>?s%jx-@S&Vga#oj{t1gm2N%6Y0cb)#c=xcHBBW$nr>m}~GzabeLs3wtSZ94-yvVL`S)n3}R z)SAZxg5SfGyQQA#DBtbdpFAs^8k4-=%i22y0G>u|mgEdU%^#pGW^Q3&Fa8Uv%{p~o z?y_zLCWCWkVGc&#k~xc@mk?(#8pyPZ)SSATe|C?shX+Oau`p(Ob0CIYEmVUdepT2& z#FGQ>%=cKj#>9e@OlKHX=7#H@BE99AY=~mKRg$if{a07b+ga_>Spzn&A7^6QsGvfm zVv1vBn{c?$KXb>osith+etyqwvE55WZY)ySN^(J-BR;q``EGa0V#@&#CUoWx+MI7A z0a#+nFaG}dmV2w;VgjIttS97ZVC? z9+IE6DBopXM&KVBl`GgRJfdO#R$N)X9jKqFhf;^|qjSL$!2dJ$azs9ESYQ8Z!i0Sf zyaK0GUl6w+(U2pgoUh~Awi96nGs6K z&HvHsj6KKxgO5&AD{~=_s&tngz0b+QTYSR$<2c<%W|j^Gn8hR@XCyR|r`C#*;I7|- zy#X0b?bXWudHl$^YR(JEFQ6gsy=WjVDwzz|F zna%b&NJ9!)A`+;B*)CgSUS0Y2MZvWQq*Mr=SW-tNS~Spl3Y`i+D3GvpIuw_V|2-mW7!a-hZys79IPB)vW$tL?3X~(Bto_ zQdt3aB<0K-$IeyxNT>=IX2P)73`2L6y1QW(%^-(K(FG>s9+!aph=BWDV>Un@AOddt zs3=S2)}BW+C4!Xwa!;=4g5z{V2k&h_>u^nUnmTnN{VUff)JNEi@hAV-|1De1Gv{e|C8}v$V@qgYNzbr<~!kRn5>#__=hB=ROfn ziuz#DWOn1)#63Cd5p{Hah8q*2 z>A_FLr+kLdpv)(&;+l}FZVmI%byH#O?m`Lw|S8~n)0tXdp; zS??@PL_XGa8Kc|X{r$@ZM#OGCfG@XYt@WDsbuE}BSPr76UXN)zya>OAmVqSu?K_n_80OOm$H5%B$;or3qkL548KC2{hb|E zm*2*}KvH3j;HvJTEht&njKI3bq?SKvP=JbT!@ zl6&fp7}|#sE)~z1V!5g;d)gmaxPA@vNkQE`6LYgg|61Lc_(w;2+*2eH@&IYWU`m6a zfLgdxfk#&t)P0v0eSC+d5QGnt9QidI-equl=u4@islNF=d5jLMJPg=pETJ2u4lO=3 zrI-!H!G|d1h3EF+MRN3nB-v|pAcn1mdfxG zm_^h}1)(uUkPg$zg{WRva7@u(@rNP+E{{;4;07c^nuY$`oatQt%@Onf9C-bh1eif^ z?_?J-{n4ZBWS0lnd?ofgNO6MIos*bcxk#K*Mo+xkxh zxeXm$psU<-2>22&&MFuXr1*(njZ28f?NZby1KURO%$d$Y(pvZeFrZ3uR;pBv9}fIP zXoul`7U~o)rOxuP))aulR=MHb?Q$#-`GJ><`hp;9n0Pym5jB_Pm=P2s9G&m~6zjhP zmHevq^b$d{AfzwpETVUAT(01v%xpn9&T_|SFao`Ia!PplCAHwoKhn*nb&W9%b^o$y zO>H1FgP=Ke(H`5=*+gfR67_4db=+cN!lag_CQcXGJUr>V4|GmOV_b4mnV7KcscY5~ zk-|j_)Br(m;F)tczP!{PaSgYy9Rl|+T#U z;ful9pFdf%Km&Hcr6%^z@%Nxw!4W1q8^TAAWAp)+%N>-X``nz{Kjt6-exc*R!SGE> z*09Kh%1T5|!B5nvRS$q*#DU-Q@mm%e3Nl0Xuhv>lVC?`s-+NP(4h9i$WrUJ9=9R0P2Qtyu-)`!!r zg?-izqa$4!_?&x#bB8huspx%o@3J|S!rce^o3Fyfhd@m{?Gw^&=1;IS;J!RyUCQVb z9JghIM>@^?Oq1poOh@Z9s_--m#f#xmxoyxqu`b-j$A`j=E&!4G{Cr<`um~Ow%Ei!W z=+FNxzcLhVe*g!>Y#c+hf+xKycNh~TwA#tQ0Ax_9md>=U0w;v?v}Y_@2WWIJ3I2kK zN+FJ`>g{Lr3+$BV&G~);!VbBn(4s$hLqqiv=s#(-O7c=vO?r*0Oyv0F_TZ)ts4jAn zzySJI=G}zQiMJSPzyX=>H^jA9`VP+^fVy(fuTnCai2Hg7@ByrGbi$P z95=VWP$YlTNTd?G=|iBI`$W3vE0BQK zs;TK0}Cp%6OVoU+#4u+!(g;8Dkt~}rWcjobpG^@j!T*pwAL%B2Jr7M?a!}3 z!D^Q>vJaCDyDlVyy4czW-D#l+u#urFtl(=3;HQX@Q>sA#sE=Uy_$@4Rslb2)nx>>l z8>934`0<*v4|&lYCFt&Lxdh^iO5z=ua0kY_F5agDhMY&gBR<;2q#??K z`;#n_8+2~y;;k|(nJI@nuiH7-jM(11`i^Xvc@}r;CPP z;9vxZG(Gmh5uRTr@4u*(u|cwBcOnPxML*?eorB6WH~?kWUSl4tv?T)YL$?L83e>X# zdKB<>HAg(`0;uzzzqvRN=0yax)5oRL8Zn!58O`n9{8aQdJ}MMmV)savVX{d7J$KRpMDNJ#dQ&X-zg8_T>Fg3oC2aYQb=sqI&{M z`3wIClnf;(S}_F||48-s0NA~+D=b;wFWS49+N0e!AJII}D8dFURFLE9=S7M(?_Z=Mc zi}UJXKya}gyvJZteM%S1L_@LxUcV!cWzcD^KxOq<6geo?77{L#TpFkn*psCkK^v6Y zr5nkLt}=0vdjk7bayL@4c&qySE}%c^s}doLZUpY+EijV#zNRen8!JN%R;M~8>g#23 z$E{P`wNztXH|G_9pAhqT104Oc;2+oT9ZEFyl;*LahI#H_&#_oRIRyG?s~=%NG89VX ztfCwKO>7HZ{+qT@i2NhHH$8u@vB7LP!)mr*ME8VGws?~lAB@is5Ye325OXRwx4{+w z+4&5TI*6nGQ{x$o4`~VaAmifU4Ki}^$-8O~505Xc{`Usct*L;J!t;hPWD-i-)92Vs z4J~1x{l%%g8$S;|52Dj61!BqBC#CDE3_HR3jG06|A^o0)Bd~VYS>2YCt!!`;M(ooM zc^Jd7m*aQN;5!QMgs#6_tmv`#;RGF#LFv3umcTmrcTb2kzoBtd#pB{%X0br!YMXcJ zD?ve)>K8~vodIcu=qgsm3V0CknINZO3{;~+QZe9#u)>t+8OxNQsW5<@oQB|XSvsAw zkLOjb?)W4c2T1}XMc6`k$C)Y4uww@M@oL_K+JK5;ZGZpvGT8u7?fz!W|JAX#{dgGx z+_jc(a-d7s>Og+6^4T{(Ocg0pjSRw|x4P>0*@)Dj^NYb(o1Xle^JGC4Z#=pxAJ{;- zj*A`0WaFJbV9^k|^2Et5oUxu#H^gueIbxE@?$H>Nw;B7*>He3dv`*=V^iRJoQxW!$ zTw|Awr~yxXF?_#A&rcvpuO95}ZOp%=h!GCZCxw|p0B}Xjg9Wnls>bs?NZo?7gdkLUjkYO_1W2(oOq~=_m^$#wis|JTFvfhSPD$iy;b#f zC8145snpF0qzQge1R#F|L7s6H6L5Cu(~DQoZh50}9KVoI@Py!l0EdjG6LLF^THj3Z zJYpu^|JvcGN4>EP^2@5dNJ3sA>VN4c5Cfuz*zXvbERj7z@qMiAHXq6Zn=Js7Ev>40 zUAaLZMw)Fl%Fhc`6Nq6M1SLr3Y4V9l#b{!AfiE5bIs)4*Tj8>Q>A6EBw5e8dT3A)( zAZ^ZBA$AVyf0)Muxj-JR01De__#ktvrRF71S-5bj=hwv8nJOgjO5kV|5YNgSq2Wt& zlPOIY@Zj^Z-cHe%I0kl!B*Wc^hCNnwDDM(f)R`C=tqpz#(K`-fwed!y~%dGvUPW zwwdbbD!3P@8I|+fIyuH`egI;LzuLsY5_<%}0up7xKEEF4ES1e7#18Fxv{!|Sc!NU=dtQckhMZrFRRnG zh5LFpx(>*-`~+`q-#!P4XVQcxZWx{zUHj799B~=!tA%jXrM!x79M2JM$=|oF{at{4Qq-r6 zsld?zKrwJ(DN9~8vtsv=zP`RFqGlj{%lFsXnwSgzL=P6Ect@S{Oa=^ZgDLSku;{ze zkDPx>^|im%)OgSCZoLgNO8#8&qps<8j1Z8Mki@~0R=aP!Y1>mx=PUeX{yh`M-*bjW zQ?iBA@;~hVT+TR?FHF#hF8B*P{EV;xORwjfAPEj6Y&tSFgo5`x3pOHkL5_F*3;n$Y zT>nBbeFtx_i4X#=BcS=^<>)^Igx7yhh9kl9$jvbPd305E*E;0fujjVtB&_Ytn;SwJ zUC3)vbK6e7wOG;V?UT2`mS-x!;F>cnLJkTQ6#a_57+TufIZiJwij{=k7wxR;bJFPp zKEzaMprtVEFd*>+bvh7m`K zSblc|6?@3hC!Is!B9k~zRcMC58yb>*DgeL?T3%sN#rA`#l ze=;50(%wveka(YnjWQec@tbWTp1b;i?F*zXYiOKi1p{!3{GJ=FcXWuU@vddZ-S3XE z3-PIqv@{P{NGt)j4LY^Z2zM`#DyDR8Hq~d0F1(SJDq;D0aW=Wi$ECJ>{+gm9N^yMm~2{}Y%doI%n@ zyO_(%xR7Q2C4OzRLNFzfh0E^`O^uC=mpcfB@rHntJ#4eRwX#BqV-$jme^nxf zpOJf_cUW?p4=z0c6n7Zy1{@i`r0#dLTJ0MH3n4!h|LBYXglnAQ1KCFOlpOvhC;%jegya<`~n)=CV2|i*}Z|D>R z{EJrO`z>{KPo9#Iv0O5>|1C!72#42uuvaZA5ZUl&(~6KJBmr~*7Q9R}g_MFqUC2N| zlI$1WOS%AlPR_U@-{jK7G?ejai|RMUZ}xM9WIXBGDU5$d+1S@ML`gx@w20K^A6l>a zrD*hecQ#!PkJo|bs}cGh2E(=dV|FJnTRTn6-9NmZG!ZfW$Ri|_G0WPc7Au7XOmH%| zt=ZG(MuS5Z`72u&VjmcQ$J5wyCGE#OCpg`vt(XIpLXmwHpka0aBRa{-VZwLpOpywp zdIO|?`dgoDSVUt(5BFO%`_Uuu?L!zyWV1N?-37$qA=mMTRHuH+F(bHWJ9ywkq}8ms z57mBg8wHdAhSCvdoJCPgqgJAod%KNy3fq`Tm$9?9Hy;O}9Q|Sp7yDERD<}@c1~I-w zrdT<`K9E8rU8rQP4)u2f;K|VpU=I7AP2K?@kA=It)=QGP1@JF}3J0Eo9KmEf@oNAc zM7iSMO$V~F1gG*Q8kR8O`Rh-y z-;SJ^n8;uo&NJM`5Zxq3rm@Vwbq2I{n+XU ziwJqquf>W%Mk@)9sX)X&oi78@rX-nx-thO=yPkfas2^lm3H3XkX|DE+qnFoN1mlH+ zEB~p`2V<5KGzj$i2u$u{hyb026(J^MDC2XkvryO-4G8km1B#p^<~5_L44(fOqWU(+ zbSGt579NTU&}Te^g7WfS1ls&)jAKj|hbgvn_g4j3(Nt&8+mSY$hp1Uzz3bfI!!%m7 zU&K0drGU^#>Nhgk*0P+UD1l7e~{^~|!+_+Yiq@tk*+QhxTbr+hIT1O^w z#2sl-c7PrLvS0kn%p@Oew6q@|{{s9RYnW^lZXPbo$j#cX7#9XYKcr=z%~1v`n_dM? zpsKqL!k-aoeA}wMUE9_HGPsM;Y1A6NmYo#_>F~8vk&z|X-aZFQ4f5FghUP1N6@u9G zDSDAt>S`CfVKNn@K!PmO^r~8#at~UWwhYY2+cAZ!YA|lwBv{n$>(#Bf<^)0xrJ8dY#vLfc-Kpyy?VQq>OEVi zCw$t3*!=<#njl=R?)%-{4y~BjJl&}*PsNo@D!6bx}+Wa`L;M3 zNg<2cv?`X(ak~j2IGrh{`Mj%FdB#!@>L%`HbNeh@MKW#%sT40WMaOmYI#EJnuIuD^_&fSYYZQz24u6D zZ1-M0y3b|ECNpb04KZw|@X!c+!-Sx4%yDV;RoNuIEz z^!PM`{6I&3Kx@ea*-cMV8ZmjZ(UuG!eAD0(Fk zzKr@{+d|SBN>X(NMzmArf`fqV`<+Lu?aVUuBQ&EZ5G~c8{u<`MKQ?2bq+u z&P*;1Hzez2|8`*>E#7~sI9eekhFI0Im3}89tU5Zs_?QDsAslQ|?Wi_>MuD8!zQfHL zRUz6<2Zk&cQb5U6KZdLF006cTAkN+=%vfH(XJ7{3kkfQw?|8L(zBvX-)$|=&#&DsR%s!$*VJ@AFt|j3$LaE2V5QDtQj<_XFW5-|=raIH zO>|u?#KVK*#!A&0CxCi@E?8DZ-x9rQq&1{V%!w~1f7+UZ66X-`u;LNs;IM!9F5>iu zS)oN_R4w<y+`Mx7OqTDwe z{XNzJ_DjNX{?5*Catu7ey`G9D<;0cYlArPWt5iHbZMd#~!kJMAUBCyC)X+ozHU}u6Ub@b6z?p5NwQK&Zkr$cMm82_X_%lSsb z%Q}3F7@)X?^={HodDAeD@-o?3%?(r&EmqL7^wQZB(XL z`hobfpi~3zoAQyMkF+s>6-~x__tIl_Wl&960@W#W1aNnSOT@fa}+UPZT@dDtXuFnEYYCxk_daT_ET7V0>8^&Y+ zac21>vo+P0k+CPohDz^ueg3(`6}G-*LlKuHjO^u5AH<{~8?S>TXL2joXQ&Y_AB#=u zGL^COJeD}hFd4KR_L(2NuO2>lgAwQeMvT&+HOS2gLFTd4QU?LNzRYO!o%!f14G;kP32(9J@sBXgpO`~FJrQXs8;9a!(yVN3Dq~4ZMgL* zZT-nN5r&Mnof{S{y-3AjZ$t)A1+6Pb`(Fo+#+pM4(l_QfXZ~j3>Pi>9T!F8gC9TL> z(G?JSR1^kke;{eApGkel(s&jm>*8r@iX=E{UE6sv1#T~3b6NSff!JJU^ig0ACsGHU zPjP`#(z#rZxTMBcZMIRwfd{@EIt{tO!0U^k-N0hRJ2yD&!_w;(!FObD?&0^0|bqXxOUH$U#G%=u^?vFT?9_zr(!34dhxCoCC&abak z8v!8%&KECeZb9ebjNfAsvqmaHH1}YiO4$z|3-zf9N{KnylnJ$l7cfC2z$5Wmy$eZ# zb1V<2WYms z7LZ3E${E5^)wC!_$?e(8f9e<0J`S74H;fRyna<<-SR_hOp-if}S~g3pSx$~EL#R4u zuE^MWEY$+SMgCxVj27yd79ougSL>uaBggvYi&JPK)bmB=Ca>3vf5wcYZg1v`z_!3> zry@G(iP2x7_-x!R&~;&(Jwu^%kgCw~2$5#9Y}C1rBC8iznX;2cMl+qeUEaprCn&j< zEuWqs=Ry}H+w}wJfpMXYt{l&jxL+2yUSb+v&;!X_@9RVL2~&H_8>?y?6&+pp^5sjG z1tpJ6k-6a|<5?Muen&YDo80e*&cQQ_Hn0ZR-^Yvl&Vr&&&N3gz0A+?1 zL^y^(d?reB>aiWM0UJ?W+WSnsFoAJQ<3N zgQTIB(oL1WEB*P4vUr&{cNIz-A#fMNnzcfBlBJ3$HyGg)k<4&>aCY;`09n!o9-EUl zgGugWRnm7_=LEp~R5eppyk>y+dW3A$a$BxVqOZNU=<$GT_p^{VV`{le{vN*My1n%v z8MyZ{Jh0n!ugYc@)UovWP5=wjofI5qi?>#P|DJVq_$vtOVONv&0^U((K4{X!;Q~srz4Zp4 zP=HU3_74@)e|ZX5pu#GZi;XC<*Iz?w{4%HCRBUWx$7Aq41)oB<>H_@i^ z2%&+e1m*&$HvdvyS;?R}>hgq$o{@neX`BQK>;ln)#Cp*g^ie?Gv-b5}b}G#SKbZsX z-%@1n_^*KBPV>%RGcz+x8rt{sJQ#<7AD{>h5fLH;u~|A`>GJi&`fWLu27p896hIzO za1L~cH~r_g{Atj7kpqGS;ln`}zY)xqSN^}Q&H}8eZTs76)7{+-0!kww2vP#lEz%t# z0-|(@0Z0lWh;*lv0@6yVfKm!7NQcs0`y1TTLd!ijRr;6 zC0%Jx#p{8Ko(P%_x&`xwR$j6)K1$apmr?W*U15c26tAEhV%?);E#!t7b zP-KkImaY=>@`3wVW##4P{8d6FROurIdkV*_gd)3(e2(O&XGX`yOxC|9NbG@~q{df< z>D3H;mm0vSwNx(4dy@O`TR7>`syC$gs;?HvbFFA(3_5wXMCJt-AW60a3kL8#ti{*3n5c;8ag*u0}Zc2p*jIduvQ&X_v`=B(#4R)E?2#sJxwf;5Y^6nmy|6|er z5Abx_e4CU>zNmTnDqCT}F#DnXO+%dvu`_21eTnbHZaq-5;)xJskekW>V5NQu3WxG% zYw};MRe5|?Wwhi&3u9hy@j8y@V&UjVqiaUw!Y#{mNJ~yI(Y`#Xp4*B#h(R0ad?^U?H5}DI@v3Q{h zLX~hNY{i~8G8@wdQn4?L162OMTb@X^s=KJfEKTUCJ!gWJv=5kROqwi|RM+JYS@LN# z5!m0D!Hw%a=SPT$p6NxzWm00F?=SW5KNBL`Oq?5|Cw1{nNi)HTPfJh#_yFYHhmIR2 zE|`#oZ{6uodY+ zUw5|-KHkq}8Jr#-rZTvxiNA&9M8xaJ6h}OB)^mMpS9Yu0`0E_{U<>3G5=jr(EdgsR zSZIFtza+ZH)l-mRnJph?AtWb&3l_P*Wn!eMrRB+fS$_28@nwp2C#12N=PCX2p{AV9 z&;vY@TkM`pZ_EkrC&qXJ7K)uB27k5WFEN!$hoZYWzeX2@>=zskZNu#vlCCg!dxj9V z78oly%MdHR#=JbeI0BgcINgUbN+WcTCpt(^z3mO&|L+KG9v5)6pKB&Oexz-i}As#=>MADL{bQjPoi7nw9Q9tx&cPG2m*Hd^aM0fnwXEjtE(%M zgU~PT5>hHk(g}>dbIFB;@CbXSc{=2o$rF)>$6oBujdrn$tc~OPB$H&rl)7f8reF7# zif6O6@6_I0PJRg;_vnsJXPY%EC}&*3yUza8Hs6q+`(|G|q?P8K#tS8D$TF%AxN5KG z>$_x-ZE6*^wkB)j`;9ILe>m7HCz=0qanyhaH9(j_mSf>za&y+LEdRIOr{l=+!9omn zE;py2#k&}1`Wdf2zf%~S>pMQju()IEfbJc224~@4uZL(7g9A{}2y8MgW9h(%?;H>q zP#vyGrQOgkzT=oZFrbmIpI-O%>2CD!hbD*SPO_<{$7zen9yYuJ#mBCxxioup*Lk;~ zVqUyM#A@`*OW{AIs(nQ;DgKiDz{GsVDSmm6_IPW7G|mg#k)@4SQB5}VILj0H3=_&M z5{Nph(?exFWn=7TUo>^ND#v#J{ zd>CwKnEOmY_<%zH8>fsYZOO4CN}=P|R&7YhciyF;BYdXrUQ}(af_G*O8BEO@X|bGz zYKYp|F^2{R;~mfUwf+5CFC$=)1*HtcSeNzv9n>6Cn$ynrdD!$Zy-@X$bDF->FV(II zYe*85@*jBklPcV}qk*%YN>;tSJR*><`e%>WL0^~fOic!^=gByQdib+=@w6J~*LKhE zt%e~4*=9vju{DVm5Be;jEMsVVejn>P&=i&%&DE9saJdk`KN zG745ouSf>Bq)Kzc!ko;tg#+FsX6jHc%K?YjnJ>Z0sxU%w{Gp%iyVR_PFe;W$Cd9iv z6T8@QwF?bF&1Lt=s9jh^H#wHjnW4X4#Ov2ig!npc zJ;2qSm5RM56-UBo3|i+Khb)0X z$vab^Pvi{oHUA`BwyyjSeg(eNsS0DO&c%irLmgX_f@Q6fi}!}M--OWzL*u^ZcQaO2 zT7Q_mMiuiU%HZWX$ZibD2te{g|6z0gm*)GnUvMKK{4e-jCEzS9!SBzlh<9BiexpXU zvRmMx_$ROk|ofp(}w)x~D z-ec_ED!s>b!_2G}x9vMbc`)XSsp;s-XzK5T;{|Hy0B891`1&+hG<6KJO zOiX9ddAj`a?9Uv8B~`+>3d2-pDRmM15Lstx%h7)M{7L`4E&gd)<3DS`)2pi&++Wdv zS%aX)>(LtUZgoHiPJDaCMg{2qFYsHJ*O-v2@ZVQk^fi=Ww7B@^M~`sZS90tJz9R7f zCx>-KdsjdYz&9Y^=p0r<7rpL1oKWFtTF;HQ7(f6@B2)DANUgm)-4~=&;?D+ie^W^= zGCz#NNID^{QP~MS#1uN$)usBudvSNy7|8;ijq#{^$2XJx`!&8UZ%=tLREB@p5X$OO zjfHP5ou0OokWWFX&m_2=&JScxwuVfwqf6QEqnQ{vpz~whMkAigf74r}t)WHiL(r$? zpdzzSLeHwpozgM}iV)O99AkN7l~y{JnuR17-b2Rjmdsi$VEqCu!6*NonchXWmzw40 zucsY{%?ZSnK`V08NW;v(4az9ht^9yK$p|Ed$n(nRuas-JS`q z(l1bg7sbW%Fyqw~q~)%J1NMlL=F7d=bo~pB5)lTV*&H^^m=JBn=zE`I@#IZyZBN~; zcW_9ss0+l=%7opXu<8MysRp&s4VJd17=PTo=dcT&+mhcR|SV;@?_XaK+ed*EajHt)2Q^NKq@zP_w&B{Ou50Ux7^)>^g4U!AqnFDUh5sv zNG=T&hkKp=c>{xZ#VBjj_@sjVK&KbABu$HgnpsoDxHt30>r1Ha2Z8t%NaCzZW^xW1 zG8)aDRr%qHpu%V00%E6^i7f)JcF=tTGKE;$19$^OZgI)mySca|5EIa+S$GZFSL!j2 zuUDiXn&1I0EKNWogCGzDmR*R_yzxh#RrUroQU#sJTUvMVPj{un?>H>ig%kM&`wEz$ zJ{kO=Aa0kT=8g!2(D92mWWU;AMF$6pdr}KO&%{eQYA!#LW9Tih8JoErkI9hv+UJtb zy;N$g^`^#&ur`alR zCs9xiNk;Y4ETN-SvB_Wz%z$_2s$Ztg}%(PI*yq=!jwSRG{LA z6AhELW4=_GBUzwRs=iI1iBa}ecd)iwF!WB$wy}Bli=Q`{XHjKjiaEEP(i1e7?bfw- zD9a_paxDXVqEa@IO;BZ^=l_sCVmrgs{eb@@Zb%x06ARVsA5~h zDJn?TmRTx$4FT!0ue-+=pAw^0_V{3&n2jpyonP@QU)do)3Hs!g^ZdzEoh#*qcm516 zC+i@}cZvR)d41`pj`2($eTu(}D{2oza>&uDzNLl7#};9`+rs*Kua4sdbTt`3cz@dNMh6zRH8hH7v%YjTrF7I28)& z>LkiXNtdMqyuIyAJgCOU$8FO@YCB*l*LAJ}-uoH|bPz6K;{YZv3^hHJky{wU>k=g1;E3w0nD$}?vKYp*a@uyzy z6w`=BlhThyBJ19g;E^Xm*b15+-zNr(XCPKo-Md9&BUG1?t?4+EOy*S}O0iT>F7gH6 z^5h38F!E^^OX;lOilQt#2UPki; zgz~O`wSEQIB(L!`qnw%iwEDMil|Y4Ob9FWL&9V!zM-FNeraB`7uD0e4)rQ7KjgE|z zW`^F_SdvaB(j(A1`E*Z)Wc#r=6#dq`abtqQ)ou7G#6Pmm5fM{*EO5n`c7O;GD7ch$ zN<-Y>;N<9Dbwk5-2;Lb%XF-a92TBETmNT3|a&Y#@`EkgEb_|m7kY;HyLc2=e$tbj9 zX4#{RjUGpoQR?n}XVpQ)$@PjXrbt~RzNMjoyr@6D&N+~*=urr$idf{Rva}O6-}CWF z#$}^a6!rMoln{1kkqZ%12c#JMD@1)<2kD)sC)bWAM#|Gfx=WALB}S`hhHR-9FMYi( zR+@-*y?TGKnR#W)EOnp`)2HC%sOo$^=^aFJRD_)QFa`8nA zCN%k5J=#Zi$%h4~?APv2JJ_Cp()WY_^JjnRj-C+NN+Z2i;LA9b*^GDf|&mJe4I zBJM8#h=0k3DB~$Sw#&(w6OhRlxbdx^m<4KC<0jLfYQcFANa={+DitxVGi3xq5H9|* zvs0(mou9?7=o5vHb1pw-Lhc{j3#l$P>L5+FI)UY1)ZS3D+aHs|{gOzEhlQ*Z|1-D=@`*{ujR(P{;@Q`861Jl4T)12pL>wuCMq! z|4CAgom0Q+&%FVorZGQ8Z;-<1mpmY4>@pG3KWAxSu_3`?5;ZVjhaK|I1E-A$v5~QM zs#V69z!3yW^Mw0uWkp4fDwbZdQb0jXjR7Fey5bS3lOd#a3zv85lTfo4E?l5^rGpm` z3+k7UnM|qgrL=W$42HS)`nBZeJk}>U8Wa-#K`aWppc6EIvHb*)<%C$1Yv-I1E|5?N z;zSpV@b`N%>8KMX6tt5Ow^;SQMDHDGtBVWaGe2!;#!4Dmkm5oM^jKP1(L%{5YDG03 zHq6irlO>EM^&p4dB10`l25h#XQ!c>URl_-wfJWz(nJu!}$Z{^;(E}^&{U%b?sbe0iO(#1%fV;X*K@U-#*Kytvr4BRS?pF0Au)2b=giL zCok=kJtAIj-L|(T@A%TJ<_}g09Q-(4 zUOVg5u-1oC0{%k!k&XnelOxbAg+k~JKPI%nN;CJrHu|Pmp`B~Rq=}ij+7@k7q?g|* zd4PU^0BTq%)qmouLNhyG&H%0T4?i4o)zFH0Gi>r=#&u8 z@^}VZmK*Rzb3_%vc39g^n;mMcukXQ7TijOKhD*H?srtu*LZ(Xr%=)bG`|sc8qP{e7 zUK+92U)CySq6DRPy2^9LTFjo@?#)goWa29%j&wVW8rl%ez;Ami4T1(4AatNF-j(|u zkZ_Dq{T^rbyPzw8gi^FbJG%T7>@gQsi%TSc$G&zeg9>8kmSP{n(UAv+O2__Q|JN4O zq1k|<_cYC)sG|?o{SZGDJ;2Q$aWIp8zbcuf!N7HK=2owxo~4+yfZJRl=(&C?iqGU8lFyiF&FF{#&x5hzq z)QTWw8H1Z1VS~f;mqr$`z3cj==O6RR-^TBy&@*^%qO3NG zt2o-3A&?hHxwMZ$d{NaHv^dhF28fY&OjPtZf+>5Wa+L2x1?1RHz0!S7i9j}g0oa23Mk;m3Yg^Npj<3QT**7Ej*JUeB zh4FX>1_mw?&0oJVfsV>)Ys1SLJlKD*aFCwE_g-5j%ZvflWX6cG&ab1Fc=^{ach1@q zC#1~q+_AvWHH=rFc*2c4aw{UevE;RDc&|p|v{8SG1_q+j^0n>% z+T>bWBVtIzQfT?cb`(QGASiA$>g*%>T53q*HU2!)3PR-;?R? z{??Sg%ALoj@m?{LFe--&idq=8iiuIpK}7?f4DRRg=yAybfhf`Pd#ouEXVfXjbKel5 znc~8I(GDBPE7||{?b}5}4EEmDvFur))t_xs3tmN;uG8$+7QI4MT_(iT2_zLM9V)o^BaOD;Asar=nSt zx1gmF34#n5K73OW*pE}{wjyaNjmMf2q++j8D7IirRn*pL+y**#te;v4-V122?=j`L z{N zd3JZ8%WwuVC-Ao(*(N=DF(Xy#ZY841-7Jb2a=7^i1k3@YRTANg4YJ7_D&38P^5jS- z9RG9CfL}(+SxY3R|DqvAESD-PWpI$mA>^~6lCZ{6-jFIm=vUytRe05)t)d?IrI#Dy zc4JT=6oB$#BkDDNfK2;B+4bC$BH)qf5+4m`=@xy_rhn1t0(nGmozK73G{1)aBL^ms}kT+Y`_gOu?_=^IixYykcbG76HFe0>)WhXyQdek8|&Q zp%@H*8vMkYyiL=@gm!FXgxbL3$MDu0>l(iKrs&IBl!{TcK_hF7+!3=n+Se0^w{MP!gSZB_om?(Tlc=!Ci;cUqKy0>=!#X+S{BHUKJNoY~RbELK#!K^K=Gf zDk~*v=67BTo-U@Gq-|WtgPe`h%GnyC0alz=SH`9b64EwY69z2KJm2Sj!Edlc~RMeT+cS zzXqpckzxg5(r|yy?*rBio3e8e@h)PD3StFK^CYeQ|@p@3S?DM}Dab`_Mv+;%3<8p+u(~rH(J#m;8%e_&Wud7x{(oTOUUarw|b# z8?dxJOQ8q-hFkW)Bt!6~^9LRBJwB0Uf{%a=s8{{r<+Y3qmIq|jH#aAA`2B9ucYV}H z=#L#~E~{!?mr(uI2|90Q70|gO1P|?B0Y4cHH}eg~jY8CeU0}gEi(`nK{c`0n-p%Y{ z$Lo~~iG~t|Zk*{&z<=B_7aa;`QL$P@V2L9%Ma+;ryKsk3Fn?66LoTfut506R;>*o80IywUsWdW}9+B!Pi zNkdD@f_d$aCpOaal><8ztpu4^S)+lSPA>hfgS@i-6%0_gT@!%3E;Yh7i{5^3HBs9_ zq1*_p%`&wPk3IC$Fg6jkJgRmNrbSTP8kem0ZHl*ZW3?k+zxOSN`}-{cpBJ`bG$EbB^?^Cel?S=211 zhS4*GOj=g9`)0 zX;@cBJt{kljYQwpo8hvaMe<<6av<$eR(1MKct@UWOJTIhQqOCA;onbva-{AFayu|H zB^`s0=%nb%N5?3})(Rn-@&3{~4CEB5ed&6|DDoibG2*2(-iiYvn{Kd@nkGJlJeBE?;r} z&}{BV035cSLPPZQi>$vE7kj)pPT(isiB9|d2_dQHyx5-{7TzTAlPhIR15*5&6IJwC za_y>>St=u*9|7A}u@1vg*0+bAo$g0NMg1Pe1|$z>qbLaTYTHer5+ zpQYgK7;-HyVm2of!?N#xp3lOV@Q^l5C_j_I0Cj|qQ@0ssxs5LI1?-dow^vC>p<3mP z^;ox>)~M;>49W62F8mRdM8vDabp?-G|A>R@F1gF1PQ7=HiR)@?wX#V;NH6-5#jZju%EPt_U6 zZr6LdezK5+!N;|9_YUbBxMb-hrQ=+byk&KK7ss^C1UG=(8>c5nc}f(j(qMVn=2EGZ ziLUR3zf4U{wehV93FeJHe{`h}FYRbD@~Z9b)KnJmfVB4gzwkoC`zHi769rV=b@| zr`so5o}*ScWJT6FREVwSUWy^l4v&ts7EAz9!%`TZoE#YRYtdDq#fmxucOkUsUWl2a zGUrq)6Te#%>SeQ(ZgH~3q>p?vFU^O}p-&p>`l`{bY@jMlzv4NvC2hEOlUH&w();kn zrRs@JK+PE1T9M0pJ3EmGY=tK8%)dm~plrOZZqV3bh9~09D1_3((p4Tbb27+9Z7HF5Lc zd6xCATY5^kwuFC-k@bey#|m zsuRbTbm!5vX(=bFm*8iFNS}@gCwgQN$}mHt4vBNLsj)7&)*4HlBVsbyQ3-^exrTR6 zKX)ACC98_i(Xn(mLo6+`|GNdiJ~?jP$zx534X%_R*->#YS4G(aK1q3Y^LqUUmCxx^ zYXHWwha*6^ueBONeg)`HP!^mliFiVfj`5b=w)DSdAtACDz5teTti_iWx&iEGg{_0h z4g@jS0vA_SDB-x?d;yvL?IA)#<}p#!ykJS6Cnq!SxC#TcsFTekk^QI#B4w3V5A_s% zf6iTU08Y&o_KZ=_?C*vhzynQlE`veHT`004FEz#ffxWDh)PAfe{yWBa?$nl+7VgSu z#EpY$6WrQWU;uNF?bQImUz(!5tupNwYRF|wGFSs8N3GVHF^#^%12^L!z>;R?RhwZ} z_Ta>5GqAYYo$_;BawXwjOX`*W(xXCW*3zTvAX+OaCABD3;%IHHFbs7B=R3 zamemYbwVCd0f_B-kURWTYoAF{bk#kPX$I7=&`uSexP1>!e%^xi&-^(t@zg!9OQVfL zT`T5=zC`sivd!M986CLJYi4$i%?!xm+hvBZBJamWKRgfWT9>-F{RI|TO)m_2J|bj9|KZk{@!K{b6~q3%xB;MT&yc{)ub55Y4$+%O zG!NQ5(|>Q$sNZjDQ;-h1-T&~SDAOqBIzvqJRhQ}-j9VsK3 zG4n_84Q}*dcuEF(hw9HPE}w{1a{A=>__V~s^YjY004wthSYs0r7G_)h^QYZLj?y48 zP4%Z5kKM}Wb)Sug*|jiWVV`_QTs9;aXN>aK=ueyT=> zb%_OBPL8rUL5wP?+th(%*HFTiF`C$+fCE;Jmn&o*F*|fN zBGv6V*->^IkA2Ahk0Sn_)J`L{^#ETUB)_;T4+7@dzY>4(L3?}q^WMO|8PDwp;iB2q z_6~q~l6ZkD*#*}!U0SingjweZuiFzX!zY^xT%R`MQqiA3?$hn{-RtYOH6<#yEaP54 z1kNILB*Y1Ud~6Q!Pmhf&`y%4c7?|RpKV5*c{FSA4S3(X%0O*&TD@Pq#*Ot88f1!X9 zk9Ot=WXXR9vqj9H^rwK4v(|=5sXct8)ot}D3lo#3$HTf5eR^fagwcl|vHw5`j2E}98T1~4iCu?B=hAvN%c z+h$3j>eZ`raLq2>-ePu%rRJo;lV}xHq@J7P49@hgPp?xFg@wQZKwHX5&BOeDCA5Iv za3%G&S0b|s3FIzrZl#+Cg6v$&Gxe5|TX%gvpJEgD3SmNtK8C69EB9Oi0&WO#KV!MV zQi-{?SZ~QdpQf!8(b=>q+2u3Zc3-p+pinvn1`40xr-SOv?=ek*$~wWoVl7xDf9doopKSdA$otd}78sN!0&AB}5)Kf;?zXwd{F3MT-Q0LGMrZ_s1&k zp)i`Fu}mYvXhX`EDX`KN_L zY2A@{z}TK4)^>JE!fNs$c?yKobEpje6xNI>U=U^VzxR$#g+>u%Pq~IDjo%iqL=+=S zig?`H*J=)PlUv9uz=ek2@*8^dt}{>NbZGVVVYCf{!Pt@ zxQ9=v<5t$*m*$yMEFSXb#164S+4M@O#}QQyP5%8jNE!!!oandX$Hx~%9Z89a)xr8- z#~}Zq46CAsuEy-S5HfYExU?27PTDV0Nf#DLWhg@nroaLcY5~v+-~hQ=-gi8IQc(lk z^xrQe8V6BV`5tV{{OsfAkxA2nW*#Ph#VnuU(03@Zls!ER? zmqUU^URqMJ;47O$7fKlnmC)v89?TDbfqhDUE$@7#J$!jmk$I7coh?xS!@8>Pm;!E{ z64rVgr<|LVNCbcD4LB&jp&DFLIUcA*e)K+~XGC&*nod>aZl#2Dh@y|VD)HGp8M~&! z?wNi;ll2>A4lC#Gt#?EkKaLY)7Jv`PKYpOs6h2}4YSY( zm=aW;&C&Hee4!lko(B8ME2AEeuQ9w4Ayame@MVeAa*G-D5z%eW>1?R0htR5#zM7 zLE2ncAiEASXo^*)%H>1|JA0#8I9pGebj@)%5i)r`P^)6=-tl|Sbm{^0Rh9*?h-rvD zi5LIz66!=03@%hCJUV_uDT0f@20f8~df*v3v<_#%1!d;>pz)p_wQ)9vfufiNce;4u z7^8pP3=v-68TX6p2fC_n9vudS$jaODhKEKnD&l#BIe+d1ovT|xLCp%J@4!EuEEO9a zw7-7EZ+?bNi4aRs2qW~D$E8mk&+iD@MQn6DO>K=;WnoEc2@pWtU?{y zRD?mVjWK?l-YkWuLK35=nu!wY3c=g6Zs&C0IRZe5dDVxCA*{V8EbI8fkmevR|~QyHUIeBwqgAP_{M(*9Dr_g`6J#2)~GkV^ zjEm$nsH{tcIe1q{_K+jaF#g%6n9STLQyjLC{q=0!f#O@@leE&YN18F*kqhFhXY>Uu zo%?llJg#h2ef0eP5tt&m74>VgArH(SUiLiSS6!Cmt`u(g4i6zW`f}GV%(klNs^+E5 z<1sOe!P*di)le#TH((WJK=KKgzcbRIz@Z-0K)f|QMQGn?x>)y|Sy(b>Kr$BYK z%sFmI*Utk4qYif^jOArH`d101J{mc z2W&&+=4@-~M+p9g@4khfW^%`Vfq_lnz&mDI=zzp|ENcb@3WbN3>d4S`jsIwnV)8Ck zd3eY&JZduI%oWeOW${@^3dj8LHiSLxy#QBjWv?U9U_e%aC8i zXFTSrw0{14(eE+AGUjuL2qI>NNyyIi%X;-%L_8fQ`k$CCo$d-=D0n9iAd7#9kM`{o zH`y&n?KY^smeVnI^n$5?7Jv}>mAnt@M{Bp_h-+Z9X6o)TBwY^NXL~o%NJPFYA>)=i znDgWEfDs2h~tD1cIkbSZd8P zufSe!%*eiCQT$4!)GOnQ;6e~WDO8z0aa~OfeU}98)ER<*Ia%p+?JyB;3VUbS3|Suj zz-Ia1`DaaF$)+t-R#}Xno z6E~haLbkV8UPRFNng0=12et|NHs-?C9`W~mohF9(vL9)qcG8Q$wPB0_3_eRyJY zjxH0*)Z93QuNyYBY7}nC_@ELcpRi1W5k9@iBleHK2^fXvsL-%>g3~qhsm>R6>m-QM zbH=R-v(YOz<0w9+(q%?^xjeQNL7T$S)|SKK15V-3HbNtPZ))TTdVhm6s=C{at4)UB zmWWb!0M*Xb(*a>EoVQ#xYMiox5*Wj!s8E`4;}v;qg~WnM!s!DF3BI(1=`)695r7_r zqgDkpbN=CZM|NB5jMH7hlcDr0?fV`O8opMthMF_HLO6ae?fLUpuG?fVgV7xf{Jmmn z(-pftu+tfQdQ9)=7zCP=`W2YcHtd!1-35A!UBOHa;ZH~t7DBAKhQUIhI^~{)pO^WQ zQF}67ZLLIW?`L7`@zNQ>YTmh{(nHBUqj4wqfB4OI%f+mNlR>agHPrt6@*I@<<-`cm zm%Rwu_PMT4sbworMmjxO+S>htdyl{DjVQT#d3i;CGW%!BduTRIo#BC{5JnbGW{E9v zj@(xId^6z-MLf>-7ns5%Y)W_uAW346(7UepVPEH?ZzEz?-dJj;Ld24%>wz5+Ww&pz zoIj6=x!g+Eh{xt4+VA)Bc-3;1m4IZm!Ei3)8cRXsHH^n2;m3soDad;~c4?yk?vYt} zmAq61cw<*2NiE%uawf0ydHq|6ys-f==-gEyta}llxv3 zvv^H$B)YoVk(`+OBw4xwo9be3ruk^^QkfUj6*o=LCN-NIna>jV2oxifW%mXPv@T&es;yk1XdV9l01O=*!vFvP literal 0 HcmV?d00001 diff --git a/editor/GuiEditor/scripts/GuiEditorToolsWindow.cs b/editor/GuiEditor/scripts/GuiEditorToolsWindow.cs index 7189db861..ee93fafb7 100644 --- a/editor/GuiEditor/scripts/GuiEditorToolsWindow.cs +++ b/editor/GuiEditor/scripts/GuiEditorToolsWindow.cs @@ -13,8 +13,8 @@ ThemeManager.setProfile(%this.buttonBar, "emptyProfile"); %this.add(%this.buttonBar); - %this.buttonBar.addButton("onProfileEditor", 49, "Open the Gui Profile Editor", ""); - %this.buttonBar.addButton("onSetTheme", 46, "Set this Gui's theme", ""); + %this.buttonBar.addButton("onProfileEditor", $EditorIcon::doc_edit, "Open the Gui Profile Editor", ""); + %this.buttonBar.addButton("onSetTheme", $EditorIcon::grid_2x2, "Set this Gui's theme", ""); } function GuiEditorToolsWindow::onRemove(%this) diff --git a/editor/GuiEditor/scripts/GuiProfileEditorDialog.cs b/editor/GuiEditor/scripts/GuiProfileEditorDialog.cs index d1fe5e7de..5859f51db 100644 --- a/editor/GuiEditor/scripts/GuiProfileEditorDialog.cs +++ b/editor/GuiEditor/scripts/GuiProfileEditorDialog.cs @@ -35,15 +35,15 @@ ThemeManager.setProfile(%this.toolbar, "emptyProfile"); %content.add(%this.toolbar); - %this.toolbar.addButton("onNewTheme", 11, "New Theme", ""); - %this.toolbar.addButton("onRename", 49, "Rename Theme or Stand Alone Profile", "getRootSelected"); - %this.toolbar.addButton("onDelete", 23, "Delete Theme or Stand Alone Profile", "getRootSelected"); - %this.toolbar.addButton("onNewProfile", 25, "New Profile in Category", "getCategorySelected"); - %this.toolbar.addButton("onRemoveProfile", 23, "Remove Extra Profile", "getExtraSelected"); - %this.toolbar.addButton("onNewStandalone", 25, "New Stand Alone Profile", ""); - // Frame 22 is the circular revert arrow; frame 11 (a plus) was reading as - // another "new" button next to the three that really are. - %this.toolbar.addButton("onResetMember", 22, "Reset All Overrides on Member", "getMemberSelected"); + %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"); + %this.toolbar.addButton("onNewProfile", $EditorIcon::round_plus, "New Profile in Category", "getCategorySelected"); + %this.toolbar.addButton("onRemoveProfile", $EditorIcon::round_delete, "Remove Extra Profile", "getExtraSelected"); + %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; diff --git a/editor/GuiEditor/scripts/GuiProfileEditorFieldRow.cs b/editor/GuiEditor/scripts/GuiProfileEditorFieldRow.cs index 2a70cf8cb..0129a8f5d 100644 --- a/editor/GuiEditor/scripts/GuiProfileEditorFieldRow.cs +++ b/editor/GuiEditor/scripts/GuiProfileEditorFieldRow.cs @@ -141,13 +141,13 @@ class = "GuiProfileEditorRowDropDown"; %this.editor = %this.makeInput(%pad, %editorY, %editorW, 22, %kind $= "number", "width"); } - // The per-field reset, shown only while the field is overridden. Frame 22 of - // EditorCore:editorIcons16 is the circular revert arrow. "left" sizing keeps - // the button pinned to the cell's right edge as the grid widens. + // 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 = 22; + Frame = $EditorIcon::playback_reload; HorizSizing = "left"; Position = (%w - %resetW - %pad) SPC (%editorY - 1); Tooltip = "Reset this field to the theme's value"; diff --git a/editor/GuiEditor/scripts/GuiProfileEditorStateColorRow.cs b/editor/GuiEditor/scripts/GuiProfileEditorStateColorRow.cs index e915bb7ee..18ff118df 100644 --- a/editor/GuiEditor/scripts/GuiProfileEditorStateColorRow.cs +++ b/editor/GuiEditor/scripts/GuiProfileEditorStateColorRow.cs @@ -112,12 +112,12 @@ class = "GuiProfileEditorColorPopup"; %this.caption[%i] = %caption; } - // Frame 22 of EditorCore:editorIcons16 is the circular revert arrow. "left" - // sizing pins the button to the cell's right edge as the grid widens. + // 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 = 22; + 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"; diff --git a/editor/ProjectManager/scripts/ProjectGamePanel.cs b/editor/ProjectManager/scripts/ProjectGamePanel.cs index 6df552123..020db772d 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) diff --git a/tests/shots/iconSheet.cs b/tests/shots/iconSheet.cs index 76697061a..0f8762e2f 100644 --- a/tests/shots/iconSheet.cs +++ b/tests/shots/iconSheet.cs @@ -1,5 +1,11 @@ -// Temporary: renders every frame of EditorCore:editorIcons16 at 4x with its index -// so a button can be given an icon that actually means something. +// Renders the editor icon sheets so a button can be given an icon that actually +// means something -- and so a regenerated sheet can be checked against the +// constants that name it. +// +// Page 0-3 walk EditorCore:editorIcons16/24/32/48 a screenful at a time, each +// icon drawn at its frame index. Every sheet is the same 32x10 grid of the same +// 304 icons, so an index names the same picture on all four pages; the pages +// differ only in which source art is being magnified. setLogMode(2); setScriptExecEcho(false); trace(false); @@ -11,33 +17,64 @@ testExec("editor/main.cs"); schedule(2500, 0, "sheetShot"); +$iconSheet::perPage = 60; +$iconSheet::count = 304; +$iconSheet::page = 0; + function sheetShot() { + %first = $iconSheet::page * $iconSheet::perPage; + if(%first >= $iconSheet::count) + { + echo("SHOTS DONE"); + quit(); + return; + } + %page = new GuiControl() { Position = "0 0"; Extent = "1024 768"; }; ThemeManager.setProfile(%page, "panelProfile"); + $iconSheet::gui = %page; - for(%i = 0; %i < 64; %i++) + // One column per icon size, so the same index can be compared across sheets + // in a single glance. + %sizes = "16 24 32 48"; + for(%i = 0; %i < $iconSheet::perPage; %i++) { - %col = %i % 8; - %row = mFloor(%i / 8); - %x = 20 + (%col * 124); - %y = 20 + (%row * 92); + %index = %first + %i; + if(%index >= $iconSheet::count) + { + break; + } - %icon = new GuiSpriteCtrl() + %col = %i % 5; + %row = mFloor(%i / 5); + %x = 12 + (%col * 202); + %y = 12 + (%row * 62); + + for(%s = 0; %s < 4; %s++) { - Position = %x SPC %y; - Extent = "64 64"; - Image = "EditorCore:EditorIcons16"; - ImageSize = "16 16"; - Frame = %i; - ImageColor = "255 255 255 255"; - constrainProportions = "1"; - fullSize = "0"; - }; - ThemeManager.setProfile(%icon, "spriteProfile"); - %page.add(%icon); + %size = getWord(%sizes, %s); + %icon = new GuiSpriteCtrl() + { + Position = (%x + (%s * 44)) SPC %y; + Extent = "40 40"; + Image = "EditorCore:editorIcons" @ %size; + ImageSize = %size SPC %size; + Frame = %index; + ImageColor = "255 255 255 255"; + constrainProportions = "1"; + fullSize = "0"; + }; + ThemeManager.setProfile(%icon, "spriteProfile"); + %page.add(%icon); + } - %label = new GuiControl() { Position = (%x + 66) SPC (%y + 22); Extent = "40 20"; Text = %i; }; + %label = new GuiControl() + { + Position = %x SPC (%y + 42); + Extent = "180 16"; + Text = %index; + }; ThemeManager.setProfile(%label, "labelProfile"); %page.add(%label); } @@ -48,12 +85,14 @@ function sheetShot() function sheetGrab() { - screenShot(testRoot("shots/editorIconSheet.png"), "PNG"); - schedule(500, 0, "sheetDone"); + screenShot(testRoot("shots/editorIconSheet" @ $iconSheet::page @ ".png"), "PNG"); + schedule(500, 0, "sheetNext"); } -function sheetDone() +function sheetNext() { - echo("SHOTS DONE"); - quit(); + Canvas.popDialog($iconSheet::gui); + $iconSheet::gui.delete(); + $iconSheet::page++; + schedule(200, 0, "sheetShot"); } From 8916d58a05d610b5cbc11ce942faa609135d226c Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Tue, 28 Jul 2026 19:05:14 -0400 Subject: [PATCH 02/55] Editor icons: add align top, middle and bottom The sheet had align left, center and right but no vertical counterpart, and the engine has had one all along -- guiTypes.h spells it TopVAlign, MiddleVAlign, BottomVAlign, which is where the names come from (middle, not center, for the vertical one). Derived from the horizontal three rather than drawn. The align bars are a square shape, so a quarter turn is a pure pixel permutation with nothing resampled and nothing lost, and clockwise sends the left edge to the top and the right edge to the bottom -- exactly the mapping wanted: align_left -> align_top align_center -> align_middle align_right -> align_bottom Only the alpha is turned. The set shades its ink on a top-to-bottom ramp that is flat within a row and independent of the shape, so rotating the RGB would have left these three shaded left-to-right and obviously foreign. Instead the ramp is measured off the source icon by least squares and re-laid vertically, which puts the derived icon on the same slope and offset as the one it came from. The art lives in the icon set's derived/ folder, kept apart from the png/ the set shipped with, and the sheet builder now reads both. 307 icons, 13 cells spare. Indices after align_left shift by three, which is what the constants are for. All 19 smoke suites pass. Co-Authored-By: Claude Opus 5 (1M context) --- editor/EditorCore/EditorIcons.cs | 609 +++++++++++---------- editor/EditorCore/images/editorIcons16.png | Bin 52811 -> 52724 bytes editor/EditorCore/images/editorIcons24.png | Bin 91069 -> 91252 bytes editor/EditorCore/images/editorIcons32.png | Bin 118357 -> 118701 bytes editor/EditorCore/images/editorIcons48.png | Bin 178293 -> 178169 bytes 5 files changed, 306 insertions(+), 303 deletions(-) diff --git a/editor/EditorCore/EditorIcons.cs b/editor/EditorCore/EditorIcons.cs index 78b59a339..34fbaaa56 100644 --- a/editor/EditorCore/EditorIcons.cs +++ b/editor/EditorCore/EditorIcons.cs @@ -16,306 +16,309 @@ //----------------------------------------------------------------------------- $EditorIcon::air_signal = 0; -$EditorIcon::align_center = 1; -$EditorIcon::align_just = 2; -$EditorIcon::align_left = 3; -$EditorIcon::align_right = 4; -$EditorIcon::app_window = 5; -$EditorIcon::app_window_alt = 6; -$EditorIcon::app_window_black = 7; -$EditorIcon::app_window_black_alt = 8; -$EditorIcon::app_window_cross = 9; -$EditorIcon::app_window_cross_alt = 10; -$EditorIcon::app_window_shell = 11; -$EditorIcon::app_window_shell_alt = 12; -$EditorIcon::arrow_bottom = 13; -$EditorIcon::arrow_bottom_left = 14; -$EditorIcon::arrow_bottom_rigth = 15; -$EditorIcon::arrow_l = 16; -$EditorIcon::arrow_left = 17; -$EditorIcon::arrow_r = 18; -$EditorIcon::arrow_right = 19; -$EditorIcon::arrow_top = 20; -$EditorIcon::arrow_top_left = 21; -$EditorIcon::arrow_top_right = 22; -$EditorIcon::arrow_two_head = 23; -$EditorIcon::arrow_two_head_2 = 24; -$EditorIcon::attention = 25; -$EditorIcon::balance = 26; -$EditorIcon::battery = 27; -$EditorIcon::bell = 28; -$EditorIcon::book = 29; -$EditorIcon::book_side = 30; -$EditorIcon::bookmark_1 = 31; -$EditorIcon::bookmark_2 = 32; -$EditorIcon::box = 33; -$EditorIcon::br_down = 34; -$EditorIcon::br_next = 35; -$EditorIcon::br_prev = 36; -$EditorIcon::br_up = 37; -$EditorIcon::brackets = 38; -$EditorIcon::browser = 39; -$EditorIcon::brush = 40; -$EditorIcon::bug = 41; -$EditorIcon::burst = 42; -$EditorIcon::calc = 43; -$EditorIcon::calendar_1 = 44; -$EditorIcon::calendar_2 = 45; -$EditorIcon::cancel = 46; -$EditorIcon::case = 47; -$EditorIcon::cassette = 48; -$EditorIcon::cc = 49; -$EditorIcon::cert = 50; -$EditorIcon::chart_bar = 51; -$EditorIcon::chart_line = 52; -$EditorIcon::chart_line_2 = 53; -$EditorIcon::chart_pie = 54; -$EditorIcon::chat_bubble_message_square = 55; -$EditorIcon::checkbox_checked = 56; -$EditorIcon::checkbox_unchecked = 57; -$EditorIcon::checkmark = 58; -$EditorIcon::clip = 59; -$EditorIcon::clipboard_copy = 60; -$EditorIcon::clipboard_cut = 61; -$EditorIcon::clipboard_past = 62; -$EditorIcon::clock = 63; -$EditorIcon::cloud = 64; -$EditorIcon::cloud_rain = 65; -$EditorIcon::coffe_cup = 66; -$EditorIcon::cog = 67; -$EditorIcon::cogs = 68; -$EditorIcon::comp = 69; -$EditorIcon::compass = 70; -$EditorIcon::connect = 71; -$EditorIcon::contact = 72; -$EditorIcon::contact_card = 73; -$EditorIcon::cube = 74; -$EditorIcon::cur_bp = 75; -$EditorIcon::cur_dollar = 76; -$EditorIcon::cur_euro = 77; -$EditorIcon::cur_yen = 78; -$EditorIcon::cursor_H_split = 79; -$EditorIcon::cursor_V_split = 80; -$EditorIcon::cursor_arrow = 81; -$EditorIcon::cursor_drag_arrow = 82; -$EditorIcon::cursor_drag_arrow_2 = 83; -$EditorIcon::cursor_drag_hand = 84; -$EditorIcon::cursor_hand = 85; -$EditorIcon::dashboard = 86; -$EditorIcon::db = 87; -$EditorIcon::delete = 88; -$EditorIcon::delete_server = 89; -$EditorIcon::disconnected = 90; -$EditorIcon::doc_delete = 91; -$EditorIcon::doc_edit = 92; -$EditorIcon::doc_empty = 93; -$EditorIcon::doc_export = 94; -$EditorIcon::doc_import = 95; -$EditorIcon::doc_lines = 96; -$EditorIcon::doc_lines_stright = 97; -$EditorIcon::doc_minus = 98; -$EditorIcon::doc_new = 99; -$EditorIcon::doc_plus = 100; -$EditorIcon::document = 101; -$EditorIcon::download = 102; -$EditorIcon::eject = 103; -$EditorIcon::emotion_sad = 104; -$EditorIcon::emotion_smile = 105; -$EditorIcon::expand = 106; -$EditorIcon::export = 107; -$EditorIcon::eye = 108; -$EditorIcon::eye_inv = 109; -$EditorIcon::facebook = 110; -$EditorIcon::fastforward_next = 111; -$EditorIcon::fill = 112; -$EditorIcon::filter = 113; -$EditorIcon::fire = 114; -$EditorIcon::flag = 115; -$EditorIcon::flag_2 = 116; -$EditorIcon::folder = 117; -$EditorIcon::folder_arrow = 118; -$EditorIcon::folder_delete = 119; -$EditorIcon::folder_minus = 120; -$EditorIcon::folder_open = 121; -$EditorIcon::folder_plus = 122; -$EditorIcon::font_bold = 123; -$EditorIcon::font_italic = 124; -$EditorIcon::font_size = 125; -$EditorIcon::font_strokethrough = 126; -$EditorIcon::font_underline = 127; -$EditorIcon::game_pad = 128; -$EditorIcon::glasses = 129; -$EditorIcon::globe_1 = 130; -$EditorIcon::globe_2 = 131; -$EditorIcon::globe_3 = 132; -$EditorIcon::google = 133; -$EditorIcon::grid_2x2 = 134; -$EditorIcon::grid_3x3 = 135; -$EditorIcon::grid_3x3_2 = 136; -$EditorIcon::hand_1 = 137; -$EditorIcon::hand_2 = 138; -$EditorIcon::hand_contra = 139; -$EditorIcon::hand_pro = 140; -$EditorIcon::hanger = 141; -$EditorIcon::headphones = 142; -$EditorIcon::heart = 143; -$EditorIcon::heart_empty = 144; -$EditorIcon::home = 145; -$EditorIcon::image_text = 146; -$EditorIcon::import = 147; -$EditorIcon::inbox = 148; -$EditorIcon::indent_decrease = 149; -$EditorIcon::indent_increase = 150; -$EditorIcon::info = 151; -$EditorIcon::inject = 152; -$EditorIcon::invisible_light = 153; -$EditorIcon::invisible_revert = 154; -$EditorIcon::iphone = 155; -$EditorIcon::key = 156; -$EditorIcon::layers_1 = 157; -$EditorIcon::layers_2 = 158; -$EditorIcon::lightbulb = 159; -$EditorIcon::lighting = 160; -$EditorIcon::link = 161; -$EditorIcon::list_bullets = 162; -$EditorIcon::list_num = 163; -$EditorIcon::loading_throbber = 164; -$EditorIcon::lock_open = 165; -$EditorIcon::magic_wand = 166; -$EditorIcon::magic_wand_2 = 167; -$EditorIcon::mail = 168; -$EditorIcon::mail_2 = 169; -$EditorIcon::message_attention = 170; -$EditorIcon::mic = 171; -$EditorIcon::microphone = 172; -$EditorIcon::money = 173; -$EditorIcon::monitor = 174; -$EditorIcon::movie = 175; -$EditorIcon::music = 176; -$EditorIcon::music_square = 177; -$EditorIcon::net_comp = 178; -$EditorIcon::network = 179; -$EditorIcon::not_connected = 180; -$EditorIcon::notepad = 181; -$EditorIcon::notepad_2 = 182; -$EditorIcon::off = 183; -$EditorIcon::on = 184; -$EditorIcon::on_off = 185; -$EditorIcon::openid = 186; -$EditorIcon::padlock_closed = 187; -$EditorIcon::padlock_open = 188; -$EditorIcon::page_layout = 189; -$EditorIcon::paper_airplane = 190; -$EditorIcon::paragraph = 191; -$EditorIcon::pencil = 192; -$EditorIcon::phone = 193; -$EditorIcon::phone_1 = 194; -$EditorIcon::phone_2 = 195; -$EditorIcon::phone_touch = 196; -$EditorIcon::photo = 197; -$EditorIcon::picture = 198; -$EditorIcon::pin = 199; -$EditorIcon::pin_2 = 200; -$EditorIcon::pin_map = 201; -$EditorIcon::pin_map_down = 202; -$EditorIcon::pin_map_left = 203; -$EditorIcon::pin_map_right = 204; -$EditorIcon::pin_map_top = 205; -$EditorIcon::pin_sq_down = 206; -$EditorIcon::pin_sq_left = 207; -$EditorIcon::pin_sq_right = 208; -$EditorIcon::pin_sq_top = 209; -$EditorIcon::playback_ff = 210; -$EditorIcon::playback_next = 211; -$EditorIcon::playback_pause = 212; -$EditorIcon::playback_play = 213; -$EditorIcon::playback_prev = 214; -$EditorIcon::playback_rec = 215; -$EditorIcon::playback_reload = 216; -$EditorIcon::playback_rew = 217; -$EditorIcon::playback_stop = 218; -$EditorIcon::podcast = 219; -$EditorIcon::preso = 220; -$EditorIcon::print = 221; -$EditorIcon::push_pin = 222; -$EditorIcon::redo = 223; -$EditorIcon::refresh = 224; -$EditorIcon::reload = 225; -$EditorIcon::rewind_previous = 226; -$EditorIcon::rnd_br_down = 227; -$EditorIcon::rnd_br_first = 228; -$EditorIcon::rnd_br_last = 229; -$EditorIcon::rnd_br_next = 230; -$EditorIcon::rnd_br_prev = 231; -$EditorIcon::rnd_br_up = 232; -$EditorIcon::round = 233; -$EditorIcon::round_and_up = 234; -$EditorIcon::round_arrow_left = 235; -$EditorIcon::round_arrow_right = 236; -$EditorIcon::round_checkmark = 237; -$EditorIcon::round_delete = 238; -$EditorIcon::round_minus = 239; -$EditorIcon::round_plus = 240; -$EditorIcon::rss = 241; -$EditorIcon::rss_sq = 242; -$EditorIcon::sand = 243; -$EditorIcon::sat_dish = 244; -$EditorIcon::save = 245; -$EditorIcon::server = 246; -$EditorIcon::shapes = 247; -$EditorIcon::share = 248; -$EditorIcon::share_2 = 249; -$EditorIcon::shield = 250; -$EditorIcon::shield_2 = 251; -$EditorIcon::shop_cart = 252; -$EditorIcon::shopping_bag = 253; -$EditorIcon::shopping_bag_dollar = 254; -$EditorIcon::sound_high = 255; -$EditorIcon::sound_low = 256; -$EditorIcon::sound_mute = 257; -$EditorIcon::spechbubble = 258; -$EditorIcon::spechbubble_2 = 259; -$EditorIcon::spechbubble_sq = 260; -$EditorIcon::spechbubble_sq_line = 261; -$EditorIcon::sq_br_down = 262; -$EditorIcon::sq_br_first = 263; -$EditorIcon::sq_br_last = 264; -$EditorIcon::sq_br_next = 265; -$EditorIcon::sq_br_prev = 266; -$EditorIcon::sq_br_up = 267; -$EditorIcon::sq_down = 268; -$EditorIcon::sq_minus = 269; -$EditorIcon::sq_next = 270; -$EditorIcon::sq_plus = 271; -$EditorIcon::sq_prev = 272; -$EditorIcon::sq_up = 273; -$EditorIcon::square_shape = 274; -$EditorIcon::stairs_down = 275; -$EditorIcon::stairs_up = 276; -$EditorIcon::star = 277; -$EditorIcon::star_fav = 278; -$EditorIcon::star_fav_empty = 279; -$EditorIcon::stop_watch = 280; -$EditorIcon::sun = 281; -$EditorIcon::tag = 282; -$EditorIcon::tape = 283; -$EditorIcon::target = 284; -$EditorIcon::text_curstor = 285; -$EditorIcon::text_letter_t = 286; -$EditorIcon::top_right_expand = 287; -$EditorIcon::track = 288; -$EditorIcon::trash = 289; -$EditorIcon::twitter = 290; -$EditorIcon::twitter_2 = 291; -$EditorIcon::undo = 292; -$EditorIcon::user = 293; -$EditorIcon::users = 294; -$EditorIcon::vault = 295; -$EditorIcon::wallet = 296; -$EditorIcon::wifi_router = 297; -$EditorIcon::wireless_signal = 298; -$EditorIcon::wrench = 299; -$EditorIcon::wrench_plus = 300; -$EditorIcon::wrench_plus_2 = 301; -$EditorIcon::youtube = 302; -$EditorIcon::zoom = 303; +$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/images/editorIcons16.png b/editor/EditorCore/images/editorIcons16.png index f334115f5f59ed53a01e4f9fa0e4ef36153a8d8e..0d09ee41d1f053925f518e8b6e17980ec1a0806f 100644 GIT binary patch literal 52724 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 z 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 diff --git a/editor/EditorCore/images/editorIcons24.png b/editor/EditorCore/images/editorIcons24.png index bbd178c9d4133249b5d887488ab0785d0c8ce38d..b019fe64b23b06ae106d54f62fb05658a62d3659 100644 GIT binary patch literal 91252 zcmXtfbzD?k)b*u@lA%KZM^Z|pJ4TR@lI~PMKtQ?~Wk8XXmPSCN`=MJvNaa!Uxc%DCrp`YYkeRcDBN|!59x#i@)%!;f(^t*?c)8TnS(zd21 z7Llx*V&|OttMXW`awl!uf)Yr=^jNqZSN3_Mg^pH#z zMu>)ifx*<5Ra8WT)vt{NBF>Txo}6=kb#`tJ|4YEV zj|(sIO!*!?c@m-hJgUW~a{Tk>&-SH`qa!2mmbSKJ=Y7xfy zLVlK;oztrcw}y0|EKIQNO?hkM{rciss|)ZSA@A8+onp;?? zk|XuUjId%q!4+eMg~}tKn}6|5AAS>Na@}=(l^9gs`KO|X`N*gvEiKK}#Kh#FwnU}D z)SMMSad@*n zLCSx_MZM|jlUd{{<(1ctotOv+?jP0yYsSOwe%CcLq@JfC#$0iB8F;gMSiW_Y6(5#7 z@LVuQR1RkJAbCLVeB}H0XwECanYz~MYRZef#jZ3hY@W%*`8)`!ov9A=tbJf$VBnR6 z;ZSWqJ{GR2tK*j8go-i*o`zt_{j-1pcKi4Mr65U&sNpx@lgi1-$rR#%4=}{Y+kYU*JTRPtY4UBSAy02J`j#l--u>#kTuA&)VE`H}Y2P&Nob zPhZ~#?dQ^yfvbd{k6xEOiG?qrWt4#}SH+oBbJwyp){-HV4)Hc5_=}3^1jSu z5A!~;@t@0kBOQBv-c(^C$}kD>+GIen49+mqy}%Ztzssp z+U5>}`vliVJ`EgTBWc#WOW#*;W3^w*&6(T#`56~?3+(Fn=(jsDUe#PhD9Si2v{X@& zMJfgSOF1A!a-ySe{R$M9m0j!VnvMQ;elG6e?aeLB32m*dt>ygv>ue#dGV3o-YM!f* zuO03~B8meU@+1#v*Y16GYv2JHrAWG1yAXnSen|cJLOc7A#3nQoj|7r!sJIiJ99EF`*#hCy^WkYQa{JBF> za8O6rK3*Fs{xKOKyr>7!skT=VCpJj&DeMxvW*r&tK1akWNRCFF^ z_C3>w7P7!+Y9^E4S5!AIr{qoDk>6-zh@dk%q@%KotMb&aS{F zhUhKFhvH)H_4RcvC!T{Xjb>FTH3(MUopE@02qDm+3JzvwW)J-|QrhnTXJCxf>A4eC zptd8 zRGmP#?|~TewKAq!Kzpc#3N6`qiiN|g%()Vg($dnl4L*)C_btdH_hsW)yQRxM0HZ&k zBFbrPZB0!;A#=2<_bj*)2YE2T--n0I5*=t0SZBw^D21PDQNnUi1hY2vxrXc@%UMf!i35l0c2vVsDR@Q3DA0&aVYvihf5EBEmsSvXRgT8itvUV)*9I;a$_jg1uL z_b+_P*m|SdukO;({h=e!EcRa4$kB?0BO{KLIsa~LJ!yS@HjIdDJwC^zQ9-hq*9=rp zWaXgV**nX~;3si;Kj!c0DuWHuOE(O@HGQ<>-OG>)5%_!;s;8mawzP>1DY0SeF^pp^ z{5vrn@a|DCoacS_J^ts!Hrwn;w^`6mj~=jdaMS|xx#i+6yETlguYbXZTzz~}vXjL5 zb+k=;(wi2OUG6W6LXetruV=O=1>zZ_&7C+_70Nx=iD!YWx_#XXkCfU=T*G?3W5Eu$ zv$M;2{TfSCwS9WpXz0?MlS<#vS1qg8I!BB1iw~+(}}+Z^U-BR`4<4DJK&VyuQ~|I z)6dL=hcm3`=1rvzZW}5L2%W2M6NkPrWz%gx9bvp^_N#sM*oFSaoj;*Y1|!gn$# zZ6z7{{J{TS)^K%^QMmC`8_n6RHRk*|JP;3=!)uSOudj_u9a&Vgi?leujF&R?tsRcc z&zFx=DmhXgd?Dxj_AeqJbZnM$XfQ`+!v6j%-sFJ>ZS3fnm`CVXW+YzHd~@i3=2o)j z0j*`7!HnFhrL;t$S{adypY6g3bJ^e2{C;wChmP*u1YsK+^4kT-1NQdz-x4WY-Q1Q2 zTK!*+Y`O+m;gfgqQ|P%Vbn=yq2J`n23D%2oUK+h@a~gO-D4>@-Hj< za@s5wB!|v{ya4= zeE6Bse1tL6A^ZQ&)-$1q=_N;)Q{ zNidrXE-bID8r4#?Rzbf_>To_z*3fO0KSf=htnL^oooH2*35TbLhb|WMZN&{Cs@lG| zXl3u@G)QAWMn+~vj1f{$?^|&T$U>Dv5DN>7SlSRD9C-sfRJ(`1d3APUO_vx3CyI)R zvEhoG%w>+T&AFozW_7h!F)te(9lZqZ_{n-@H?bWCycrM>P{Pd2Jb*puqQ7E(?jQE{ z?eUuKKkM8>$10H8NxX7%!+!MWk;4!|uD7?>`itf^sVJt%JGU$u&Fr(a^`JTAgbDj- zmkeu(@bBp8X#B-bup#bMo(gC_*vpU!F}~;=Z+zFm`gxq&DTgppLP{#V$n*L|RoS+i zOq}8maoa==DI$9iHWD?+pC-In!&7;%gBudRRp=Z!c$XH_`9b=LU0TSccDE_l|W$2 zKHF+F!gkZ2X{~j2v`bC*h6e|E>WxBv|2FF+#*B@PrI=*FcgU$v2)*O{w(nn8(3Bw%*aoj z2?e~B;gI*hyy3ZzPkmWbY2nc+-1Hvnx*eS#QgTTXwIg{kA5+Wj>rEK2Ka6$I*@$pZq z)8o|G|2!r%#6cMmTe%Ot_)ZL^Bo%_qY9R2D1iwR4Kw7#KYQMKOb1}cm>gZeF&wsqaDDg_)Qz+-vd7rvTe*41w;;y*U{x&;W8U z&2g1Z^w=%HLi~D96#zt+)Ml?7{NymJTbpo^W!@009vXy0;07svV{U%QZ<&C=Q$IsNNb zh~IrI8ylPcj*i!i0g>>FjgFahZ%xAIpkZCvoAo@D=(=(>2IG}>YF(P2hSI} zmKs#v8PM1Zc5vt~n@KEGyzF{QHnY0A+Dv;V9}&7)pqX{VWw%h5a%nZG>k<%Pn!DCb z;hmmzjqbe`cRDrUAy$Q{h{Gx=%U}7C(S{)X@TY--jz?1hy4@>Y`AK~3qg&E1i6n!HS z)ThOih}zH2e7NHKzADPM^GEKAM9QvsTywCq*8rza#C_3FSHEAoK#Ke(fc}&;IyROI zdSWyj8Qk2Nl9{RhW5eTj0t)!_Ian;w&2V91VTwu0hKFC@a0Rnbk{);W#76dR{W4Xs z`J-nX9z1`X;!VpBgV8iq`LR<=`sBDW2|JK_#e2-y@ zinEKob90%`r!}2xKdysV#lL#T|`;9Ie ztE*YCP`aS`#fG-Gx6!fzXR))>)8Qkves1cC6&!oDloiuUe|L8JU^znG@DcYOqBb(0 zUEJ;Al;B}3lxRE-zqt6vnfZBx1|NgDB1&wO5ZlP+__%KWj+Eo;-DP^+ccdSsXwNG| zp2qJ|RlF~?9UOFqOF85+fJp+h?gZESl8NS4R_-g6^4r?kC4$qI8FQ0$#GB7m zO-);Aaf=2VmgrakQPG0Qr{@NCwzdgVPRpquKaT#`s7a*AMxc>ZJT_^U550ri)y|Ti zpHHmS_f?fRySO+`MB-AN@*_Egz`edNF4*X{>7>o7t4DNJSU1}JbMK))-Mgi(srhTp zMsbZR@x7Ut791k#^mYX2{;TStGV-oWj|-Bz9(pxd0SjbJ5rHL$-2&VVbzjKCj7)OJ zu$R|ub{pDiG_QMXP{qU1(r$MuaW|S6yE43OB*@24sf#}j+BK)8wR@n4^K;yh2f`M#3b3w^A3xUrt9ll@(m^U$iyyObBGzzp5}r@Ra79>2HY!2paB zsD}*}L>WXl;qLBU85I@9I&O}oR1K#JJQPY934XwGetu|I8eLy7Sv5cWVmUPx4|Vj#mY130PxS)$8b4t{+kgK)t`}q()0&vqrH4r@ zYzSPHGIXzp*EBWV1}l1+*38P;?1gtXL*t3iV$Z2giu7FmJHiO{8+(OE#zO+0ylt z4Qwcaby+z(f2Ura^DPU+goz#wN|8)s*Y zU$e6$)OMFhQmI#>V!At-YZ8v^i?ZYwqZ@}e5BsD)EV{7Np%v%CNfs(dzWj5ov4{4Aeyu7@Wr6<$jKTHTd-4!V>yu1Rv`lv}J#>4^uAM@XrUgY44 zQOtqr`~jdrR~usmoZ~du)!OAL*LHBBL~zZg_|RM1>u{_wPRm;`PkY@d-A{ENkJ!EzxrB zA4p@oy8CUZp|+MSOj?X)&YtbogGc4ZinkdUZm^82hM$PdK8exHlDU8T_H9ga=nV%S zZYmYDFq|dZJAXKPb4Vm7-J;XSCD7-6n?>2u-CaaCFCJ-9mKTI$1}N6K~{TM@Vu5uaZ9_HA|hgGL%4?IK1ILf5?8uO8gg8=lHy zx~uEk+L*sT6JSjnqH$}O?V|6bd(v&aKMYRP4FH;G(}C(ZQFRtlc;F8mL) zEr^#+PG3btM1BAlD4dmmZ@9mIxE8kvkgzQHQxs-}^w{uSXniLYAK!bHoK#JPqrA8qU&hd z1xeS@A*fDK-dkEt9PJ70x?I%sH%aW>d!j<@5HtXCQ6-nVHDLuqK4To?b;;@R9i|d7 z`}beNG+^N(!oqBfe{oE>=Y9Lz)Ue(m&`AKC#zN!)hTz7*-X0Ye7FG_xJNXOms_JU@ zj&R|LH6l(30(`)YXglqmtnxaUP9x)-rq!a z+Kuo>3bW%Pj~NP66HR{bM{=mOG&D??A>ZjBc-CV-UPGvNu{_nFxB}4AxOhRQH>9&M zr>gv(x3_nFLxZ?+x!v6F-+drrK+7e4dUF#r5sA9F{WxFy!@A^UUwaa$NK3TNzkmN$ zgdqq!UME*|WOzbfX0Br!F>H|X?EJh<0rC@G39d;OIKBZlvYX8@A6XT?p@}pE42DhQ zF$aQm$5o@Lq=Tl^tZjYiqKL4@d2t?47(54LLaJ}4gWmT!JA&CbxVu*`$=O>u9?P`9 z`l8#8gl>AVt_iF6aI(&OMWK}7tv$Rei=6OsXSqJm{d;@U()zBshsx@4B|?b>cL*`S z{CLg>>(M#nGOni z)zfy|)YKH45zrrlPhj;R_)A1i`|Q*4iI$$gEY}}}k??!u$n43OuCC;}Xyq^~xkNG% z5}W^WayowWuY27JY90V*i)CQB40xRF$AhKW7i-O5udf3a!KIbIe>L!Z^oZJv`qF6! zR@}^}I)42!p>bv{OMDC)HP3kih^UgZhQG6D{-fYP=CEZbr7H!-NP+x}G|>hGs8sZ>66FrhElp&_RBZk0JD5?65Dmm=)xKe20W>I66CLx;Q)E8XcBGE-n(Vez+rzV0UHn2! zttdLQ-j9|B1L`aBJU+CoK<^~X0ft~0R ziO^jS7h1r-1oHj{?(aoP;T+M=J5g=2p<~BKMk*}XuW%qxTAyOh_WwvBl^?-)^T%FH zhe#C?3TX?F*j%5Sh%EfXaSaY;#)UxBd*3%WczBlo3D18?_uk+_j(}S`1cpbTJ7-VH zk;-S_$#yzAI+2|TkYzrgYPZbxf{+g0SpnmenS8jn**MF-0|m`xUsQu90dKN=iC|w|0IqblXY5X zQbVX8lapbp)fgU2+Ww5bE!xSdeVpyS?B7n+&hl%kwT^reMzUNM)qq zlYPJ8z578@RaY$NB#^0WL>%9MN5|dKkZzXEXH+c}`nf3lS|aWn1eK5@-*}>~?XR&Q zP!T%8-h3Svh7V}EeQK>Q!a9FeyR)tp8cdS_g~-all~D3w-bz1gMCIqF7`I37e~>n4 z>`f#kCD}KaYP<_rUtP6T+zYVs!(U>r5$Yg}8vu~YEQ2B-hyIacM-9)oQhB)36x8ft zh}K4Ca5?%H-I`(j=RxmFFRXdzXp8mK0i?lxeN2zyC^utGp$rW^DvxL2K0#vtr-Y!e zp;eQYF#zYT0Zz)aP}N;c_0EH|uxkK6ar&;$7>*Gp?z#JvP8O2Oz0FH_`nBE z?9>^3dxsC@fL?JsWNTO6vRqi`vnk)(P*=J^;_`8c@67AKG(Bp{kBGzdpLvqNs;tAT z0;p9x@xEYpeP{0~T>u-s9B^<6zhz9#%t9pru5oLM-+D}k%^qpj0?WrLc4Z;P5?Y|W zDlRUrSm@I(x{vus@L9bCtU~bwKtFVOg?{!458t)K%OXEX?Uvw)iHefe?s?yBt@cjR zJO}$e9BF(0(~j&&3D_b^yMw4GH0qpHlm~QqJs=f5haWr&cvD{c-+x>QL631wZxN~U z^S)RB%od6=Ob6Uj3Annvk+(Mco12>k00P<+f5z3t-wLaFI6gB|G+~(@4_1b~eeD?P zGPZAW^5?p|yxez1arwDEvIX~6=Nbe(Gt$`k``5bI^_a{5PM)gV{PD3TYU`qD{U^>s z+_%14C8Hh5JW~@hGw%yWHl9&k+hRMHyEr-Gfh#P@%`&H%R^Z*CH);WmCEgoJ#l>Z6)1&MkC21Bq4NmCFH1CtY5+*Ob zB-S`-DlJuSX>7b$`ect{Q5@zcVayB@3#i+xm*(eP6#d=NVA0gv>_IJYMAV4=z{AUH zvLqHzb*j2QN!$`snQ$usX*I!OF8zrS;`e{`3V}dpCRnXZ?HnA$>6)0tJ8Gk2L)Um7 zPJ`4Da1ch3)=G2_W6s3P91~7^a&pqc$Hyn9Cc}!h9DTQJ74_cvqU)@Npz)W$8sGWL zcSO6~>`_-=XAAPENkO$)D>KW#(gCwkBRUr&gi-?8$so-vZCj~t8XrW(2VyM8vFD#X zFu3OV7~t#sg6#?ipc1_(E+I1f_iF5vpAoeJ@xyTBC^(O+QR5Grtvz_~U`kR_@{z}H zA+A%!t$e6CJd81@Ri{N70<0tuWnkCLgsYhaS&>nj386=5Zq$5xb9E(UMZpnM$R83oa$`W! z#ZQT(1k9<$%&%X$d?z2fj}I@4JUl$?*;Pxt0t1IO+XZv{{&|{JfDCI2s7U68moDQ% zJnM)Ws&0>#m6avX^PorWAy_AV|IRp|m>Hj#2*pd0hae`VYOmCVi0#gzQg9gC99EoH zi~Afr1v@C_Id4@Xu!QWFEr&Pr&@TVh$^J)zXpu%L=Vp1-f2e%)|Obw*k`l_ zIg*^n+q`)W$HGWPRyO$#-9Qb{t>hWOM$#zzvvj7v)DfU4^4TE6LR~H{#Iuh*p%3wC zzu4|5!LGMAH-7-sA8qv2{)c6MNj_kWYX6MT-KPEN&6hM59!tXuc*^XYCr}08@K*i( z{5(Xr^tlrC)wteULa>I==kgGR9+v?xpmLP~#rYn`u!<8DDk{kcM}L3+&4UB&MJWyn zsKmn5bkN(=bAzV-1~A@&`hWa?aY5bIAL=$)3n6`V!znQIl70iV?uH@gyd@w`Ko{Oe zetSUUWy2zt4@b5YE_~}GY}NT(jMH9{{caQ3Xh5&OXgZ^I-Tj9s)~W*_QjLk5tt;!_ z!HH!4G`H$mrSPLmHy;w5_fFa+%

FM#_T#epvj5=+;8PdIQG!-n07| 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ԍuw|GV^h!qL->9au$5cZ!XCSr5}x0PQ_ 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 diff --git a/editor/EditorCore/images/editorIcons32.png b/editor/EditorCore/images/editorIcons32.png index fe235985244628d8902238580b3545b21cb41b9c..377e0eab77d22705867e581807660ee26a43b5db 100644 GIT binary patch literal 118701 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
    v0V!C5Ou!=Eh>y4?hQU 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≀+1O; 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 diff --git a/editor/EditorCore/images/editorIcons48.png b/editor/EditorCore/images/editorIcons48.png index 4c4cc00d604ff59ced14c5c1718d3043bf10a3b7..0799816ae7ff00584bf54315c4865e54edbf2e50 100644 GIT binary patch literal 178169 zcmYiN1yq#X_dX8aGjyl4bc@nTH_{z~ARPiK0-`j7AR!}NN;lFe(jeW4v~-8kNHhP# z^ZEYX^{!b1)Au=h@3Z6D*ACOxR3gBo!G$1*Kt)+W7lJUsL)aqr?LTXRJbMTRK`IKe z`W`cXo;f`I-Sex=ZUGMeK^TV3wAangL3N4F&k^eTevu5~K%}(x;q0eC!#E>8DJs~^ ze~N4q${QAls?YEg)zlnc=IwZ14RrMAzWdrxzx{;veGplf$j=oeY#4{8&$ZhPxGi^t zk>#VO1v)`dhx>ZeOhWCVJz-=Vnk~KKLQoDY14+#77V&zygA2Wmg*k|PDC~m01Ex*b z>ax6(UgkgKubCp^vfR;of80@c7|T*#9!)yv_JyrF^+A}7&Z$9jW~S_N$`ZH=dbpDX z(#2U`KCrJ&r)8|exb^lr|9u@SA#ojh<8~?EN7~!v3DW{m5Y!nGg7y5xi?|pH4SXnT z_ud^Q*6tTbE{J&1(~bs9TC@%CjYM9UR3avVbVK=C7wx4N?*VqnI6p5sWP|~|I5ZZf zBeZl@hFLd$%+4HtTEfn1koPcGF`eZp|$+xF2pdOqKHlK!`Q3j5`@zaVEw(Yt`gwL;M$iw4nl{2P&STICt+g3gAm zkoTZ+J1T2!uO1rnr4+_>+wRcD^H?_)Qqy}M_~%D@k)NKHB)sQTg-ERJSxUF+Yo2}@ zFEL#Dxniqm3-$1wwh9T24E%I>A*)xHnaLE3aJl&MMEC4!bizO76yuy1ues#0OL}lH zCS!;iPpSk(4qPS=2v;*>;QVFFf4T(v-6+g^i1+_ z0ePsQp`mNq_uVsRIyg93x8qEQ0y6yJ!_Mo8#S2rQgcy}>xY)RoS8s02emV4m4^!Vf zluuN#dx#VG$tkw?Xw zySqb2#Fx=bQ`$cT^_b01= zj6k=SFQv-rk(vqWMC>%lXPv)G4%x+GiR7TArPZ;vX8A#-6|;w&#IgC~vXURfKa_-= z-u4~>HVkO~{#p#0o2Y=M{yR`*>)U#{w%x(5fM5k9llq++} z@_7XJDp|v|tG9PD3)}u|&(QMUzb_vI7^zBW`r^~j(y}34q;MYc<|`n+bGgz#-}Bi8 zqP`QLZv4HWq5YNgV|&E!;lF?X!p$)j*;nKmTUvPM7Z#j0V!zIQZ)xcU>W#m=yvzl9 zkM6AcIX5?lDe*X<5Q=hm^(tY`D`oVCzR1sus zmMMPg|2q`sSUdNJsG)cf#f6wEd45sz37)LiD-@zB;CCJ@%+2NYe%?H8>N_9)@5X?E zGFJ4Mo1OKL3O6w^(fN^e>AEjrrO(cmDOcfnbGav7G8{>--{XI_FT1?{=6EL{@htSO z_h^5C4^S1)zy85N4MHaUW7zBl_PF7RYhF)!Svlp(O|;LP*4 zMmg2h)#``wGWXpQB&JBXqjQ^83xr{<#hCNf$S2Ou&Vu*v->(c;O!z@n#vFt`7)DHS zHvFG|X>~r@n2I9tVZp6>v9w8QMP>VM>)$^LzOHm)xrl}TcW#t={Ox{b@5|A=u|j@g z)WL-e8R+Q+K7INWFeA0l>???dfICKt7Io{&Jn)zyX={rmTRkTGE>*%)m`GR9Gw_EtkfL$P`5#0OIq zOJry06yyBtY|c{?6WeCyRzmUNizs0@R6ILBKlH7-I?hKiZ+?D0?~NvO2NJkQeCO?b z`9=W9g7KOe4Pn37Lc0NDVP_YE*XQPbu#0dhFJ(7*`ZNw5`RhJPXY`Ej=!7>aPt_qD zv${&bP`eBzX5{QL1hu-%MXDs;S7H61Eho177oFGV?(8@XUpY8A$-9qc=n)D)+n1+% zhKns}hkFYxiD#M-uO#>LCMtZ*MUZy&4{X?XLXUh*Op?hJDJTc0jBL1MLNz~U?50;E zA8GDp?UKl4eF(f?-E+3Hv%}``M>fjY$?4uRb8{yPx3487_xI!y`>She;w2@N9Kjp= zcPzQA(V;2EDbcgnGldY}^vq15*C2!dGCW^jXV1R3Qsw3An+jGJTO!duIXU@F6D_o> zh{&C$P8-urMwo}Y_S-jZ9IE^sPqye`esk2lhZ5?sE%w{C=BzK8IF>BPKY1^%uRmU` z5&X}uyWDUv*Hdis8zeK2ieo@U@7}%pyuNOir-?W@ai`g5fHJ-}HxugV>5-bp-zxhc zQKz$^K^(h*XK;MnfOse0!`&SN*rx7)&3E2zg{iTb8E&wgVYa9}{`5JDU(b|9p$aI?^E zvm}Hle3bIGWqlb*u3x@Q1{-pl+cd!&?THl?AsAOG|UQXwE~~W5|5Q?w~m8x+-h7i1@-4xi(Tw*nR$yu zI?LU=cRAN}d_`YlVfM1pehDL6?2crp{!j7qO|fTh3H>F@`wEvju9}ua;_}Z=cAkvX ztRI1I#bD(t5Py@IFOp|=tgo-fU6rh}?hbu*-|F}Mp%vZcQ0z$nTTRUg zJbaYA!}|z-He`cf#182{$yUY?Pia9DyRq!1o2z=Hgzw3*dKC#w;lJ*9}B3oESC^4xvF@ev`DIEz@ zwXDqctqbG|THjIwNgx41(*yq^jQKmuBLkNg7xkNRW$`I}`)8(AZqR;RcDD8CsRmgToGqk zCs6VO@lzPY&tCR@c4{igvWfo3ABThe{c@UYq!Wg<#P6{&*6ugbE1dn~re0#)%ZUx zp{J}8-=V0N+55hwzl9;rcW>WPsNX!qg3`ss#W}cupsK2>UL2*ArKYC#v-_LC5pV46 zH;~=#M(Q#dQKM5+B3nOYt2#V*FnEV3Y-$VKH6fO-*qQ3ucuC@rh>I6BB=ina==V2O z>P%jtQ=xgE+4+A_Z|@_jxXaX;5DH#nu!&*&+lNyQTtED3k`wjsuL%xw>1T$=t5qgh zFQ<0wB!i5X+p{X}D^O8?1<4x7Uo@Y8d&Ow$^x(k*SCa9#;dk!tDzg2r4Xs1*FwUEw z4}-wau+TL!JBx%T{OS^pvY%sE+1)J-CGIeGT>jF!X@mMD|0%BhK4Y- zG!SicNeR5XyeFRmWD{Y;c>B_MgWX2)HXu=d)V6+wMQAt9``oDPD)RTGYrj_W&H06Y z@0EY=#v*VTMh`dD+gL&c@pda!J|bKFA;j`+jf-Xq6rSx&^6u*@Eg*-6hqFzkJCBasJWjUb$usfu>E-D-mrpUE$lNZ+ zny37jhdCZ@ZfHEJ)^qdoxKKGrzs{dIv9An(r*Mee=wv=~46iC@sWc6@tH6WQvn=vL z)7;F=4ya!ZCWJCDZ}MU@5lN2&SHOTOq4GZlgi-4)9|zrR4pW3A61v`q6G;=evIUJ; zxw%!rq1W%;ae%;|t7QBv_PL`g-;dSsa6D@w1oCrt2ra#q&19UA#!VVbGv)iz;Q8=Y z=YeRLn~V6m$nXK%e){l-NYR11jsR)kT^u0dE2?quKo%kWKEzeGID6qE$Zq)f`I$VM z+9oCpDWI{iurPt|P~|din$mar?Pm{gpz{BVZED6~eLT~z+)z=ow?7`*E$Ja-OjA@phh$RGd*gwT}Xp95=u{_w=~g5-f^7kV8*#qzj$ zY;GCT}o=|V6JGn5-?21c5Ta!4 z<>!H+cc~-#`}@`H?ekx>Z3tUD+2+PRU;XOy3G=4=#r1FaKGUa~f-bXS0wl(=QsA_%xI+tQ*0(C<7sc%cc==mnIf`W&wYCB?9@caUy*m zJCxo>YgOVUgmfqJ70y#)#@*hVvCw%$)`|uO zwz9-F;R`uDek)WY-CxozBh$xC^$k*gi84Zv>+1RW_tMV=T~{#z5Kr3PW(Mx9NpKEl z&7W*;7J#ggGL+21*_r6^Vv{4I=_q^i4oe$Firzc&P{e}Pu4t4DH-m| zeba;ev!X{w$H(Ls7Z*948$MoMH0OSkZz@>-VsNBJ#&v(rPyKa%?s+j@^u%_?=~++H z^7(mPPvJm>CeaZ8tcj?BL5tDW+=H}e3S$MM2qsqjEHu$GB{Y9O_Cq1NO$4ZE z7=uP}v?=-^z9LF`pR1)vhV4)hl&H&c93&cF1MlMNDvDRN9WBr&>-4txl)7?JfHQRqe6$BaIyayUwuhMGG1-{~ou3;fUCUi<$3zO(h*!ou*h zgqDWKs?Upl#F&dk5CjzgkCDxLH<~XaWoCNr_wV0$#~UtR;#3Z}oduejn%2-ubC-L2 zagI+<)pQyMVq;@_PA@^3Csr8Tf=?%+YHn^mU96B}f}n#8hlhsVKg~Y^uEW_u%@i|w zPM&|A&7(u0sWh7>x!&!Jf-;OMpwd=VL_d0?R8pr*Q%t*CdNysu^a~ftABG~b_}pWp14zF4C#Ate<&S!S+gDRq2yCPH!9WLT6-_M$IG z7E7eCkefJJB*LxlYauthwU`mRkRc=0$}g*To;20N0V76lR0-+wwX96by<$NP+UKC6 zg9u$0SaPEN=kfue2gFXhe5uc@vp&Yo|x~XD%tChpUHq|Gc7F*mo=Pi$X8_)Dp$3Mq_H?| z*=&PB`$-tuhXJWRF6YTDJ;v$z@o2C5@Tt?RXEqoIj_&o5%7Du;;}{ZsG1S!3vInP| zyda+*;$<3rxaGFGve9uq99BlK3l*HwL%`thI5;>2bTS5!wX?mwy$h?JqO>gB3kwV8 zxXHi+NkcaqTAG^bFn3);Muv7_vh5`E8BsOwmJu^s|o;UOI%tLfey`5gI=XX$qs?UpRYW4 z&&)#*$bx~6&IVy)Bs@G|YXa30JQoE*Cpu+ z-|Y%^AnottgKq&Zh^>U5QAL!%3jK^=BHouCuuDKgLnAeoFAgX!PV8w9N*{D6 zrIgkG3>hmwwCxp5Iy?1v{hCn($y64PLrX`8UqVN{B8rK5)37wy))v4gB$TtQRtSn+ zwoaT3xQv~Rjlx1V%blYhe?G1!pW&1zqmdC2;N01lxxFC~zB%9AeC}ysA>(d*`HY^5 zN_!T$1@^xE^XE_JHP7PpQ2rY-NcC0M@{|riI8$6?R@mCkWEoqGhAz@5 zfxRZ^!(-d>2Oqj{;Ji!%>H7zhYGm#Wn|~WCbk0Ubv^PKXUXR?|_*pErV9j4)N<|`( zJ)qL5^7|qiQn5}V_ZgU&HF+(5Wa}$#ChsS-WksXX*i1}0e>i`+ioUh=B$hLmklN!# z9yEq;@Ob$QJprP!KF{zPf<(l`KEkj3C;IK&Y$lo!`oc#e(gkxk>i16{1w{{b$XL}LLMOD|&xzHbhbN*rCwWUQzNlbwXH?h_8 z=LL@l8EIVJtLQXkgVl!}ONI{M+@5Oy4YHR6VQi{$#{OcT5J_b&!WTYmpWE83{f1p%kE)jy$;`tIHPAXxnFV-6bm11z2^ zRJgpU>18*^9i}*HTH08RA;{nzW)3NIV~>f2Wk6Z4jqCH!+5P}ykdd;4dwJ&fg;|K50- zgP&{Wfhj{B6Cd+*eI?T8%Axs&#a!B9Mz|}?9@@u8T$ri>i^mHI331lwYk1B|KuGwY z!~Ggd^ak3fRKO>zs(8@-W-v%#oo^?lx%$>3h>9H8rcUFdxZ`mZw$Q2Q=;A=xPPT(i zMMV__A`erTCIWp|4`7&JEv~w2_;Q@mA|ghG`KQl8CJGXr5+p4U*V}jRZln3`?)zUy zsn0?7$+y3N<;>Hz0UW^A#&CMVRZ3D4Cq@lsPgS^~_ggC~xo@Iq`mUJ89aF))Xsv&4 zp$3^1^ND}INFf`}WE8j15WML0?d#Y3#S)RzMl}Kec9H+sy}=10AFeU&As#18pB0km z$QU#G>3Cmp8Db0%NW?n#ji}omK(QDXTg>N^6o2k)f#N$Ri!-{%4ezxe%f~Er7t8`EYWnx@do$?K z3W}pWJ<9X)TLnDO_xmnqA-y*j?Q7Y;K+9lyX=#a6;~I5+?b}vj-LCzEt7yxRf)F8e zw)w6SbQz*HeBg*RaL^c=9*^=rz1#u;A;s7^J}*azz#E4U8l_@y?;h^nlPfI?UHK?DYU#JwfJTfB&^q zgcJK8{>#s5h_$vm^QU&bse#*q?U!RsdzGw!yZy(rx*!}$M@myul`n8Aj=D|fxmEeZa+Kw-cg?NsZ6kk!@1xM zyAF9SgS21JgJF$Tm7BSSca{Baj4j}|RZL9M{*(*=*Gs)wDK`4bi;Xde0>p7jiQRs- zNS(>9OKf^UMK$s!aa|~i?c$65bX9FFM+6ed`PdjF=^T#3K-&Ekan2`6Fd47A9iODo z5FK}+6cHHKV^RK%mLGL(lcMjSMjouz`FXRCR9{{>rrW_}X2F+svYE0M?TuFKMH8Ev zB-I$_)Z(5rMNIFR!g=-}**H^dYy9G5y~>=lGsOks1$j$2=ZffLe!Bf{4gc9eef`8Mi6^aMdX z{6pyX0;45{)!Q^+t=qf1ul(M;sV2veZl{wjssrbJ$QZT-QjBwv+fr!2>H&B>3hH15 z{7qfz16Y5GhzKL13+FX}`M#1_+}6qfM=vImar0qo#bN9we*w*0EamVhaPKZk7X??l z@2H;l*~1wPBM>obknmMjVLsv%^XdQmG%2l=wY&b0h1% zd*y%mujB=biJhF-o@qS6f$m=FZ#4RW_35?M?zjmY)CP1km~o;Zw}DF~$u zw?f*R;UCu3iZk%5^fjOLwKpC(jOIC0v+dQzY+ci<}Mrq0@f!@S%XseH*kGZ?+(9KUY`XlRC;otDYZhf=#BsI6y1z26ATe z$^l5HEsb%#LoVwW>a zOG-*wB7^|cy4-WlU^XMHQmZeLUWWz&fMsh6>{ktZ&~UIwfQ*c+u-AF4q@)BNDmF%> z=KfdX84q{joJ~G^L`HcIu(rUYejtP6qYW)=D&iA;S0?ClEai>M$!5Mi9&xk@#epNGLv9eDlH=t4 zn1!BVl!nF)4V0FZQMTDjdd6Ne1HQx>b)0^`74jE;pW$G$@1-HrDftpf>+r>xVkHpj zed81z&*g%ly5es0*Paw!o)JR){L!a?^5RsNNe8Wl4q3997(cV7tEJKow3FVbco-~= z^taK4Z-U&1xoe8&rRbeEI2k#Bj&kTpESCE%0pRk{TvMu&O_XNv&_w8?8V(}?xd4}Q zbgh%vLjXFPTs_A1z7CPR$=O@;ikCLq1c`!d!rKG?Yc*|^X$J2p)@8;p;L{cw0FF8rs&?Cl&Kxqn8ZbSAvZ^cE*IL>Z|e0l)0;8VLJs zR~aaZ!+M<@y4^f~7e+7}0+*-N|NcdK0NudzMQNz@cdhfC#}}h9WFmW6>ch(&9UaBS z<&m*wJ4>QwULJ9zhy`vtLa1Gs)Obf%xjxd$Bk#r;Agzc;&hwnIlgAskwxE96S^SAa zo85n72el@8+bjjvAP&8Efs&w1Qi9hqek^qu;(LrHv#vFy~42xug(p^golieHE9T0Tbj|zPg2G~^1 z`c_ki8vRjbn&tC%jC&KkYBT4Xyp`O{dRV;`2o}%vH6f@cw!#%RYQDw4#k6Sru;!^o z$+%-7R$YFK*3I?hX+KX_H6X@Vz!&RxO)L}Ym_KH1*D_#q838w7^m=5NlCLY4YQ>hU z)fg>=W1-QrO6FcTT^ft0Z$$g${ff-jAv;ahA6)Yez0$&Z}b`Ma8^ zR`6k)lsJ!{uuHnl9#8)Ct5x2OZ=*VmvA@zt`^;kPf!nh%JFC9at)4r`_3^zs9<}H* zYooIOGc6UNqYj-}C33|lWIE)E&<@Q)YkBl$@o)0Rs4j{rx|IPF+BE6=Z0T&~q!l+S z-;@^lJ3odIw1%S8)125svYBbl8EW|_f0@okOmBD+z;d#(6FdAte<eVrqSgWEG!3pj6`JjF965~(51Q)cA0kImMrNfT;V6FBc!iJ=ZE)xcr5J#vU zxjYr*{Y8Q!!W!>IgvvOHjG-Z{=U0-u{|FUGsA8Vw}G5<){ ze`44+EFy_R~t}fG%tMseS zAoDvrC#BcW(o#cRpE6h=rC|sV6aX$)#3W^Yz1M;gU?r24!S+o}OG`K-^!$7b^h193 zLiNQ|f6;9c6@y=IaP{A=(3S8_= za*2mW8e46CWqU|)j+y!}u&~$>E|UlNfiVhKP@u9NGr#%9&q21Wt4KHMpu|>SUZSY> zlyD`PIIc5_WpQr5%YOvRa2iPoiB%C|0v(~MxMqLUP%6|L?=7({&U~@I6o?M#1`R)9 zO0f{q#0kBz_fh-#N8RFUdKAC?)vUFzuO#U7U^D$(uQVW?&6IosLre!f@**l$V1<0p zH`Jl36XwyYp$!^tSgScej%Gbp9t*t2k}1!5oyPiJf8YF+kHQ+j!j$=Wd2u2{9=^WM zgo?HPL472? zi}?hOXn|!&2n**P;sXG5Q5F9+7PP>UvgmKNWSWZfmFgK&-cNVrwnA?FKOUp?5C2d` zZn3CJT2@~%j3rV3WjSs+Wo(t&s zllD+dL1kT9PPv1sjExWXi6Zu(1@ci$F%M%9`~TZY_JluGdYdTUJNT++lkF{o)XKUF zovj^o67*{iyPT(6e}G1kzy@nM_5jJ^PthpQ?fh2pK|g4O(gPiM<+Nu+)Q~Nfv+DE* zEH=&4ie-6GXfJw81i-8DNfDoNCwWL337A}2N8It2ypH9)BawS|#89{If}PodE(H#J zN&kN?st+*!5`{h=CDVas=zq7!rzjjw?)rl8q+z9J0wK59lgESr_-j_)c)of>!2kns z3!y?S8^5`{cAFB!l78v>gCqqVAF-XRfJ3qAee(vVlxAz+>*7yLdT(`#xL=(fxcVmq zFTGIjrdS0Y`gaD`raNt7UB`*!<1Q2cyQv|&Mg9`LAqQV>>J@xyw^5sjE%E=+UN){G z18&fYX(g?Y_IzF8AZ}pYc3e8bXcn*!-vQCRr<}Va+Ri~|9@7z(J!uais)Do`B_`nZ zc3{Kf-xJD0g~uIh$mKQ#CALIq+;Vuv(+z>GK=4PSCxG2t9NMAm82-2p3ZdmJcpa0U=fFWA= zyKecOT<@NohJX9^O&8Eazz3Ofn1gUxSXg4_oD0Vtaha;$6O)D!B<^i* zFEa||v+hutt@;CsZ6=+yC%w?Y(R;n(O+ebb6Pmgo{bwoeL%Tk>0F-g;k2>{tM7|Lm z+&$})`v$#(pfSf9HV<@6mRvxdrCfOdgBsr4tbxbxyS<(HwhKz&GQoGw+PU63X*xcC z-ZOdN6TaV)r|5IvZP}*9e7&_%HhXkrgs->N;|d?*3L445!fM5YdZ<=-ExCN%bJS$8 zBOtBH8pEOzvG_Mm;1~Fo^)iN4FRn}e-G_V*w~R)=G<~ou*V*qQ;@=SD2(3c1h)19@ zE^UHCLNq2T3kyLLmVk+Mv-F7=V8Mt!5@jPejtsmaGD^eJxN2M^7rmvW!YlS%>HqCf zLPj^hPtpj^4G3!D&cgPUiHL|QAV6~jbkv=B80e-k!V!A%E$X1ym|kgmL`8cl2>6h= zl$l|MtF8)Xcskf4Ng_f95T}rkkg|{B>*eD=vz#+r!Dhp2K#4!Becd#c?t%AsF5hUz z!5D|DeigXmVE5uIKGpK@@MyK%3X+w+w{`kR;vNBXB&#rj)|y2*mmpmBJ@p69CLJ^d z-JtcNC(+T+=xWeGw}9@!BPePMG~C@HTvZWfB=^o4swJ=@oLt`5JKHtdSaKBXH!3{s zTd%)5K7PZ+Jf{vr;7eeNht==pg5WBHW=y;7ZaKTKkk8-cMr9q^js=HH^LO17p(;dN zKJW5GRxGw+by|mEL?4Kg@O2##(`hFQdp^1cn$!KA4+U*s!K_WIjojRTnCirmormP| zK>uSIpqX&iu%`d}1K<8x&`gnm9VwBZ~M#Lp0=fFMnr>_?*Bk`)K*v$$LJ2M`9 zYJ+)nFbOBrAfea{E@8nYbdL&ySf(yh3X7Wpsulr0M$3JD$8uXNHU zvm5j{5wV{_QhE{uWZ2{`F+Dbxb88blGnR#9nAgDBpmuj3U_ok@B^<~KT<8NJg>pfX z5J#ux61oZx%;wzT`E$POW`M@?Fsf|V%L!7}ps3X9o$t_qD zRS0s5mg#0*sfi=H*DslIP-S#<&gn&j!T8%Z-r)uW)QaLFeCPwnmlB`NvS`d#6!?aK zq)S6Xg=xobEiW%`;QG4Bp_2w#F*Z9(r_3&+cX#K}wAZKSZB$qoH@xlEZum776+iL5 zv!Hh#A$ITaeH<8qw{lR)jH%=RHih0O5BrbtZ1qI$@4tVBjIHi{O9R?Ho*Ra{K`>Takk~;>HY>tC$)cAQ9C+m zvFh#<(f#MZp19gp?7x-L)6>hXVDN!FnW~J+Mr~&28e~PkF!#;jh#zBPaYbS`w}x6) zRyGVFnq8b-Kwg#xGF$nvLGr^(jqsKYumVPG4#?{DbNi(VZ?BJhIxs{Tb|yyh0?%ev zPQOCf*+=3SYB;PSrWnx4Iaie}YK49QYr`6JEA#o@)TB{01nmpblj3~I4*mCz1-%)L zAh%uRV8tS*#5Vep^~va!gM*4`HUM=^9+4~w$Z`Gx0y?}S=l(W#@n$@NZ^G(1XHWj8k2J@vYkm1k=fFW`Bq= z>7qhmK)Pyb;a>0F*^g&XeMwH%2~xcb?ySob5*F47-hWUrcX3PSqLLndl`6iCQ@?~qKlP^Zje; zDY}e$v3dstQbrwne~?^&fUf^6XjuZXS&8wqAu%?EBNU}Sw*!7NT<-aJaav0vn9o7k z03zy`h<&Y0E6Iq@Re;XtP3Ej*B%A8Fv(L~11&r`Zds|6~^F7o`u-y1;Rqm?oNE&}6hFdq@Xsu=X74d9G@<1(JXGef;;n>8DEt@#HK#|_`t)YRPJhf7Uz z!U!K#4>0S71#g(0zXr%?C|c`Z4`s2Xd_+HQJhbQ(lRd#?hhX4O69AABK-IwvN{%uY zS%3iPJ$XDUh_}Z2O|f=9D-8|5;Jpwx0Q!cB!SR@c9Nx&x&&^5R>&|4!V;l3?y%Wv3 zD`aooTvKCkh)wD&BJz4}EZn@>jj;-sX1|>9?>Y{|_f^pm>rATTO;|5L3)y5m8`cfA zMpml_=TiZYXwXA5hkbVPQ=6EQi9uvhm^s?COA8h>gh>L5Fb`#86BJ-RF}@!iu9(#~=G~&*#5I z_ks9$u(TwbL9M3yrHC(wkp}(dj$xocsPU2D)5VQ{|K2Wvdmutx_EA%`Y5@Z+)NmSN`HBczBtI7FS;7pz5cMFa9gAmD#X&It0WM#d zNm3;<#KZr9t+&-&A<4>$wa+s06c8)H5P)WEUxc<*b^gTL@-9k8Kl(W6t2p|7 zV(?thIbR;OqR#A^V8b;uQ=Q%tyJ6dVq{}mWnbGQRHPH8Jq4VUV<}=UvU&&ZuF^2c0 zDn~qE91XBN?{~d^kk(z6+#U1m$l2=k;28!=m~Wzu5RP=&y}0n=Qobu*cnQgfwGr_A z0*zEH+f!1ZR{=tFCs}UAm^SixSLn|%{%RA#ls8uFwLI_3eFsqvqt8`O^U&O>Q3Wo` z4nR^i@qP3Dvl=lq^)K&q`%zwwaCMpnT$j+RmZqlb5;>^rOe^GFikdD!`98g2YF$*% zQk$8!@qh0fGQ#n!eF2UK#vl;08{tEl*Ieqzm?;MC4?Le6fJEepsUgM0{{>YEH&ZluEpP_r?;{A8)r);I%Hf#UJiypj64m0&&&{`X=}@18^Ec| z@^W)?2cYUU*@FO{i*de%+i@TY2vF}UiG9bkaG++8&mR^;WaY&4ERPgtls(I$RhfoD zCbab+3-(t>Op-K07>aV(UvZRG&>_yGRMFMixAsZuG;M_{4@~UuxDOTD1d#!@ESQ?K z=AyVIpU=-fpe?=AQ{xs_n+WO24!B>cxc^)&!e?f$P71q>TYC(`uf*T60N>2Ckbg zrf+)AqlykHwYQZIcCBj*@vcB&0r!Zjok&v3O05*PxeIM>1#G84IU z`mHo!IcCBU7}h)hq#v5PdT@9UTZqY3Y5ww_hR7gR7M@v7CodQ_nJ`PFesM9H)0D+2 z;EnY$Ym;UFjt)78@k%;H_0+_~H<{fEC{m<>IQV5K-S+Q<634QD{#Sn&n;IJB9?qNF z(D`p;lo;SnfmxYibSKb(w5qOn{~9_L^K9iJV5TEy03_W}SrG`iQrFx}5hmMO_)XeL z7h1g6Wdz27^bo&d`FHaSr68M`-@nJHhS~|v%3?fw_`}O7Ln})MWxJ^CL6tI|l+Ox> zR)Iv=!WYC(%%fqgZbZRJIcO+DtIkq7tfJAkHh+gn0!_<}c1VAsp{LTz6zM4gN8yPH zRcy*9y5tI0MD4w`eW@({zVSd5AjGtU_UWTDGhz;|uPEB;>n4%OhfWXcJ8ZnY8%U%6 z)R=APx?)ijW@q8dQw#%si8%#tSXwC z6L-T;>kZ;87vd10Xr;_SqlC8J#|(Fvszn3@tW07upOy?U1+C+>YWePgN^+iRQ`)Zn z)7&}sE7gn+1%JI>34yMPj_wP#WLl4AD!PRJL!daC>Y4dXxqhTsp_ zktBX@#F|QWp)O6rCl?nq@cre&;?Y<%{kQGJW`Kbdem+C8M8cZf(%hWJ|F~*vbjpg% zy(Ap-35lGp%*Rk4clY?8qjdofo_VqdKfWt`7D@c^?wOYE_NSUDJyu)Ez1p81t&Q;@ zA7M=vasb16|Fn{o8O(RM*W(zy#x97qv0vq=*Gy=M*!ZqzjfLqSkB4Bm2owvNsbI?1x z`|rmU7;T^cy+-mbr;%S`9QG!zmdJ)Uc7?+? zMYA3=qV(8365x?-&9;HNVa7A58ccT~<{+YfGLpI}8}Vfrl|DLzpG&!V~3s4bGX`(ZuUpiMehg#sdKLg4u-9lm6l4uWZEnvHyw7jw^cC_ zY`7sXs$p&hZze3L4p88YHYrbX>}l4S!L%D-!^00m85E0R?%ob$(Aqx%!$5j=oMl%c zs8R6B78tFl?3}H9d;0CEGkBiVS?~TmQnj)oeL}Hm##lCyCJ^c?`qE=UQ&((-6a?_c z*kyJ@gNc;1^t&DOC(GPwE9?5=}L^vGSfbV7U8WX0|&F zb{c2BFSl@Yi;<&tq1FEA^MThd-dZ`s)UjOF=d(tHJ?Ky(^6{ZHaQQo-KgkRX5$}mFhR0js~pndEcW!4h-a*

    n8JIl%I8uqEmPu#hlh`3NH8!UH!x&`YV}8cB~@w$y^H^ z3^fPGz6XZ1N@-o|^aex&14|NwPUTj=;IFAEPBe~3jbqIH(t!=SatJ^{KEH|!|$A6a=}$Iair z_mG4%{Sm+p$y@hn(?T&Mu!;KG(8`;(`BU1;2P`_qMDj2-NK?9tB&i;^fZ5EX`l>1x z(z_mJ99V|P>!d3i=xi*#U(8oe2D{{}eDc30LkplzB?3~2NDtvGOvY&pd*5i% ziW*y?^$Z*(MOmyL;jpoPiO6}G!KiL3`x6UkSY&Eew*skqD*r=*V2Fn6_gKIqr|ml8 zF(WfDE(pX4%O%V3c>5L;fLOXlmteSSfWRY-$nAv;PPsRPN9y1 zR8?E|_V=N?r9?`)TTog+x4x)7-v9XS z7-w80&faJ36?4t`Jij$xbOoZ8{zKkoWt#fLn>d;&#mrik?<^j;5ZTG_g--DwAODm= zdQ%?9B&eyzCw6?MHxe!=C`eEnIo#OL9*{Vyv~gOZIJ%aM2G2n@#Z0xLBtG z!QKyW!Z9tKB39=3NNF6Eb(|uo8{$INNSc2#cuQ5Tn4v`Jk@*6cd2KzXHVMKh8`DbAzyhg zHnLIyK0XE<$8jMM5&19rhFI65xwdl|z25|$B;b%cpcEr6jAhGh&SJvZS@sQBD=^9V zwbZ^wL}adCKeqpw^yCRIQd0w;_De>L?!Ui(UqR}6z~BZ#^6q22jskz7;5YS+iN%Xb zdmyqIj0!}WLLS>9QSJWl&=G_{x`PB6fT8EIIm54rg|2oS{RNI3#?3UT%cTj&-mvXI zhox}7=Q7#;qVbeeFY#18q-X>$9x$iARi$x-D-^b8OI)Bhlu0Xli=d0_l-nU;iGE{X z4yF?p0WA^e)i<`ruN{(O65bfY0>hMt!1c=0zTEj&*JfVztS_buTpYMrLn7SAb3RY& zR9l>n;lX~S7vA69-=yN(TU==jCu{KH>oL}7u@HSi zyVEWX0;L*;P{UrL;xv?PrsKa08!_CT1J#o(Ula#UFs4N&vLg~B2$LuIa53!45}ub&CpPwuCUijeH8Sagp=(!n;? zdBx_6`|XF`2y3+b+PN7$1A}Pz5a=aa8ygQ)o$%!b{)-dAR*Wo=2ok1ybC;6+$n%YP z6_K2r93&_a#M&{2`o^>?YqD~>SXpW5Elx#!2BaSzuxC9Mjo^1R)Q!8 z&DZ5?TwRj)XE7kV99130VtfX{8#SP2b_Cd^^1#yEJCd%83h}-pcjLwwqh6`Im^RIg zuFMSg>Tj+x$RQZApUJ+&K_Vrg^mRA?hW#MMxI9tfbwI-{UWW=g8G~};iZm6O&y4nF zWQZ?VN}-d&$8-4guU}xS8T@GGetZ_2R^K!~qu)DkK7x)?(|?Z#201Zm%7k6_s;XaV zc6!s@fF~?p#s?Bc@{I0SiNBh&O9UgKqp6Ku(R0-LHka{szs^F(T>(`(yGUay6=65} zc44JJ8=gs^zN;> z<>jmd%K6|)2u2Mfo2MixJTyB{pen|v=vAtRSs(=I;rlW3`Sn;7h1oUUWQe+tz`1>C z#ZagL&$Aw)QwO!tStz%~Cx{Bi{PJ&v{$zM(kDQ1Zk}~Dxu3QA%=<;&UXM{q;V3Ku7 zf)3B#oh{X7I{N1_cmJJ=J2l%z;_=^&P`1W?(H1jIsKZgN(vXfN(^f)cC^6Y_xI1Aj6P;uHC?3IR~dzFEiX4F`^9AN zn&w_Eh7^G(YE)or3AOF$o;t>&CrU@Sh^BQxj3`+Jn>SlweUJeKjw+_z=+46 za0;xjhxTHtT%t(0q{`fKZ{E36J=;Np^^|Qy%AJ{k!2nW<4RX9+EyRQ8iY59l-{yuw z{Qs>95swS8AM_)esua>_+;VCG4cg6vSZJ`pe6i zGj%J0N{`Z^kR%E5eF`B0Is5;&TSL6lylxZY6++BZaLrvFaw{1VHsMlkn7Ji z_JZD32gKC)j-<}B4-TY5vohOJnkDSi=bovUat;28*l33 zehXkZXiyY276%-RJUr&D-{E@DP?pVbjH7Mwg`tE9q1nA&FQjfa{aD!eK<_epxPvdI zZs4SYhuwj?U*2nnvJ#sr`qe8gNewbYysZ+(6;~9YK#z?Y!Q$Q!-km#sErmJhtONrK zM}2ETTaxg%fDKy2^&FjnH$3@sV5#d+^GbTfm>Z#1*ml*mG#Zn`-k7k+5)6V`;o@P-h<34uE96R7!3^yXzc|89Y(eM3t zt!gtvvjhtse(;5H;R@oOpMr<>FEKQDn=1cIgmX5=o>RYp;Pz!syjF>}08}rX#@7a% zY4$gHj8)xlGQ+{Ya}HK(o;>9Ln?YwN=RrC1Ad^$+W#{hR-r{^+b>DZZD9rH|@dL$y z9qz7S(iz^Mb%GJ(26vienNe;o@_GCc(g~Xl!z$yk%a8w8B3bYvR#7|CY3EP=_cr!* z^YiDIojp7RacVum_W2ba;ok19(R^PPm`_(>(0g6!6<(QchPoqv%HG^J!p)=I-M3#S zCb-2zN*!Ps=etB%d3p5})@#u`d~~XXQ}*=l*8>9TG49?&)OO}D?7iIfJ7VNsut#r4)QQSTBj+z#hZP*1Og>&go|A3k zL24=_gosfdcDZ5JAOY0QXT#zp_lM0Adq4UfyS6Hl&D}uuh;{8(MH07T%J#WxDkDd8 zB3A(QQ8kvLeBx4z1hjvaf{e@yR58R=A-=w;tN;ETgvI9N<~GSFmOvKfI~vo|)5DIM zDEkmuzx96hhN93VN718Mt(t6ilzSCn40{Hel&r`%#b zROp`WPApzO@xH@0zW+oiGjmfr+vW-$_iX#Qyo_Q<`OE4$>1!WGBzqmFeN9RC#~zq< z*}-Pd-~DfQqP}J6kp#bP3pQGVoI=Ms*yT{m@0~Qg!~JHSmNU1%9P7I&-Ft@RS{E&hk~Ew-}Lds+Sq}d zi`T!(u3))a<1cLuj@_#a%LMKvWSl041%m;ohf27;s4G+tgQim+vU?{l453#Gn+C=nqxbT~X@3ei2Q#Kje z-`mSrBefXwXQ0!S95g;5T^^|+wzJ{VB3Q`eZ|3H-H0lPhu#F(O zRNVGvkl=!whKg^UWf$~TR{ z`*ii484wEI!HOj!vn#+yOH#=?Iu;b0ZvHpN2!@(5SbayZE5N|c&VY&kbO?RqhKx*( z_^3&yNcU<@Z30Yt%^e+)cnB>+Y-bk?C5Gx{MY%qfem70j(-=l?u9oF^n-hN5sbTG` z(F0-ME#?NX@Z(Uz1#9btgauA?95wZZQc35#rKd?rV>);uQMdYcg11PJ`$o~roap$w zZ5=Sh9xW{VCq#7ASPKsI(^0UFdv6K}H#?8hgzN@b9vFUE7zOhrHIK6ReXzjT+IDVr z!mteZy|S{x(%V+y$=zZRO63k=gDkXWchKl?pi zksTe04XcGLb#qe_QuSd9+H9y?0sqhU*iYRqzg`y$D{9hB=8SjjKwtUX*@*_llJFH4 zmQX?bVkES1P$8N+xWoS`!-T?9g=B^6*SF30xktpbWwFu6%r>am`cLMEUvWCu zM|Un#1<{*{H8WNK0SF5Vlf_QT_heL(5A^rvAR!@H5G2bkN^lNhK$x_e6#Ym4sP<12 z*IiOyoNLp&m|_W>zGcA@;QGG8#DsW!as|Q+{4^pc zsA(ayk$(Qd9Y5J0g?zZKpE*^@-?;q(h9U=1!jE6S+DMQQ$haVi=a3q3Q#{w0pgDk= zIW*@TtDVio`g(Ce+_RVXiu>0@25sr(WgbK6>c6Nc)Z+iS{Zp25)8jtoi0|W!N(UB=8wBpl1n-`XHaNJ1c+x7@n=FemWK|v0Pj}MoLrU)_essgLmbs z&V%nNN&k3Ak;|~K#V17Ei^|8v=`4V(kGXfsXx;xz_q}CDb2Vi!W@QOmdqqRuLOE$& zwj|%(tfh&prr{1=NvH3B_rQ+#rVY&<+XBLL}~$w#g%fSOnUfCq6ZUYgjQ7TcH~ z+ykMo6X;r zFW1HFkzQZ}96#%Uu@n*%^ct1VD|||x0F}mk8jc^m7;t9y{qcixY%v(4y;BHl>Hyw& zVduBpHimi#Gq5b+hsr)!OiWC&-)!H;jU0Pe1;;vmPMt~D4vBh=u$GmVUpbGo*gFDs zC6%nyQh!K>)o~)Fh!2TNq=Kxh02u0BiHh2}(V5Mx&m1D5bE?CqnEgn&$w^kHhRq2d z+`h;ZpLh9N^jD(Bs<8oZA)sdca*LjmTx*=|pmVA-=^SuK_x}2Cv606saf@BXjwvljokYL*L z?q^PlZxgU_svp@cZNMBR`7P)218mk%aOf)gQ{9=s9ugd#x+V6*3c8yR3m_+S-^KJD zD!|&Lv0$c^E_EA%dGhYVv@UZ>`tfO}`;Md}Bmzv#%-0>TdsRv^WM~&d^KpHx53Ps+ zK});Bbp9@!IPGbbC<$%v(V+)huk8)MC7GB-E?64vhJYje1qhE23dq^t76^*)_ST20 z01#`=K?&UNhmYJ~1SAGI8JRQ68PqAT0Vg3RUq&Gip!`xhKfxPhllIQMFT@9WdRJ!X z0Bi6p^QruMVf3OWt#dzIr7o5T%3dDQWUs;JA#$ywDjE$MA{ME) zO5ei2=7$fl0{|USc+!^X$@i@7+F-CaFaZE3^F{heI*864VslQ(l+Nj7rF5P~6R0m{ znoub_`@qL!>#tOI`@!Ig6%s-tWv)Jsm^HbxF~0AM&A zi>~#8dPLm7(vH6L9{aU(Z~kiM%jY4Io}%4ef<;BD&wYq|8tq8tPjDW;YKL~{2mZrj z`%f-zrb+~tpz~_d%3=4vb+8Ax#6ZhQsXvP65!_r|-hn zQDe}e8X<_P!8nsULdpCP)tc^<0wpI{PGGaEB53y#K-9m-MeRRubw1E1Jf~~goSX}J zfASjH%=#w3hj_+qey@L;$srh;BTY8`fJ~j0L9Iyjozi5<6E29`KYT#F8!?fcY>?8m zeAI1bm1N(GwQm=>;8IQk-DpWRlTwH3{N09uXFWTpZ`B{zdf?5HA#TCp&=5*PDdoSt zBG&>0{$QR5P0(QZAT%$qz4J@k98-M;>;#GPIP+e>DncgQ$2;?OD zdV=x(Iw%1J%YL$*t z$ozb1QzShBO1^^ULQOn2g&F96=)D7Qb&187+dkp4 z(syxwtRrf6W%$Mu(MtTYxo7F`4kMmcmMNpj{6;@2>Z$sWa>l0m`i`{lcrEj9+mK`B z&}Zo)`b4Y-`|7UnG33O=45e}h#SnY65RWYbi7pP&uXmNhT=g;qW0jgo99>euV94L88^(XtJ=sQpOC)7c-S7l5?tsqY7$PJGsENxy z-D65q^9z}T1{@ep%&&Ow*SsokLHW!Pi* zn1LyP&?f^Xm##-C=Hu)$1clGW+iXPHKPhs#Rl6^q7A z{vJy;>B$93=CfghD+5Ar2ahTU_4#TL-CJhl_zSzO_s8q1@}^G)zi*mYlrnyIJUqGm zm;%*hj8cR=V;^+x3Y=M!KOtDo|3kCH*^5X0J|`wwRAWTso4>PE4*wNE?4j>K#?isv zz8L3k?h_(j>Z_~hc^SIFzo#SYh`_~r+}-5kqv)x&E?f79X#LPE3N&vJ+US6mxK&PeBtpFkcK_%%N`Vy#H z$d=r5H$TZAeOCPBu@OXCjb1Z&OH9E#OHWz>OW|WsU{+)LvjjGTH>O`9wgDgr6qlEO zb1aajf_n~|%q-J^7}zD(#bZI8?h?XU@ec*n!ROfMRMC?UZ*oL%rA^uY{jPW?4~4`* zS9b*mf)sbcCr-RN-EOlTfBzMkF_gkh2qT*@ceS+*^$BNuH%%X!B*xKeVJm`K-h{0i zQfT&ZxItK79q#C(nF6&^>4K)4F`A?X%z zW`-Rput0g~@ZZAQl7%+2rDV-+&x7|0RIW)2AQ?d+L=nR#n>Ii6Sr=LAkg30RBMYO3 zbQcm?kT>^1!1j#8Zc<>rxhinslOH}iA}b@E5TR)qOy9A@dWS?Q+)Pma=K3B$vkyvR z0!lmW9HYlOgZln^T9=9$9zwq!e?i*$59+O{iC?hLkYWsObg%?^s3P40!((dOV0ZUQ zoZow-59x0p{XPNF76uQuK4_eD5^IZVh>-&k*%!uyD%}I#TSb9}8p|G2)4u><0wCR! zpoq#37^sFabvLQ^^@LF%?{A-lB87{QK2A>DbSm?#iM4B+?znbLJcrezpbB(I^m!~h z=Bdr+60#8BQ3?)7g~;<=Cc*>YDuA3>^Y3D4Qq!;b2J=7F=2du*|CpMb%(;q=QD1NX zKUC#6R~KKMXB&_Dp+^G`)`BC{{*7@3;li_ysiz+P{{Hv<{Uu)hf%~~)u~{Hy@$oao z*Bhv%-@)ds+MUiI&TwV8e0E7yzH{-~PqFK24A2|U0A_<#>$&e~~GJ>St7 zF(kn|7;w+uR=fKAo201x#I8m}q(|{rxPu?c%#ZNS0Txm$Xm8b5 z@$d%QzB-v*Yb`vn3n2Y8fMsGNx&8!0c%wx>PdfM^&;EVtmb8MB_tQ1?4dG{@buEu; z!w)LGen4IES(q{0#{|5VFFF){n*+4RQeli3i-K(CSu*JdV;L%=x8*Dc0AR{DjQp~D zoRgB0(g&|acy0Ig_KJ0zzN7ER5YYJpQ`yV63LX#CH8r}Xrrxj=lE>c$=*WJ4K!HPJ zPEEvtm*FA?CQoKN0o=p$#TtGE*64hBi5dKG7VJS~?Ew}nSU+i~^XcST3oPYSR(gDN z+Jkr0eoRdnX{5K#77@DthUv2w7X!l4F(GzLR!?_#acsY+229tHh?UH%x&8fjVgQfZ zNY2U4H3I9!UAh3_tI=Yh<0lqO^#aPr`PO_-BB_4mmqZspZyg`)lwR?=y=ltFGw<`7 zpX_PhA}|}&QI1>c(0JxKxkmu{@1!4I@3-W^`K-CB=%3O%wgKAWdywZ=)N_EhxR*j2 zMG<1WW5bXC;v}?x<6n#13z*ZLM->&P^~Tv>-o!(=7IW2$-`LKD9cp%$ZES4JQY>PM zb)=NZ5Gj=Hhet{+6!*+1q2oH;b$54<60P>#q^GeBl&f&NmxThNTGhWCj?sLJ2=-aE zTeq%Uu6!K)B<}b5H-W-v=@%*wWs9&d&DEbQ!d93;WKn--P1eAV@SpTG5Sy9;fU4`^Qj;+ukE z^-9-*YR_u2=5dq;&HYK_{<=dRTGU!J{m19Dfb_bco=7tX+q6!~58BM;eUX1iU_}>t z-=}h~b|UtT+kthvZ!VE`9s+M+oP%sb*c0VPIE53uq#GL8BlSyM%}M@2$m=*+_#4PO5+^Lp5jnIg)b-e@fJE8uM{NI;53ii_NM~=ML;| zz-J;l_&6AFEQz8**};dR-7g+sT13n2b)Km{qyaVn_wDOVL}UgOt5BCHikT)@g5rfZfO2c=LtSzQ%6HX z$+uHIv$Jqm3q4@k$47uG^{NeXq0YJvOy9eS1CHxycZBhwq|*|bCE9%icC|iuJp$yh zs@4vP#(7p>>qS=u>_t5nrGGi`LnDr%7b3$G!{)Cvz68V-UiEW{sKCeUg>uIdG!?7l zx#=tn?w7>L7JczBjlw|ZCZ^3%S%kL~>k6ezWo9!cPpJyXM9uOYV<6Gua1)PNB^g!o zT6jVV)BhZQxwrS50r{Q$8S)vD)K2HguiZ+I*Uc9im?nZxPmjb)BvVUf z8!u2XNeQJ9Zf?XpvU?XIm(!y(7x8JZ4{E{5B-FnnM^zxie^pv~wc_{5`B~^5Ox@ki z$z6~o?<;x8U(FML#mf)Qwh;k_X9KO2lvM6cm6+m;bR=uJM4P)kCg~^q)6*Lg27ppq5JV< zma?+%_fB{B<5@APw3eHXc}yy*o?T=mG*N`tkijWjv1(AmAvxRN4(k~KNLoit4PA$c>CdZ zYAp`G5B#?PPDVH04qj?*;|PH7?9X<*!HK`p2hXXm$K`Y(S=Zp}E%N^eMdak#Ho;fG zw{ZRib2fu1GuW|*vd4wbTbyy_=)%FCnspLelntj`)XaPQg@Zjd0dDkD5+-tTZo5H0 zar5eJC7P~}Nh}vcSe$I2T3|?!FBj%yHXQnS1rV2K^?u*${;>@=MO4`hOraFf^)%br z_r-tY5;_#zrs0eZPWoJYG38j^!|KZ&8|j?3D+&}}PtJPog%LIJoV;WhLntm4X2ZhT z52D?4`Ylam0zuc~pX1}1>gaJxWa7V%sLh?_VK~et%WohFbqZfgiigj_R3lzly>&ec zwr$&Rz`y`=6ir4ibxsDevi&oLjOo_k+pmIzYpXFf!(`|uPe?6e!tRH9o{@45ImZhi zGR0~u8ZEDIl1M%7LSJ?N{k5dxLgF(9 z`$FVWk-{8lN$ePBK1Kq8?){~~=ch|cuWY!{7Y>VFJ}&?J_Z>~s|6aJ)bN0xBN6FB@ zRJ?!6s-41lp zlJ>p8b(H<=86U2Ul=vnF0)gNzOn5zINbQqA_YM5`%EPvPj5LxFG3Fk|(?!cBe!}7KfO^JFU zqoCp1>OTYhD&yN9S5UAD(b!_EJBoI*cZpy6;$)lRo0zZ=!Wa?T$TR$8@{=qywHJ&#~Tv5F|`gCTq-GL<1{iwvT?^DTJ=J(NL(siSFRX>WP|F6 zd()Y9Z42OdUpKhbT>O-_Zh>jLla=pBynNVUAzpB#ZC1IUZ6cV9*sJhwyqWEoCM@LX z7eGc-#DPfk4TJ`t7Z!?%aywBRCZLLxccbJrS0m7nQpghNX3VuY19M={5drMwZ`kh^>Z zmMHstlxG$zJKjM7~qwU^~kHL#>m^?}aaL!Wgs zzNYXjJ&x{vYT}M6c02I?g77%5tBTqtiW6?yy15y!nbGrgF>0YY`F`VbdkgqEbX&ND zgk0Z&SmoOr-Yvj(bW>=&1kVjDepxb3Iukx>G^)i>m}j4cy!ZRZd1}O6iY?qz= zE5r|zF}!o%cN$8s4AnrQ+I6<$KAxe<9{+KD%xOgGoLPH794rc>%2{?1q!qm*kk1%X z`FL#hQ)#n*wXukYmWO?chBhS}8f6u_h$O(J2kSWuXox*ua#0uE%%WyevUxXxJ9j?n zPsZ7KjET%iM@JRbBLe%`E4x8W~?UXiKUd*Rg5L}5NO+$E_8k6uxmMS5k@k%*WJUs(k=cXB62(>a0L>LyfpUvh&ugz4*ct)|NnQl3loJ&N9TeL0sfeas2B>e=~ncm zQ-sJHF-wVQAs_@PgdHXaZvA}l#!_<$^8dc3tX8=wmHrd@+dU3QP`~vNVBIKBlUi*c zj!=+|!jLRP64`CI=~3r!=h!C?0*T#RMrFWLYdEgxHC)6|{fYj(9A{(Q#AEKA;@n63 zo`zhr{1t9Z*t1)qlNzNC0i?wEXxiUPg^ezuZhzoSWv+#8!2h7d7#~?CgJ*mvz*5;s z>V+;^tncs<1hQtO%N3&d=bVXKpLcg9_S(&CY!1E@39mtCKpVh_4Xa{tdw zAHkR4{NhjV?09*a)ceMWu<(W!|399CYWJNJ_DO8BI`U{_Z*Ldc;LCqz4MfE~skhEJ zzHU+fVn2gF7f7M*V)f}NE8{H^wa$x8#4o^NARQ(Vyl%F%P7q zBePysH%o?XS#b67(84Y2a)JDj2Y;{BeOJFESVe8u3W%>e$ZO(R>Yu^uGq5Rpe#%us zsC-SgjR}eEKInOW{k^(7y>SM!zVbJw$ig`7hG3cLK{0H~495r{VaQy1AeF-(5 zAa`E(5i|s6yszSMxG^4DCoOiF25H4bzg40|-~18wV%k1};vIIW;&2UMVM?*D<;rh^ zRj59-0Th|w4(ExihNBO6+a=CQ$$D&p0QFb*SJp=3*{RP}40hpS(=A=2`Bg&cn#l8 zbZSkn)nX)@EyY)4K-x_aH_prGA(BEqlwuTORbW<3k&37B<3YX!1jA0Qu9^>y*E}{! zPGOZu6(rLL27H{|GLEf;l;qt8i96RCKA~(;2rUF$e#Jr-&py1zPP@KfvwpVZ?kSnRPHlUN?^Lc0)$m;~b zQZNAL$4SHQ|3^_XGGT7aJAU_Wc=#F1y#xXp(pf{2{aVcprZO@I`3XrXl?9T4=F>@& zc{<2W!;v4H{k}9Gp$=LO=ai9k%FmsNe7ua2Ma8BW*7a(hV@Q)&8!3t`wWEJ&?5GG~ar+)@dHu(#QkzMWGITJhl zW4In2EZ+aa&}A>&{88B&cj>TtzRE#{^06I1rvPcw!q!MSkT?sXdYpBPl})kGInY#2 z(uaBhwi>fjID$69tsFbvnw(Y~VZYP0C#?^*{ajsL@4LDZQJyJ%E7D7Ji0sXwl{xep zuTZ=^0T7b7-T1o(SSx+Dxey)nnF(Ij3QO_96?CI{E{|x^cY1%`S1x3 zJpc6UB+xGB71f_f4b9f{n|y-eH!A|ajvfD|(Sj&Y;{adYyg2 zl;?ma*AB$}l0k{(DWFhwvgA8;nG2S>JYZ7#H60>&)tvJZ?g_ufoHT333IaA0Ql2=< zJppQ4I6p{~@wxhKTGZUUQeIAezA^phpLMdy`HhbQ@eBEP#O34$UzW9Q$vaX0YRr9$ zo#@-{D5isp1_+-7K89^{G=#ldT6CM5O=7-|^2KC`)T{h{BstEU88D86*&m!`gxDX( z(`q*4!5Mm9?*jXp>s(_sg|zkbgkwY^E7EN*LIjqNJM%?a-`5BU$AaCL_o#NEz|dau zTeM$zFh~~U(z*HhLKC$^OtI&mdG+&@$vsEHG@(TG3W+T*2SyDx8^_30X>)V%_@cEb zVfk+dMWbh+&Rd*M>N!l~!OW{)z!^m^7oGy2F_AbY?d50Vm{|6%o%>O~8Y~#CQaFfO z{HJQva$BDv*Yzx@E&`HB=H5^7w={?fn}8%@u_v7)(H#Vl5cL6JcS&9%eLiJ2Z)mOBiKTtT=Tf?hYbhUOL|%EWraPs)v2|K zdXNQYYn&kpdv7D8yR@>vV7D3-j3i$Gf403o{3nylz|8#4a^Rb50E$gQZw$Y%xw(0w zN?gDNMWKez#}es3uUCGWRQ80If>(>YzMfB}F1X+ToQt`;;lmN2a~*-6l>x zN?94s&8{p1HxdiMiw;WVIw z&!p{h(wy4zv5(J;%Rm=8#Gsp-U2w_oFtFce2+nnf^TD(8EqIHrdeRTQA?gmowaZ1J$ujRCvC%DCd2p}p4|${Jaz{i`{{uE=RcO?MapZNrD4+zUY7vM-aL zeT1GHCy6_MnQf`hC)$V8IJweY{t5zM`T;g_JR!Xs)OrY`M`%I8cJok_@*X6RHQzA}2q z>+9)L-Z;w>A27UO7bRTDP?MZKaK@NT-I;I(T4Ljyvx`*30y49rhEazN=2gyqNx7GC zxv%uR4*K#)p41&t;a4+=iUrbY_*V^TASaE(OXO(bp8PcKFVjK!3Yr7cfTo@YGe0g+ ze`cQoUDB3|Nhj=qrnYogzCcqi*J$5pBON2{L$sJ{F$+iA^ZM4rVCUpo&*h{`{SbKE zQ7!%b&Z)q>(eMd&%$`rD_gg(UB$~}Zbe!hs{noV$^}90DMi0f2*{)2-H;V42g|EBW zbIdY)dZgXb`d-ON1V1Fj-c=@LaheN+nXuloT(<%p3R9fEk7k}(7rij06G4&+8Jp`b zc2)OJTIJ;YEgc;;@ak#NtJDesX=pWlx7&W-k5hTAt&>YE^ZaXnZ^Mbpy4J)9a)wd&-lne3iCQ)!z>ryl)|ep6oHvB7mClX1K> zuJ*fsE-{VwY)(l&cjH&nUIu-KYJ21Hp!xTGyRWCa51Uow-a*BisXmy|Fv;%a6#bXW zwCJk44*QfSsyq32yPTf&#k2Gfaa)u;*#G4oZB;$c^cM`K_elRj!wk#(`2{tV_(@q% zEk(muL|xMdG>;9={Sv0R

    oy##kx3y1US_=%0*l-Baz#W%QE}Q-v1a7p?y?Z=C6QW6{BN4ISSV z>(x{O(k_e4M?riP^Tcchg`CTX4QZkm)%X?|WHQN6Y1Up(n}+s! zr92|Zn(&!Pn2W3bDG ztX$^3H?fW=Tf)j4TY@p$DN4txfwzvA>Xa(@@(^K zoF@Hz7%*yx5bw}Xe%9!CMDb;@1MuYrn7cC{^$h(HaDE46{C`S93RfXf)!FwQlm>Vg zlvPvJmVWyA3yN63iSvrn9RN7Jm#qfFHWmWVO||o(FgX3ZiU46(Ua9Xhy8|FzPBA>l zV)WIXjrnO`kfykty0ab^IwIC8ZfA8H69A-JS`t;C8?} z6RM7gM88h@Tklw?Utw1%OtFX+vAyNn# z%YnoLA6mBmEHORwbnr%+mE*!lSn*qnhlOHD*k1jNMyAow$B16IUVmax2D{UHGm4jA zp*Tg(phcLtdy;@YG2V%olk9F%9DhG+JLYiuSMu7v9bA%1QsHdY2K998z5sL<8HDHh zrb$hGy@k2Ck$s}97(qCK4uf`~NvU&?>gxgl9d6zzNAjb*0Mn^T4ai_YU5%J+M}(Un%-+nQ;hyrcUehmgS4$VqtKpl%!+Pl{6_D#l_8tg zx=rzr99%D}zXM6kGf~Rjm}^Kne9F0xCp5}D#7HDn&ef*b3!bjh6Ln!fhjkcer5;Q~ z7yL?Xqx)$LK^6EuFY8vFwIp3INfgSuGdG($c^q+>|1-U%xqCc1Xa~{RTKAR7{OARl zm)HE49bc4`IOK$ZAw`k<12SGMGpUdF)1$VeL^WwX`sMc8lP6{9MgvYa$ueu>{SP6!H$bgZ`|7os5ub&}Vh*~>rGdAAgBt8^;fu-18 zJ*2*C%1t4w&s}v%=r^@qVaxKLKZ1j&F8+s!8DtFUXY`kbJ`qI{I_JK#gBA~{Yc9#m z%xwJ>00p+>hZ9*jCcO^}-o6eMsjDE2*_Wa(;yV$eJms z-XM?VVxjvO+Oc{Dc@_4G6-7F;=!-igd8vL)FDIHpdnDb<2|wD>$>szD9cE!y?mupX zbL+$9y_yK7T)#BgnkQ>vA?C|a`|ChGrMHuly1MSrz7_H;bnWkZHA>dhE#L0R6$~F? zIt|HoE3iyuD^A)BCNg@x{AdsSt1uR}_9Y(!G`)G>|ZYoHVv?M}ICYjLZaa2#lw zyuwn%u;jhDt0Fws`&@-<+Ls3hpRjPF%j90yvm|g0NptPL{Mj7iKnEJXrL)$Th56xr ze)9z9>yiJw+w5sJh;9Eaf`j)<|2j1~l@sZF+;uNvN#<$Li^J&!p`bd&ME*v+W*iP| zge`q%^F+17IxxsAh1w;0q+E|e705+7(cYKZCAz=!Y1uO%v;eVV$A1L?DmqJ*drc)> za8cZX`3ib+t!VuhQ0o-i37 z2d7;y=&rXLD-X5WX~H4!X-G{b&F0K3PWB62aZfL=hA(2x6jyBFkz|RmkHHSFL3@Ph z)6ac8gC9Jz;sU^OG=7}?T(yO#_EuX2sI)?{uM@iu?2p(hr26U@2XqZUf6O6Zn)0l# zy=S(z3ucwRS8nd^D!Mbba1)Pl$yp;%Q~|XSOjy+H!(Th;6E;^4Gboi^bFYAOuDPnp zqZ`ZH((hr6v<&1tO&Cbb?CIw(>Sf(k=U#qGL(bRZf44Y&$OAFSvE*LyL>hrRe-CcM zc_h@qZ~4=Y;6@9oQEDq4uQ(72t6ce4P^YvZ_WPHuj=5rjg`7b`Sm4X|c8SULaW?Mb4vax!vDqr1#i615BZ(r;~%SG#?W9~gSmLdq_ruO z=?{9Gm26jo_b61|Om4J|{P@x0zK=zr9Bj9Wi201hP@Z%V5M6+a0P!p!CBvx~Z`~RC zm`JNwQ_zJ>#w*d310Rai=OBpFFPn!PYmr$n0o9*FI@u73JAzo21^3q;E{i)VGd#d2 zy{*zqe^B67vR)#M0NVx!Q8)YQDFk96L5t5_+SA}kvOyH#ftlR=ZULNXBW*<;m<{i8 zId0#^MVkfX=QFt-1qTGcfnvry;tuv=!lMZfsPOS+5B(5zgZ!$9VFzxJd0>^sP#n|v zWEN}NoAdIVZJnRz2XkoxFj@2a5$@)SqVY!@BK1z}9dQnVb2lYMh|fr7f2h7^ix#_^ zrO&~8Y#l-|2wAiAIdgCj;#Ibjn_>Q|?8J^WL96_#WWir%LKMABKO}b7`6D&6hipK*#5@{4?X@T%&6k zTWb2kK(*x9XRU0LLBm5*K93dl?uCAX!&7{ z++#NX?|oKh9_vP}v^3&>NLod$$Qi2rv_B%T78~u_O474RciNbd00ZQ*<3EDYkbu5! zUQlBV;#JyKB_F>q-X)=LxM2h>pU8Zt`tkjCz%KcQW{YA~Kaiq#aRK300P15Bp1Cr? zpaC;0?zqbdypx2R`UiRlHV4+Mx12DV`b&>J@J-{msf%RVVf0B!~8$9S3hscB4#w=N6j!ZgN0?9zU-7El!HJ~ z7i87LJw^sQ_0wOARWa+st*|^;6UPK(kDkwhHG_Si4r0E*`%(PXeRm*P*XB}TBVO1! zOngKyb@f4OOG^=6!jmS)?-x_;EyV%2*dufgFScSg zQK0XZyv|I8lIH{P6b{`kbh=P~v9T`pug;zec76EWY3o2w3mnI+D)jq8^eC_@s|^^G zMk_P7v45*z1r;|no zUeAV5$@Mg^&+m)v36B1m`_; zzrLv6B~S>_kv0|X#h0xyp2XqhwcZLOTbtSQ1uq)vqaSh@5dULMK`B!gB%kj#aM7ao z#f~>RTlK|7BsK}`T7T~yC^?Col+{;z1v&4@MPH77@DNLm^*Xxx&3KXqgKoJYA!_Zr z+AjmnI=NoI>+}DQr?Y^n>iOF6xpawiOLwON($XE$(w)*B7nKI-PH72gPy_)9NhwJs zBn3pe<9@^Mzt-nkF7a^hoO5RO)Smr5yTzd@p4!3x1CT^<*rAkHJCk(OtZ!sjRdIw> z>+CIl2eFa^j{~PG5LE}6(sYEMB{uky4QY7GvVYkn1)a~=;o5-^7M0ZReFPH;Kcdbj z0U(2Po^s_IrumwiCG1Wh!&pxkPIJ9DEDlsY8=}ZkosnN#XcoVV(+?T^R>tXfhGDF$b73kDr2BpJ3pq@(bQ{55SDYzwHVFJU*iF#%d~W8$T}$$gs0092y1DsYBca z4!S#v%Mker{=pNSAIBg7baG>(bP=Yx^Y?GAH(6v2av{L1hZDPyqFWNre^U&(hx`h( z1%9}Ui^F&kVrpvIptq6^VnDGmwSLgbqDuiCQd+xC*og*a2YJI%)IC~MX|Tr&$pBCZ zE`1KQxk!Xpc^zJeP zbssfPYt!7A0oAk{yPcm??GZ>!HZWLSSd)H4qh9UP;c z9fa`-+aJ8HE$DK8BgKnD`N){Z4kR?X$Iy3xUAQ7mU2grGOnk5-?+n%RA)|XvYZoC8 zXk+csU#jN$hh29D0LO0^6Mb^s0@R$N$KuO*O=%HG6&jM`!W&)}6}UxgUjdUaQ|IB3 z!BBI&632ARd9rhlsKhGu)S3u@-e}>@FcRFfQbSP7JY_W!-*W|YygrhxN0zh7$JI|xPYXG$vihSj0wUk(!icdH#(bEJvYp+Q zwZVjsJFQ+)aNN4xJJ6#IeKknPZNm1QB9wF2?qafv&@6-@3^y(j7_&uU7N2~snL9V^ zq>ue;tb4JOe)B8jr4)xq_^Uw>>Wi9;j4QgWz%OPi#>O{Ozt;d^fj1ns-PbCoygkFM z>3S4}i`VDQ_WMukou7e?KC=Zh{hJB{5bz8j(w>I}yHeCpI8&;a92^}T%^R8{Yg%^Bl1bHjfUwklIrC{} z@GqO~LjjPhnx zFaHrlN=h0MKwkk8UjPJRPYX?ox&Fd$VTpbR>^p$*noEzfs=ot0z@{o*b?p&2wCk^( z@bLF%Ly{|DmKk{d3BcF<0$>1x9=tuaxrurmKpUjN z6rcO-M9gNF@l7??%p|-&UfjGgc-5{nlGuWjw+6{y3-6bJH9`;)@&rM1$cxlE3?_k| zaeXaU7neM~Pn%g^9CfT7eg4$1S_ZaU>TL$Q^5OvvRaL_{ul-5LtipT7&4tuw@d`ah z)fI%u&jrc_CwMZDV&>AJLMg9l;@G&~^Pu!(4PGtgZhyp|JGH>29fAEjxB`pD&`XrI z1XeHO!sHvtslc`xGCZVoDYgD!_mGh9MV!gt!2KQ(h&vU6GylQ5-?LyX71O7LzrWXh zIDORdWB#JNPj#re=#*VMk}is{u^fg7X2HEY9JMwQ%MV#bA5X@CCf-jOoDTpZ;07K; z(KC0*eqzjZau84E7=;W!emlBkvl44%F89ILidHrJ8gzU=x&m#H9w>rNto(JxYxw{` zaguOdqxI;i7Fpr%_XDa2V^l~O5U&)i9M+D1=+~Nx9}5D!(*~Xl1)QZ5;Qab+ag7+Y zK*qUyCLL0O+TN?Tb;bVw05(@=@X`J5fSLBv!RX5%*&b2E4UWH>HC9_J0i`TW0-tR|P z9$&IUQNY;+(o+=QdHDF0!S=B(fRZF1c9uhiyl5w038&zmQ zLJWpg(mdovGS&ynJYMdnUofoK?wM;`pPutJ%3{D`6NqbrmUI}`O0+gT{}+PQKPSwC zZzYz%w{&s3tA7Tl$cAz5*%QJ#5=)_fC1l@ap~IU2@QvsPrbV15w?Ew!nHCt6wgGwU zFF>Gykb6!wx|L%xtG*2b59VEYp--;35Igup)%{;cvp%ymz{iK#9%~SM8WoCZ6Hfx@ zR^z(NeWZSalu`$X6+E79)Ac8lUAlJPpIgZkd2FqLx$kanhAn@EhO;Le8zHm?183C9ZT%UBwb&>~%G^Vm@t@To@@$B|0b=NaOGS=**X)5#c{({DP$q|K{P4M2zEz zduHsIrzBWv=&8<`YW%vI$nzgN^L~D-%iGWN=wSxuhe4}2QV>=C_?!*TFs=S~AoU1nSVXG_JsTFnS-8Mm=h)yxjzSmtcOZNh}z&?mltcA*u3R zZ8D$10taJ**;g_P|6u8I}Wt#pA9Zl?v!&g@*L_!n1{c>^jAY`y>=NZWjR{uwP z-@M_mp8+1wpEPujxssh;*ou$1@l0x!UX@I)L$xwG}H+Y0_y z%Y?omut$3=J4u8iXw$o%eF@No6q{T|aKcw93?q&R6gBlR7qIKKKl7J5(rISr0sI-J zPmq7`pa#j0#l<~^Xby00^n9Q}yE*2IMXzrs4ND|N&Cyx3?3M7g65D5?J^YhYFe%vb z_6u0MTx?V+{}Y%3%YtYBYUP7N82{v|u>2iS|LmRHA0!d0w=aiHeLrIH_8W2_=Ocdv zp7I2BIO9{uvdQ-3h$-0LU&L;5xE!#NzKjEvZ_D;M1JG63{mmxL8P(s10~tR3M{Ihw z*B|D2`-Lfw&u<6us`;iC7vtOKe7GfsjY@$4jhSLeI$tcbztAo?=6%R62P9u^HADpW zw6_Al`H>-V{VLrsbJQ#I##9|1o>v@Xp^cTlv4QZ6pk{G;aj_ z$gJGA=#7Kq7g}_<%u={xm`Uk?C=SXE=tr+#A7r?sTJictcsB8zgRZAQe)+j#x&%uE zxocNp``}rTpsxa)Qa#2l_uPjT z-JHkry1LT3)1dWYdhRMJ+dwo2sYdXUT#3}D&!2xjm)6tOrFDorCc#Gr8QHAjxV|a< zGx5RKN}Ls!9Ig;kQrDe!2H^$s>X+l2QkyPp#()12Zs9;i{=|tloD=e1s7#XLk)Ond z;HW(m13gyR?~Mumo~cO1WOqR?9v&S>Cm~Bl@-2bZ>G7e7JIv1eU~Pa`Yg+r+ly#w#)cstH3-DTXFjk)h~{H$TgpF#PD=Y5!9oPAyhCZn$wsl znVbb1gN@rcF$ac(n7&iXu=@=OOG=UgcnY7t>tk+Q=*bx^2Ypsu8h~g}&0Nv(Q>Sd% znjKvMJqBRp3}4YwYu^bcMx@CXri+=GHN!ZiD!jlsC9p@`qb!DQIWwB#zXH zTReExqVk>D>=I;2rVa+=x%6p+rn)}s8XOMtNK^DNic?4BYObBJBJ3?Ks$6Ky@n=qT zscx)zQuE~Cz&QwY;0P|6n8oW=>9;hIL`IyxMa}t(V>s(cM|6!pck(Oo*-f7QkZ~BI zva*3Q%jQbGCVx@t9TkrzMMR%tbGA>L84%17?q?Od&jVx#ULS0q)U9V33$VRUdmANjn>IGIY$W)%l=Fb14N7TG7^mx!{m#K?VOsawVTeHu|&SyjU9 zp52nFEdCTC8ye%mwL%ay-JHl+ch(?vwI5b*Z*LzG4Zh>BBTYx{QfJe#r1foN_RtTF zur3x}i+U%aAVCR21j6NM7)0{{_=F3jL?7$2D#o@5Fpy zT=}Kq4gqRe>fU#x20ML@H08IOzvA#T8;9L=swZ;@rSw)*(#h-#xR!p>A7I#*o7WPn zmE#V>q=s0$=@zfat7@ z@c-kY8M9@lh7_A_6(_>^+4~?x@vI^jVwV#Sosec3rZ`hdM^@ zXt~Ff-lWAJ|J&Y9;_}jJ^rcT(t{~~e>v>Iy*W$2Jj3o61ICHJ}BR7;wk0Sro zr9g$_KYv!~u@g7;*E5-*g?*u`16;(<)lLohE$`zq%MR`$xCk=`>07OUe}%Z}Sfy49 zU{84M1;%NqHHt4X9 z@@o$mY<5_0^y!B9Q|*%G$ah^GH!6Nt172M^L>k!Wrm^;$)adC8lCK=#hC4R5?fk~~ z0K*)N-e(-(1RMIY9EtOo@E5~Vgf3oKH`kVv>2~?&DCjOQp6~9G|-sf^%_s76{^W&*&028ofq?9wZbDfD&KA5FFCKe-^#IT z`2KlJe~2v_zvxaQROf8jqojV7GbI%A7m4sz60qgklr9dHkORrJ-+>lDgM>j46;fC3 z@4Hpd*RUoCb)ZCnRNJc_10xN*H(-{&jU7YdZ^>XH9w>z`wp1~#+AGjy*wbP_ET9aAG}5kd86gxAg>rHc1nOmOcDB{o+X7fS zAR9KR;#xCgmI039d%byPdAgT#X4oKhO`cBjW!b>V*;ys{PfYX?6G#r>h_lcQ#<#17 z`lRVq^KoxUz{;}T8vKQ;^`EdB~+T6BmIPZaXEvU_-ihPZt@JGO6l z*s1+K3gg|FtQ|hi#xuhN$#M=DNUcQBe-W#{#o_*nD{BC+CT`a{_3AO~gJ(q7d%3(t z=Imd)8Ul+1bEsM|ZRX1QZN-CPTgVHaZASF~O|71z=chnr( zyX{m#etwy%8|Krjl&M!a?bSN3TEBm+r-@D}oZaxuTu7kaXu)5eaHC{lfm{2056Df) z@jY$TW!s`1zMHeq)ex;7e(jeg-~Ekj7}SwKgxtpnyds#`5Qs}9U*H35KZ%YopiW_C z{cxA>`XK7dmoKOdzu9qTxdEt>80W_3g9#X3it7H=YM8zHHp%GDNNjaO)VKHTniPHV z7y%G>nD$XWmX`~Bk;M4=`)_qXOv+d${8fK*8K5KPuulWf93@(K%Pf-pjT%VC6X1Zt zz8BPV4cZ(7O5EAs=b`MUg}SoGSn*IH+c-}#pr1s(XRy^8g&_&b0|ad3-KQ%j`!9Bk z%gjPn!0ew3i0PEvj1-}@;ln){FfNrEshfjkX9i!cNXiW8!2mVzU0PuJH72wINPHMs z3>_i2qA!oCAIxZJfrG$A>&acQrrmRgo0!+HVkS)YenBL2ck>& z^1iTWdOHjff`I5BERPZCKk*@6cktyg_EN%UmJb#pMvl9_S1Xq$H^zvS&!=2{GM?vg z-M1=iH$1cJYI#?z>N6Q@QvJy6rre)YVwRT}0Y=6tS;SC-sS0)fi?3v%?w+>EM!%17 z=gZy+L)BiJAf;%By6(>IZjMkp@{~y3S0mM>Pl_kngCpI+01!$0M$lM!458-jUpb*=DtFmnWS096_aJe8&+Mz$4w z8$}aerVRgp$mwM_sy%`eudufErF1Kq=vMm@B`TX`#^TT8mOAM&%NEpoyfYqiu`%=XX<`4j3Qg)TEgwTG_B(Mq@#ZS_<&X z4i?xM5jw-mf9vb(JmLqWUV_F36CPlyWYlGS%LsK1fltW5 zIobjF*~5=fZlx|Q2K*(BW%fCD0jLPB^=_TV$o+mGh#@yO*Iww_2CA@(lcakii>-;M zviRk|o4`rT*Y5;MFoPAMLm)+Q(+npL6p&(o6s7y7Q0{p`k#85-n}xksu{JSI!Io84ajc|`00sSu)=Y`1H_M9 ztL&TSMwTiA1_8*QgWXpBC3xlHHc`UKeEnty&;YoBNM0i+uL&6Hyxx+1ed{oTGExe- zU%az!B=^4+Fn{EZ=Bn}!panxgN=)GYG$0^*+D-w@`%8+u!R;r-BHB{2j*MS}gP0fINOdccv6olB?-NwxDxQ2=JifOu zb$I)*P)f@u32ewP`BD3yCLgc}nSq$uWizzBTNGFyOmU>egqSvjx9g1ZvxC`#1c*w8 z7>%XJ(ecS&r<3I+U_qRk@|VN7`)wtGzMO2pS?(16XUj8{+~cj)#A$^wQ0$oi)v!|w z+Mfo#=oCgdiO|Gz`Zf%9%>e~elW1wm$l+YQbLyL?2{tArEqJo?G(0Pvt~JBBHc_D* znAI8OpxS{u-Z9azmUBoUybO{6E*d>@tg6 z?Nr~MNi_nRytTe3FY|LSs<3@~_Z+{+o6;Luf*5K-t9cnmZ|#_b2)l-(f8 zbLjyh0iFv$H&S=Hvz&R>&5N4`7fTLbvkUt*h9Me5;!p>mitgN%!OF)k0B!S&-NDky zMyLmK^JPq7vHsnVF_4_@r;@9Tr#9+QY_O0NZj&~vY#AWp&_!?-MDseOnGKr%V;KXEx| zxk#ax#aQnJJszm1-MX0Q%lB-2@@!W`&g<7boKleCmiqxz17?Eox?}w-bcaC7Uaf(m)xp8&RgzvDgZ>S0b41Jdq0CyRe*X1_WOS~H zyG2S(o1qHM%!rOsM@mYH|113gl5-;u$aaWsNNl1+o+qO?7cj@RzWo>R=hyuC!`6(K2mh$rQ)~WV4n5Pk2k|D&PAn;YT zB+kx;ZlxkAI4)x{)7@bA-2ov=_X1{N@(SgAxvL&94n3+XB(dr1S#%S?C=_@4gEjFQ z@%F43NPx|*Cth=^Z+n?=_?J@C&^Ya?_+%FpoXfj+q{ZE&98%Ob-G-$hOFhK_FPQQB zyr0*0UH%U@I0J2?ij2acIS+E&k%6a&udP`nu|5DOd*i%vy*COk-SVCFteSdefvYd$ z(POfBJUpS7LQ1#i0X#nssc2~IkgFX~VKzu3&mQ6TUlc^`Y7b{mmJ)t&@W`Y{ySgkc zW*AhB0tg>1?A7}c65hVe52FLkvI>xhmLJRQ1@F+&(1x0>5r6(D)`_|WwbyF6zWF?L z3=U_x?n!8@Ilm#T13xtQpoPm+roy<14sND`iYqZxbz3qG7ZVi?c*@J0I9rn?HZLik zWjbj_I&-PAf!05j*N+!(wcaRB0UzAn4q(vNh( zP&gFWn1fwMym}I|T7sv0Dy0(L*R>;nc$CgqEjYs9#_r|KwhmUG&U)LMg{Q4iiPHYL z$rw*y<+Qjz3jxzyDDf#D@fgww@RVLc&rHd9|Ncc zCDGg61mV$*gM;y?f>dvxr6Uk8Lr{E&auu~nuZBF<-Q8{TdX#scK9dVQL8?X>LUdwQ zunC~W3U#b$Km%w$GKo&4UsN*`8V=g#ExGEide21hbrm0LM@{&vHg4X-OQo!K4G)Gg zZvPybh>F56@FdnwsPbGmQJOaI%7m+QkMM8d2-A~0<^-M&DR>9bvz0n1(GOMF<|{`y z6_w4RMd@@s;mIsHd_)5j<|-w_k&V7;T{mAXXe~mmoo~SuR?CU4sgIUP(8>n@jHw`| z%Wma7;LNCH;MLs~5cF3?DEmApS84NQ15+G$yT2mVI{+32~KYxSAp7UcKk&4FxQg5*vR^xXp zaea;-vQ}cuWOr)Z)UA0l0?bG+*0r^;(+d~=9K{p=!5G)Jl+VV>O4pB1UT$u@-|Uw+ zBO@Z7dyJtPiT_zyF?Y4Mx1S%hbt79#5r7}a1Og?9bMiUs=K)i~6+RU+#_sp;I=+GD zB)Wi1_K;|C0smJ|^mESPgta!HHWPKJzt3tvHzOT~9P^C2hh-^;xA zjZX&{x?f6S@sLC7pV(gS85H$8kNlI|O+#0cSeoDjK7wvbpmMISOxs?|UjTvI0$Bn7(_furslSLIXV8cPdT)W2ffUm)48YEX0;BF0L8UT@v=y1@m9YG&F(9^2(b_#siwzEGxQz`?H}NJ zfX79BP+@MZinfH}2ilO3z>M(SL3o$vrWhFUSwTi zfB8ZlgKI%tzf-=5o6l)rQYUUcj?s_39NUB4I1JdCN<7wz;7F-8ReAlJ@8I1EphAB3 z>lR@#u@`92-14%`m#=cFVTV0$+G<2srZ~t9-rnNG{D4hCMY|dhaVO+pFn*W$I7pv12_hte&bR)$^yr1Tio%i!})IC_hdeI z$!mM@R22y`n+zV{AET&Kg=$n6o2%kMXime{v<-JfjP#Tr&us*i<<97m=P^zJV*yH$ zhwk1|=5YxA5Bs1{s>J?gK|?;^FS|!SR~q^!f{W~ZusbgUN)QaEQRp0yiUw~67rz1& z?)q^a7eIHakl-dJD`^mONH}cJ*m4yGo!up=FXeCh|Cpb6dy2-R(yvd~YL0uo)Wl*SbOEC%KU^Bcvn-B@|@nVK$@hKeecK8VM1JWP}V zA-=FNvgvgAvt6PhTxMoV>G3X=m@Q7M^xI{(4kXA7W`GH$Oy{EeAM%J#r3 z#fTC;7b!UmC6dQ$MIX%808;pF85kH-4$jE<*nPFvPH_@i?W^^_0CWB+PJn@9YC4?BUp-nQ@%JpLUG+1ky(K$NJU zAR~z4RoFo}aFPu0Mv^T-*sBHE*)KKEFeHV)-Ee!zLCNn@H+*`OX1k|Yy`+(Xnj-r~ z6PJ27K?2jnvN!z%`UTpr3G_N0uhF}j*Ddaz$r&kf7~Aop#L!(gnF?q><-DiRI((`v z-^JBZ@yZ=(z+yH5`!I3p@m!1&N|7?qFdPJrpKCqyp}*9|9k_a|J+aX(VOI*jH#`8z zbwDqmrqzmSXtrm5<|VjAFQi3lm2t!G0!D!OpFgLvakmRP9ahX{)KW3~{iam2riWiv zb$o7ZjU@a7nbS#1PLK)X1T@ZxjzmO80_df9t+Ms^YNf=iH5@X*<0EMr;9%A*7(-y( z?clvu%iEMqv%|ikdb=iq(C&6v{nb1qc*_8{!xV*dnnVyAQB+~in?8T0TzCcO^Kufo zVo~^R5#DxKSQAQ2U^f!zfrPRFAUayF$6!M_W1M-O>Nn@RDCU2mfk4bmx|M6d3r~c< zOK6fRi42`;S z9wSvbO%jElp5&jsLAJB4AG6A+@cYKU2Z*=Ue`|nflf}8g((IlMn$)4udA7E+L=8Z) z>x3nP&FX^@fm|QZT4#FHkIkc)k?RXLWub&0-z)nqK%j7Ad40jbDSaVP?F&rClYA$9#y(vTdh<4uK> zF(Em1;Atw0%q^=(63UpVF;Vh$8+jT;a}rFVHlvw}|3w+&m2U_LbpYsANA`ng=Yc>7 zOS>NUiR-Jr!rTWZQXYO&P9deNY`rHS?w&AUq?N z^CDe|i{lPEzP<6#%IL$ymcm^V`2~Y`24R}sydj49YK?84{7wuBAX(c4PD1t8F;h!; z6R5fcOuRSuA^GPyHA;MFy0iL&jt|Sgo%_JDjfwN_+8v443tAWY*}tFr3uW=+Bgv`( zu&T94`{POhv49mWka*pb^XT3LOO`)eVhm3C`(YNH7U+Y1fkWT$rH`ONrcTwK*KArA zAymN56nh<^0>URLU5n3w2Ot9m0l=r+!|$p#0Gy|He$$E;d`?5CaBX~GAR~%m-kiqOvPNJ z&m4b3z*Osr+ld$ zG&-u2)CZvJOg17AS%&1`ww=z{Z>5O3NH#Fg`f*RPs(vD9Op`j;r&4an{sU8txbBe- zKV-#;?$A5^?#Awe2FhT0VFCW;t@x2H#@elWk;hRi=h#NcaMj1rC-itSNN~frdgZ5R z54XN71~*kJu&tajAa^mk!)>T$`hzlWzL9-im8?1~KBTc4i9;wE-6u%l?-wpU%n#ZG zk2M3@1vw=JG}S9<1rx7H7_j^2{Cay&*pO7^*vF!|+Dj;BNpn>7itk=J6I8dQ{RZL4 zD&Q~JI%AcOan3=_cdgn+zvTmVUwOVD4x)S8C4H4e5- znlqCRbe?d+vqs?`NVv`XIX}-P)2`&PTSD6a7G`W;_{X38BcWm^tpEo+PsTv32$i4P z$rF1@VykvXJU4!^8OiT>sIK$0T}@5-ESleCrMQz5Cc^59&byKVw}~Tcv^_bHA&_j_ z`Qmme;sXmhBr>N==aOrelf%jVMA;~s^}#9H5s}xf`CuY(;Z6~UY~=pyFA2IX9!xS! z-Rf*=4yUIN3TmmBvcN%#%{y5_8-LwcUiZt(Aw>()Xw@KQNvGxontlxY$iq~eq@z%> zG`hIMAprR_NWg{kj;7c~Gqjdd2war%!4c@iik&NfWehxmK1ddj9dCntxhSiUVh0GT ztY43)jneGcOkogAFiu#hda30S$F~sJ?uQOXlZlIuKYbe?|Bm+&1c)p4fJ-}z zv5MlhBtG20&{b<9rOT}d&BfifV~44-_NAdD_$f-P^+bCX@Jut~`0{(nGU8XRfS^Uw z$y`0Mj}??cP~(gaZ-=cJ#u$WbZxt=r@W&-2RH0F_0qE=)5!3~UWu885uSV^@qZAk2 zmH;kT7Q>hZIg<5#UzSJjp8Wp(Eid4nQ$Ko?$C&lE6l30u9{(T(Op!JOB`vZJQ-DJ} z{d$TqDglD3tX2C@)A&|mHTLa*JFMHXfYV@Z_1@;DCM5c#?cO%t{+araszm6?W0&H1 z{O`U0KRxtsWC*1-VaDAmIA1=Uoqe}$?S>*b$H8+X467GEO8)?P9j}O>GU!1@YE@L1 z+|A%H1A@evw}}7%dk)E+H%RbC{Qe>eq}y{&;NRk?IhPR~X{Ds1#jtH4aQXuoE_Th~ zSEai(hzT&J55Qabk77#Ef3(j>G*fF=K3L-c($2KOGUeUuuLGdMG~UA@-u?U8-Tz7= z#%O9~^0))gH`$Oo$-#HmHh?I-@0nF8*yREC=O`fY`SG`aTuhS(nba(P2H%D*F8s1! z7oCM^0sI`n_fS8;bF_c^4oNH^2DPeer|lCaiCX+5R96q4Sy)2i3*Of4E+`EQ`hX6- z1yJ}*u|0a`U9GjLyH-jWcO$ajkCQ39#Qq=K9rCgXzyrD}90Yk_Coe*N?qQdjk}`{E zcxsI?^9zKQ+}4p3bs*IL7ISs=*% z0`ulyu2A6lh%#jOqM!v+^)lAnvLW_7=NRh_84?81W*azKcxKw92v*OFs>{v0I-g;P~Qeshfd_G<;%%34-aV{dpu- z(b8~B$+dBAXSqWYww4fdPg^8_+4#Ui>8$Q7CpgP-5Ked(gVq`tjLGsq_Zc0H8BQYn zN@09aIfPXdbx#AE)*Sx^S;UFRCUmp;TpZB3@680y)aF4F1!~XD)}yn{8z43O28N2p zitumanP;ddyr~5=T=41y9P6c&8!rPI6%^RXi@3=TYL;r;;%CCd#R-lFuEAJ$ivd`SK=TWl!;6K_eIZ;q)*cQLM7)`G2}W7 z9~+IWZ7n_fTT^N>W4ddimOfn;zwE`Y-udkWG0Hce^q|5_Rt9az>$zGkK8{<#)L(q1 zMGf^wb2t}>0WUj0MM>P4FGj-9BsiAZ1{-QV7%rjXK|U4%JPyC>;B3#>!OH4clQ2tx zCOR(kqqSJD2Jy1CTSj`kt4r!0n`5T-KH>+c^pWv#P4jDWkbtkMzIh!K1pD`h1U3C# z@{}b?>f|f(OcH{{U8};GwbmlA>5btW)>>uBKMBp>;$UuILS4BAJpLqYqx-xiWOzIk zXBgN7s`X=%&Tsul_c6^8j~$Cwcm^n;g4*dju%$X00L;;NR&3te>qWcnt92j!O4t8{nwJ-0A7 zQ%9Z@EJ`+nclIZk;yyuNiS4;c4M&T^`-Gf0->n@kEhkP`O=3Q?xo?r+xxWw4fv?mD zp9A=j6JMHd2Ris+nlm{;kNVzYinaEsslL8jOBq4;Cdf+5r$~J+Ez>fI--)^H%NJ9v zjta2PJx|R#J@qz?FREkHp~aWEYvax2ODanJ!t&;(s=&8)gCm!<8#LdtpeNH>i&!=r zfLqx;GQD{Z98ua2`&&4Nf4m>TR18@f{-T){16O`Ax8w~zzQJ15&k*zD0?U&{g&iu_ z%3M1h!OIKyQ)u(+UqEwpGNGMijYDv7o`BAb_j$9u<;OCr?+7zrGmK-?dNx!|QxF9T zl2mBJdtr&~=)clA-A>ObM$1uXK`NIDgP`-qaAY`uLyh^v>{D*7Fk@1ivq(knww^qG z++?7zuC)i=>+8Y-#Y40Tq|9_1!xev1T>B1*KS-}v_?+m*HrdPT>$UaFjkS@G(J&m}C3;R&h7xUv(PjyCX;&1J9sUQ#LF?v|d zOWNydIrQ;hpO1%Wpw7E{SxJBcLj|q#zEVSZwW3fs+P+ZGm-mX=tE&@nUK@jGz)H$-S_R> zmcNzMVh^g$Iv-yx#}~(KT@LN+RMOm<+0rmPTJ0FNzwY#!0S>H~p2mKMS3il=m@A1+ zZW>K7+0^J2&@o!|F`5dGs#M*I5^Ozg7gdyfvwm<>*df0cygPO zf6lets3bul7(LQpEbaf@()NrN=D@W;3QRKjMf=5*n>|R=TNJqD&zbT z;Kyo|f3JI5abhr{TLX&>`c>p{qY31rO+#YCaLuiDVc>pndJ{Obxv$=z#ZPWG)Gcfq z>ihgHi?v535!=W3++=a+;Sd<@NYzeM$Cd9$%ij*b|HM9HN<-F6x49ID7vWdX5A)Zi z3t_e4id&Lkl7Z;%3Qs5q$0&rN+G_qc#gH@LUMY-O|A4|$Jgh<1Yx*}O!EnBiZ?zxA z1l)^aq!Falr^sl7rXfvjZIzGJeA6t=tdtcF;$-F~5N%Sq)g}q~)6=@Jt_-k`o3Q56 zab&KSPk#Vo1xx2HKhr;NO`swC`@1O#q^6>$j z80;aO_w&-yi1Qb>X-t0Y`CMe?J;>DY){O{@R*f{WvQpu`+H(J!zX2;jPOb?{yXEt6 zy85UdOOA8ge2fVwm229JI8uD5AVAM%Vi%Pn(mKspq(Tb=B8&!%Z)J z!x9h1+rU|#8Rt*qH69o{`?|JjIh3WM#?%jZRMTxf*=u4c9=@ZEp@50`8sClbVaijo zF~^i~4tq%0zIXP&y?QK#1|K+XFML%&F_x{0!RCO{TRpm*_g28=*tgMrR(k|k+PcU|jI=wxL zqx60B5or=KZR^L67v4cZzsB)QUc6AfjDC)3e^XSJOSi*$)mf2~pwzN_DGJHi@W}TK z4JpU4?dB3|4)9B^Fg%mOL6VC8vy`0Wj9sp4p{+kmvxj2) zw8V~g3sL#nm$xawFP|kEnO0DH*v6g25VC9tm6>`!O4Tb9f9@yu6Pq@QFC!yks*Pd_ zgLTcx*2P6_ay!cgtrwGSL``qXS?_Gs9;ALs0-wqIgFu3>0td!lpP*)L#L*G|L%VK$ z|Eev&#FyKdgD%}H7$2YkgWt%&;F39qrG;UymRfCna?B%R(TGO$GtP>?H6COTN~PNf zRrORpx5erbXMy)D`*Xnr?bhAgFfYoRod}z7e;}Dyr^QxKb{ydQ=ktj0QYklZ?yA5_ z^GprEo3=}L*wuc8Co~of9tVYssFqoEsZW%+$CI_j4xmOG;e~sDiewf=a4(n^CuZJ> z|8-Zrb8QN>>3vvzB?kTwcJ&=d$Vx0TT*bPQNh#CN4R#SXdML0(%D=`jw**eRiTJ#T zNQ!xoXtG@ePfS+@j~U}~u;`9Sd0U}MU`sc|amIRvj?DB&K12FHPRd_lSnzhkHh0+= z3Uu=jcXzE0mgil*>Xd2wR(Zh*_o-iaxVh1Yb%{#FM!{}uxb2o3MrONpO^D=!gkD0b zJ7+>0u-;8@84brYIU#U5Z272?&zm;=_L=IKlR`PvUy0 zoN7=&A@qBd`mQz4p%@RauB(apa4=>E$zFXWZOw{Pr4Dr$NH}$ryh^kYi9pqG9MTY( zc%}nwf+XSsz$1*sF`tSRt#N9Yq8AA7P*~8o}xMJ$?O1$6Qm!%&^w%iXK5t-xWm3PM%d^G=(%B7~!z9 zdqixGeWg(aWhlOR-GHPnL=ZjQItN@?6AG8pvG}2s6%p9GDJOV!Jv1~#TJXJ-2W)0z zy65s!mq5gazlDXgM+yv2s$(&=;1D^n;8R*!TCHog#iyhKsIH+Q+0$T31DJjmpMi#h zx3_m@2LrlPjIz5Y{%McXzd#8TtLfnjq@S~sU%!4udFtf;9SoFRB=^iw;{hUk_<-_8 z(B$7(BNz@A6oz*+uVF|^;_z*n0dHVE2vZ6an5Glvsk|+jee@(3IZy6>y(cQGM)@cq zE{=70c({@-zw-R^$L~%BfuladEVyvRAsfTR{i$ZtNcjaxNWyqq$8poU_*5U`I>;Wb z?_xg530Kt|GcYlkwp+jo=?4^D4lZu)yW%M=R45!E`=#>q!Jd~Mn>lM*@+H^;t=Cne z>1za+aLl)N7j6Jar`p$P} zLQdF*C!KG9Ya>))nP!HDNvx&wiI=Ldi!Tig+7lxA{&g6kF7quF@58k#QL`j!s;e=4 za=gJ(MERT7#3&j#*GdN^HkHCJKz+XlNnMgn7KKb)KJx6K;T{phaqq)W$Z)TRP7bK~0~3UiC``{ga0kZ-WfU8+5$GNiJjASZtho^5?mkdE=q zI+!=v!DxKad_SV1VqbF@?*^79osEH$qW1Q7=b4fh7^~b^l{eKnJLtImDOtbP@w5|O zgX!G{m|(K@Ro*Szqmt1QU1*tdhJO0(@C+LE|(L;v;Znc`nZYKwwP< z<>VKgNBRsW!Z-(kw!DXKsRSB`d;31rOwSDNb(``GuAg&hOXpGXbVK z^le`o|In{CR3Wz_TAN){nOK>PwUnyV>#dwomdi)*LwYbxs`do#$0s7~Ov>*o$nAXY zwM2~*TVx65V%QdJg<8QcMrLtJy|f$k)FsX2<>h6Sw@EWPby!PN+yzCS;T7qmYUg*f z7P1Remsu`s?ic88=_{_yy?sfPrNEX(|LdEU9A!Dxzn|@T?$*aizJ5O1?rp!Emvou} z$}xC82Jn9E7}BVvDL?Wwig#?ectg;fJpOwAdI+2vfJ{`PBe|y*!@F1lI}xC=kx*Z( zR^-T)hW4iclwvA?KPWMqZ9to*=2@ln#;=8Ik+HuHd}Fik+5h$Q^63sk*TB%-DnL_Q zfTfXgK?hQ16t^G!Gq%xoP``xZe*C3gncAYiiM%47u)uq*o7zGp(frhxoceDmR$gSV zmiAbHq%d2UfBl&>Unkn7eDr8^_be(aejW2>R!pHN)s||`8>EjB`k_KO z84~w%sn%CQ?GbeEwk>U{K<#2JUCu~Pw*`-(oxPbbUs_r!qU>P!u;o(e>5oUhj=+lJ z*W{$O-tn&=;76R&lh3%2*W$<0rFambF@aU;C9@@5e;xF~$Vgd7S+eaSUXd*iYh63FdFiO)aB)|Z`SnL11BnlwVcvN3QdvW_vZvfD!)6BLyqKcfSFXPL!XyB z6DF84TZ9XGz|)bJ9pntY_CgB1N-r>#c)P(;DyXm0C-e^87p)iES&8{cj88-gc88+H z+5-?Dr4HgPW9SBZ2dMm_ks;S&8V=xA>rw4bry4Q@+k1;Z7Le0S{oaag<2x8$AU6gx z_uT|x)Q*g7I z{kT`^%8VFh5Z1?ehJeeat!!seFj?T46hKcXL-EBh-nGU~C~&`V!N48-k=NM-IhuUE zQJztJ^9hO#rsw454#D}9{(-Co z-`fnlH5<6@4^`Q&{h&~N?|F%t!KCqGP$TDa8OYFLs;I~HF7bOe4m_)`uaC+#2d!M} zuWt0Bf{bjE_w7R|miJC~_Y>0gFy7sMC}QeNIPt!=G@>t`s?HqFlVsjLe?0N%P3D-m z`|-KxE^49=eog*g8id-=uzTE{0x}AL{m@}45?24znQJo0D3pp`nG+2~0NeCPoqPz( znDKl@EHf|*&iA_QE}2#&&`K>Mwc(!3VB*=Smxb^QvL+*b%u`H<#O2_&fBZN=Y2lj9 z?;53KrlT+G#Us+U)4MP|m5f1m38rJV!a+5Wzx~yCGBKbJL3MR?yfhOnDe zPE?*UWP~28HJG#{4%j?AjE>6x8x1VH~B+I@}-|k)DF2#FJ zJX+*pdy_a=(Y*mGCs&EfDIAaAVvEf1#fx>rTjlQ+rrs~&C|i80(1Pw~?I%{WjkX8A zi}v7CR9=l$XipJBX$4>~jYM;*uDfnduGW;%n)v!vhN8_>+GyOx!Xj)Z5c_^+Qnlqi z@aFvsGiN*}R35OT*>IzQMiq3C80dx@Z!oGu3t<|v|7w^gqsq` zjuYNF?XZCuM!RIgFU^MTKRHj3drCjzAz#VL4Cp^bc)+*nMoyGbr6N!<-?%U7WamRk za0FT#Uy(=Rlge8%Iy7uaVJ_?+xe3TAZxZ8-#Xg{(cX*eEhlgW5t)O`z)q(}DmF9qr zB=L3=;W8wYk7$2?LmQrqs)m$_?T;MuSLDmo2iSA6id@u#hpw>YrqM>GU-D1&4H%Zu zm7Mn<@NjY-EL1}>113aYdOCYH&KKVn=f8fH6Cn#Kg&fkMC`N`&`*F*9k(}-`LU=e` z>I2nq+I}g@ zTREr*X@ifml^RnuN0{bH4d7ax{1s`tdsQH1kkq znUJvW+*CzJ*={bxxcUEBIuCfN|L>1~Y|6~Y%1C8YvR8ISBwJZY_DZ(wt0an$YtOQi z$coG)dvB7JP4;%V|Kt1nKRo(=xvu+pf6jY;&N;8=iC@v%OGyy>m*%}7^C4*YVB(9t zH)`dVy+zgo&iD-UyxooFlCU`M^q*Ybl3(3T3l}hWZaKVPRofUz1s_jn$<8Z;tQ?_Q4eZ7Fxb9p-jb*B^J*_?}-Wvb3?oI zk8=zQw)mDlOa%eXk@$iOS>j)jlB?&}6I9MQavV#;+f+!fBv*H)X74_Fxk$0J2Kr{f zgQ`+m(vPEWfRry!c5s{YYqb)6rjW4kuCj%XDk31a)Xe`ouB4*#Jt4~X=AMb#Jp0?< zxkk6VJDdvkl^O!hzD~B%C};j62T=3yCXM}v%F0pKM9m<{W4Ljis5^8+`^)w z(O^Lj=Xs~32HNw+Ki>XGhED}>OF^=7QCGswciRAt#NC*(So#zp zKqoxO1fK$5yX^t;y*ku4mcI^8MIQr-oI%>IWe_ioV!8%*{@y$9n|7tx`!cNt#7!;7 za0f%CUrwK_Z$6mUXQv;w&IXEInZ#U1rXKxHjbVYhqMp)=7;2J#kKXr(=?{MRve1ned2Gl1 z1HJY~@d332Ub-wdE!pfePxR|bV*Qw9CoC6LjKg=M=#ZhypK21!uL|yL8#-q0yNO*C z{9fi!P=|qvAlIg4{`Yll&gFhMfr=KawAxhW-lvC~WKm#698lY04h& zsk3fCNyY(O^xPRJ{^w-phdV}Z4c7uV>}+k>TMag9vhCg^6SGbB^<6Hh%hysjyLa!N z5&_1Ei2+T>Hwq^Zue;li*B3ED&ffGISua3Z zz$^EFBcBCuD#DCqa$sEo!26Mk7{RE${pLp62e+kDazkg#O( z@rP$Y8+jYL&&=LFpYj@;10@-YbsHc$iM3L1spbJ+Ov1R^XKQ{w7?1Bh1OX~vZ$DZ4 z7T?)yC+mGD8@PgM2O9r<6-En0z`2UD{L#$jpLWFMK{X6{ui6MBv#}^CPghsGZzxT& z=X;Ggaa=jxK81*vwAtU2^XC!%#j@P}#m`Do=cU8{wE=CcC9Go|z;~}8>?$Yo5WWMa z)r~4m#apswHQfO_XmzvhpF(pr6T_k7lpm2CvaX8(uCCGN%uj!2gI8aO zeHPE?aaZ6fmtczS#&2^{*;uJ0(MqhM>e53b21*Qw+ez`~P7$gujqmR6Du5F|`Ttj& z$3zk=-K+1OnKR~BiYb`RMggrB=44&i_HAR9eS5Q#ApAv>;gDJw^*7Z zbQxTK=5%}Izui9ta!>HzLnpy)7PO?N_^-7YzmiNV z&wGn(snf_NEo6=w%)cDh*lh@?U#io3k83)@#Yq1{iJfQ--v<1F%n1Jn#>tYX6?gYi zUY!cEjqf=`V(5}+hzQemdb;p=#GXUW!4InS)y)RkJV@|M?*%Z%`YKB_>1k)j*Bg+- zMxk60CsrQkBAo1bkBvOwC05X(`{;ui3}dwx*W0Wf8-^iQN;J-Pjf|~AxVwGPzGuPv0TdR=qC`Ne!d3c+H?PS4>kAXGrMOE43YGlG3*5W!&|7* z{O1cyCa%k-3WDz9<7*y!Soat!{~LI|^n27wO<}JBKNU^3fTK$Cw4K~3YeRGl zGkkm)zU6l^9?H&5ZaXe9WUsxjxwX|-On93;6Y7mhQx~z{zQbK6P;n1F_m$FL?9q`S zMd=_lZNr-xvVM|CF=XZre}y&2;EJXUIdYSa9s1mwbm)s3aVvwO6cp_xRuZ@%p&6si z!+(jxhEta39a$X-`@0>ZxCLb#zPx2y^7S9n@7|G%zu5kF(NjIq_76l8)dUiGK9yRlfAv{k4;Rn0am6^ zPJYy9aRC*8qnpf_Na1#B?&{B4j5h@#v>3{UpOPoqj`sFU!veA3$xISoq0eQk*3OU(GB_TaIeg8&iOq0lc!GG=hW-> zl46DSqv+jh7ArPc>!q$vmyd__CaAyqXAH3>YGQVALW?uF^OcAdzMpO3C~ZRrJG(>@ z)2HiALIxWB3yVWp(C1oxFR^qBy*m@4UumzZ+GayJa1wo%1%zjs0eD*S z`MTBGi-Zp-W8Zl=JO98tVA1un$Jc$I0dL=+^2LcTV132sEp2RUe1#kpbm5JtCD3|) zDM{8rj?$HEgVbQT^IEZ08R##BDG7u*QoW+miusqghu9X-Gq!~ixu?Udm-N!{DG=Wi)h=adyq1$=}{3XD`urVu+MSts&DICeh@ z;|)$I--AZ9VgQ>lBQay1*#9|a!$f;{P%+V)#ydX zuaOVUrAn`AQqY%2_A?vB)iIJ1{lnkLEeVp72mX*WZVe}ttmhURIac&IJ*4z{YwvYl zhZ9-R*^tT%dsXjqD>*F<9GU-F7brH3U|ry>U1|D(jh8kB}rQ}A!sizRP=>|%WQSGlvaA3huo3jGn^^mLoApf^`j^*pp< z>(?*E@6a)lGBOJ!?3Q?uGfYTtUZBDHe+L}ZJ1www7B$pU$tMr-J^(il5G(<&B8sO^ zTOO=Uvu&E8tb0alV|s-(v}XL;Vo6nh@%qADW@ZG&UZ5CKy@i2;-~>jFFOrnpu7z(|jEnq^mHZ;QJBC`rU@)2-b?2a! zSNA+~P~Uri?ezmz& zU%v3No8I=M-OQ@mw>&981AN&6IAQ*XrH`WSsbiO{s3zjO257&{W73&DGpAxx^5*2lwY6B%T60gYb3mgx zq-PHHg@n^ik!JW*@-h_-;S_A-(9DFi`0d-<+FSKh-OhBMVZ3)7cuHYSa12!QJ{a@U zKbDd?hYE#uCNNyG+v!T_9Yfx@-`o$l-}MZ=GPj^ks{bTDu&T-R7aN*bB*6B3QAckA z>2s-qgTs3tzC1iNkHuZRs#lrDUAe@UVg52~);(#{)z7cIkM4OyeF@n-W`ynoNasrSQIjT7!L_frG{z>;IoJakMvK??Z2)Kn=DQQ{$_o#Mw8v{nuz^5m* zGw}NPBqqUU52|x7s1+={waniBBu!~S=iSMSp!-xcG`cfe_th@G(f$N9n4MvIw<;YQ zd*4VGt=l-C=Vda>Dy;GOruo;EY~9;jXOP@~sU#r*MtD6_;~}f^7u;ph)|``gsaXBw z$=CBp(RFu%w5tWHi=b^zrF<`<{qcKMm9?FlOc%Yl0#}w9h>x?W4kEO&qaHPW(NlUP z@p<8Un1oUfgA8n%28iWr_s@ez2lfsorSLS6IW+2dS+EX!=U zxwQ)oISSICWL3+=ZO{)tpufLi_UQBN{>&O|(hQDJ08l81dYsSFCFJYv@6QTo~jb+OVO9ruy!tUsYBP^-R%lc zV-7D*x4Y{r;pI3bTzx6~rt2!p*W1wo?6ZQXH-_@Kd+CxCZ1=d`wO`h~_&uNc=C_L( z`5{Hpjl+E>jXx>J#A^fx?uZREQrK;TOCuYtF7ecC%|fljpPr|pptr;-wD^44{YNXa{32R#jBo z)l&WGf?KbVy>)nah)I(PxHc$g@XXD{r2$74j&XGqrxd~g%Y|TVaq;P8uhW!`wFVET z#8H(M?jEKxCj_Mb^y=Erp55B~Iu%GCXeEg+TpJyp?QRIa$Zb=F$HVoN6&+k@uGqh6 z$;k@K_)f({xW;c=pt;s@C)SgB%u;42EFk z;n=Mr`h{^blT!X1V2WPMKQn`u=3zJ}bEjCUR>7Yn<;8r?-##pyJCq{rUEI2(3U3Gq znlqD+7zf@5Kc0c0QkP#0IOxRXL&`C4!HX_bCG*EWZPQ9b^z8F%Ck`V5GkW>P_TWXc1~-mA18g!Apt66J9-DgJ_TEJ7MtC= z;qFi1Vf4m1N5!k@8IRq}0-L(gu9n+YbP3!-eJw1! z72QER6VEOClC;rv>iPa`55z5EMCdoiN^!o}`;MEs3C> z6{OTJ+;B(2C!te7SDm3Gh0|d2c3Ta#5Uj(yOW^8p z|KOlT3tcomJZzm4)~Ti1|6PF76%oMkV}+F4na$0w4v3@`&g@DGmT}$nP9(r-GS#>{ zfl{KD%Vu2fI~IEY6rr1VEs9byaDd4J56v^Jp*u}a2dsY0?n@f|rbrZyb3UJY-aGO& zPQ&GUPkED&Rc_TK4D4=AUy?)cP7Bt4e^eCuM4ayA?=L-9U*X(Q*S=eJySoJ4$2{ZJ z?Sjk$fbA{p+=9~URil8D-2WVvcVXxfJj@w^kx$dV9%>`=ddI(B{Skm<5X>{M$duA+ zS+PKxLy)iM3mBJIg93iRVJ~3->WW9$UwwRE6_ha_e!dz6ME&i--Gd#5OtKnf2kk{9|bHksu)ZLRFc9_+g^~p9Y_YblWuCoy$f^oequq=YyM6L>6>ES-Tux`zM&x|wz1gDfm4=q7PM0oX4WJ13!8*IptfRvU%#zPy%2Cye+An=IKR`}@m z+M46s&dxv&3H09krGNioVEew>>Dl0f>(ue}XbC_}o%Pyo6+e6)6}=1!x*(6s%h^@* zdKyVbt5>3T%bri_B0p|y%pt@t>Dl7M!K|0Eal40?QtyMyn^v*ai8)6>*dMN0kFIoi z*^07l%we%0m*pkR693?Y<5m)4qPkSKiaoX8NNVx%ll0yRiKl-iBwqJ!SmIqV-!obU z;{Bz3<}9IHuM}y>jeg!@eD9~^%|9!rad@ZxzbsfYpZfph8tY}3F{Z~1x8ShBCvZJu zOWUdU8G9{x5uy2`6t&Du8B&zG^eBPorKcV-yH|d#mSN7TU&hXem4>={a7SC1o2TbR zz){r|%QAIwy)h=pGTxxLUEgQ>$ge5mAzueNpe~c^+(Tt=b;kOAm;zN&bI#1E;W~US zJYF$3-+V;l`2H1B=fiPcAHBu9wp{#?Yf4PLrzM&Bx?m%N&>Ss}@3h#L3#FHQ+}wn0 zryFOuvMg)-vboN|4pP$kr4|2?{LmDs-$R-#;`GLYXsOuwVm$t~2cu8FOZ^o5t=jix zI72xC4RO;jxr9ptY|k@@(=BL5hT)iVw);uo=r_F`==XRpljq?(BNf8R11>j$uA+=xl#y? zB7d^`yT5mJsi$L!O4g6BL~-98TVxOXD`v1g&OnX22S=@=<73*i?I}B7MJ<;nxR2HM zWbfKj@pO}5$(jS|sEL>bPbpITWgJd_Mn0o>idDFJ{*k81d1E$uBH5nstv6?!Dc%Lo z3FK~^*t?{S1EZ+F6>5}|!zV+znP{6C;#|(teyVlzz`qsMB#Uu^(Lb!V8b7)X78xm@ zJ)}>O1x(k=fgRD8gW>}vE}&GdD-*U0B)PUKHGNVT+}z~vZc^tJi|3*GYC&SOZ@n-g~;pY}x%ocpP8v z%!tPG^knJ(y2JJbe~SEyr}IN-?)Al;Zw7BmZV8xJ;M=E-Z5mCBIrb@?zW&5PzwtRn zSRhNN8_8r_i>j(pbLHjy$IJ&(XKuvPvY9LE5Y)6kWR)ozNZ7m=ZMVV;vS#?Weim_} zc%H#N_H&J7YT!dJ76KJ_U`hRX4HVuo=Im)U0*cd`AW}B|s-W|TfA)AZkA*ShOV5Rc z3v`ZN#PJK4QSYfYCT1$Rk!`qCZ8GB6ydb6yI(LPefrXj)stp082WfgMzCRg_AJ8Z= zkP8@bX0q_Ko(9@C%XM0^qv}Wv@hN-j?Fa}}(!lM4FIs;+3%ze=p`*&lKp!=PaTLg1 zXWC&^Wl*U9h;vBsmIp++UU+z|Jj>E#>3~VEAXuSMB;nbr`^X(N!{w43bBiAj`}CgN zgM7VVDajGR*Ec_+E#Gc2cv7_`eyv7nZNWfR-C$7x4u{0i>=V|>P1XS^+UTthCN|5k zIftmF-$0%10-5J@zhCN?B|l?n^nw%~77=C^meUxL^s>jsBqfsp7#BzN*>N*MrS&G#0>uSmaeQ$$dNc8pRpommUbOn_xa~Ll_V{2y{ciN$ zX^1E3T|tKQ_M+pYjl0{+lfN|e*oHM;?l(L{gf4OY7S(rQtiGSWZA^CWfE%bV@^GYa zITO z-)?)GP!l0Dbe5X;3>KGpoxe)=v(rb|T^g;{AgWLN>8>?Zq=1TBUpX)qcL>FpHhEv#o zSwU^*2F%{QCW-b`$(#GBcJTVVb!hMH&I|H=j0=OAT9l+&rMRxh;1|;QQnJYxYd7AC zbZYMU$pklaagi*hl6?|7M=^B_)AJOz^%qPNH)drIp%u?D2VVh@RS37$-b0c;cD6P` z|AJ|@&J1;Fd^`$#m^RvU?w6_GUKHBpjpkH# z0#~8aV0xsvO)S3naK8~pQ(5=C!Chh!0*p14M`h$l7qCbCJs59V7i(nFw^4Jib*5Es zu;Qc0<7!^(O8o`?3Eh=%q29YUt#vnK5U;B?B}`)P+DmLKs`irvaIqxp46jEsta^XH z0t3V$Q(ziwBK;IMr%%k^3Q0U}9YW9KG~olgsFZYfo=oSY!}t6x{s+swI0&L6QvF}U z7j?0Hllve(SdxH~)O|}ut%=)n|KP%1B>K`~V)fCS8bR>k*UyGHy)fZ}xi%QCXE2E_ zoW;hHNfP8-;-o*ynNZ_sYKrZ6wlzBaSmOSe5J5Q}^EIkJQWvg`VAcT>U|n=k+9o-{ zac!>3FGD=14ZsxOd}~@7RTfLgQDQjHoTcmTp~Bp(lDMw0gwORUfiDzq>WAj(Qh!bZ zfGAA07<>3ars1bH_^A(71DHv#qn*52y?_FKzx)`ybUEjjZxZ$N@33fVjpf`AHO9O( z&hiI5f-8AV(Amivs#ji|J0L~1HQ$x>1z_O9OBT<70GX_}k}3zvezy_a)6CJ==Gc=Z zxD89Qo@{xp!uOPR>-Zs>biuf%B_;e!4TnAcR?&a9Oy@G^l<|f-198?xCKq5_SV>6| zsb{>ydw%OjZmAY5&Z*iw`kPjKvH7(ciN}i*$^_Viah`;Y&Z-ekh9;w)W4Qp&Ep#8c z_k^kG6ns%OtjY^*q12Svz)Er2M$0ZX71qYgv0E_OxTni~&odKx*{saZ58Allffv=} z!&Ess$!JOzhi{3qU&zYGD`bg0S+1N=?Xn8EROx&^15P2W|3nGL^lJCHGJI?q!+Gz@ zdEG6P#tawsn~(}aCR4sNftSvH3ycb1ML{}^h$rxZ^b&lS1Cfb6U2 z5~LEwpR6@r|GO~?yAU|3IwXDe)I!aCv-wD1-HVE?FPai1VrxDKEg&imJk(gOo#wsj z>mLw+6+~Tl>oVoUq<7HzOhNrU4yDGMyL1!W<8u8V^rD+q_7i)D{v7AGB|GcW_cMhYox4%XGWb^I*AFBb3h}jM9EJ1n)J{eL3)|%vEtvri$;b zj+VSX!rmX6v}E&|&pWYr_54nGf%tE#`6Fia&UcO3C-#x>gzB1=|vzyMwJa ziUr%j0F5Qq-S`F6rZC>jdz>CVn7cMZu@%&n+|9Zd zuM=p#PZ3dtyFc%>~lqS{k&x;M-M1gw6H_eys zvhE#7OrL1bH$BI#xPlqx#(wG(IRRa-Y+3?Do$jg81<&EP*pm%X+K7BC`|2cf}4QoNMaTb!uY)5EX>f)sl3 zDlyeOyPSI+fo==<&yRMN!Y*>^UjF+r2kdr!HI>$Wd}K648sdt2Ce_c`m+H;upcP8$ z2#M_c;r2p6;F)(WJf7j1<=^Uv%|pt+&;3^}?V||q5b#m$x?ZZahk<#a+g^g5`|P$# z$@XADRJ*&K+(-Z6jg|i!F!8j^huO=%i^mIKCEVzLauPUOpZHNusBQ*POBvq_g-W{K zBb?m%O@_h}5!`Y6*7pyvn}3O{EquL*X%3h-Fn&t#SbT#~`eMCi0}P!lUgQ%7dXQF1 zU!D8rRCzYp z`Zew>*Mn{nw!}g49JK$&*I)vs2_1=(M=&2M9X!@{dL!BK7p{1B-HZvq90ZtJYT^jld8$90mI%SFI!UpwESXzzm7h6|g(|1uDm%UdDI90bYH#cysmF<*{PU-?+V@bqZ!_QKK(AYU{@2@!0j*u&45vXIM*4&( zsNXWFj=b5WB+~Rb0HVSlw3ner1mTv`LY>x5ow=+VVe7;u-J1Kk;Ueg-#JTZ{i}AI7 zN0KZtcOrwCG|eN(vJT%!S{xF_!Y2_6=Rh%ph-+_b#kR39>fUz*6h+;Z@y8Hn^7FA@ z(cMTJ%m|Y>n(v1T+Z2H8dt$&LO+OOvTv1rfIQlE@*5d~dY%`7i<+d)`BYZR%c(Q$$ znB_88&F3dx&z@1h%}|c`x30`S8L~|HS{F7O`QqEMs;3za-ZqTUgp0RKxH<^YeikRP z)PYAg7hTxkjxLTQ@c#bCM9oIc@8LwR_{cSJH!4-i=i14^rg_5+N;sCXt)Yqm=3*qO zjyn5I>5=Zi;2{aeL4@emV+(XAJ-j_}#U0|qk` z_V_aRIs@IczPg%8dZ1@g>zg2oi-jsD+UUMR#u1jlxLNl+>umGt1)9OA{@m?#EhRs% z&6!_ezpP(qODxS8yFi2|$}&*l4?+*hdQEeRN_v62w}$K<-AvtvNR6^TR0~RHlc>s- zzJ?jz!(6)`7JaCEqWF&({?4QwXYSu+-)R|%X|XXUYP z?2Cf4d1fI&-4J4Eaoi?A4HO`vLX(;crLtBji%D;;sbWm7)kB2)tlEpVHg0b15_|(B z3iV*^BPk^ZXJj6dM+Bj{vCLwN>)u4ufia&l(6!1P8CX_TigLEeO=wLF7;%E_HVs{j zF3*#0h!2skZ%7S8ItF6{4jWE9z7ze{!9FT77r}e$)sEBhZ*{fNyMQqsi;sfGhtMac zrcT9z|C&f5hZ)n^zu10h>W_ZuwM0tQWQi14+;1(_@hz@+IuY+)vJNm65F0~ns&_W-LjgQlecpaN+ZtFeYo)2I0 z{|(TOVrMq=M6I9EMoZn*+@h~vqZsJvo$!sHIygKhpgWzBsa#)kJ7GJbZ9%T-jsCqX zU$|HjN6xDGOn}`P!Tf4ZG$E{<>UMZcYh0TH%@u@_Q~BEU5~GjRQ4)QkV8Il9F`Nqm zIq+;kEu)Tt*{Tv`v^;tfP_d9}2oQMIis4+b$)zV&W4VnZ zxb+~ag2JY&@eH&fn~)g%3eOa3;S!{;X7pOo$$I$s7@66#8^mY`9F%MUv^Z+htX(F* z|MU@&VP3E9o8}8#%y{x%`A2IHFeSPFh=!-y^{#6VAn??rq9CV>FU!v{4TH&xNm(sH ze9YI9xZ3Zy91OQv7)lC<9Y+TT-*ogmJW^S{ge(Wu)xRMi#rKlG%Mf54H6H!&OsB7L zdNT!qbgEw<&3{i;sDorn4N!~B)=jKeA*n{*+w=FI*xR?S%YstCcSRc40(7Go1&w%y}0$a6uKH5Wvw59I z#p>+IrVABf8{=cl=i~iv+is@V&fj?Mw08itZYfX^n^kXB2xZx%I{Ww&)6hRtZB;0c zpxaoyM%r8QVRk%K663NPke=0g(M1;dZfHm-QtW;W%%|@RiXOvqG zorE;EB!!Fj`Ayl0>$wP~ZZq{S8rVi}(a?%w1kI~$G>yDcV>|<2U*J-ja@Vpy_`S)R zwfZJ$)pGPALsBYIJ|&YMmv-K6f{>69A5v}%3wQ?YXbIDVvfSLCJ}syQ3AT@47f7`0 zAnh|74T&U!rL842sZCJ)5ZJ#&;4q5C2aPBBpn7Ro@PwI!KFLCq0*db8>Why<}cp*Vy8DCT7vXLkcI} z&nV(3`-uip=&cCnY7-H*b`PkX>=mk=_-*u}S6WT%awVakNRyBDkd-4SqN_KVM43ah zk0x5je8@!ZyvLae>4Q1R{I)^+)*=bhoz$OC-#i@S7Aw5}m)yq}85tk%m=+lmAh+j4 zxDp2&c2z&`{Sz7=F7ngF5K(f+Dx|-b#6nZqO#+fS0{GMsI)kS)?y&(dg(;mg<4H}$ zntV>)Mva?gJq>pE)M7v}jPOjC4}=EQaS)0VUiRg~p|*mgQmohU-qQ&b5y#PLEEc9? zH(*6hGGedyN#H;Zw9Mafg-|o=iD8`lK z@lk%z<-|~?-igVGT=2X_$m0m*Al)xn^c9d{M5$W z{H26bJhk8kYrSvBygqIuENhw$0yr_@;bet1v6oY@>HA(a1K5ZeOk!u~(L?Jq`~O2(i)C`6Tny(eZ)4xrGYBP&|A5 z<)G?e6rF>5Ns0#ngcf?01+h}J5{M0Z3dvP z<>{zDj?3c8|IXZ;Xpa5ah8cU1?ci+&O@4O#CfzgJHoRWsy3U`qx~F+$ZrHX~<%$ z;z7|u0Q9vEt$rQh0#I4Pj7;@`FH8Bi!H9Is^;rgcHBtrtP(=m$;{tgmhWC* zK>KxZ+^X3&{PQSW|D4W7hxY`{pRK~J7O4IP7gLJ*o6I=aIW*?{U=#>6CWV<2(=yJ8 zEUTh8fvi@FY%bM7VHW6>`#?hfOD>p|Xr6R~q1A{P^z8QaGPpcL+yvmfkzF0HVxn$Jd2a7?v<< zRKcY<3q4%czQSoxod}3np&!(EXdW|6z9h+@TIHjWy= zLPFKN4ElM}0Z~x=asjHv)P$XFQaT$lp;PK-Y5`_MINsH-X1TP(IAIc9dZ2_`;M4|??P~J@K&gUb2Z%-?5oDHF_AkfO58ET?C(jO*!hm1 zkExQ$7fC(9-!sWpM}fkF6i6|<*$N5Bmu%#z{AM~-8l=gBG_MPs8;@}!MT;T6G{=>- z4U10#%#XTcr1*wRkQ$1Df4<A=}_{PEw^v9qJs>}$<2ON%PV$=pHAJLzJqqWc1#=lQ-dH!wSCSX?D2u-W0Lc!QE z2-*BO*MZ-i*8YIu*=v!>8C8BSqR1#qA{&~eor6;?ifqslmWEhq`2y_&93gasvyU-M zfx=->aW>x-k%oBDTmLXaq@SFz%e(K1u|l2Dz)1tL4plcA8eVuzW}1@cwyUil6UzGT zuce?j9#2B)w}NDjGk)FsZs)Th8Wz@p-;xwv#TM2__3j5Y=(Lzthqs+&alKEtra*=J z1l|KnGOdLH?{6V~U3sL9z##U?9DbxFgJk#l+&i488|}gB2nR~!Yl)pQP#47SvvToT zxf=41!4KEDuO;nbV90{DUErwd^@0}&w)N~Rh-9S(u^PZ@$ms^i*cZKP^5B;uPrHDs zM>uo-=1&E5467rW133aeWO|y23^GLSp1z#F8Je#A9w)M+jww|qagqC~Sx#g1>(VqyhSmHlYW&j$Y)HHA_Ys>iBKXmpawrtf-;WR zAm!kfVTVe+Rt9eO_TiPP7FUibbgOhbgYV-3VHA;Z+sLvc3MJa*Y6F~m-3j=tN{(bAFty1}#96?EbRrV}5Bq=1$I-wR4e@Yp63 z&gI_YylkkfE-}9#7uJ6G$iUzO*%}g=(PIt96jSAnVBEcIQ=I6Qp)v3V&BW4C%i7dD zQ+XjvNHe{)^#H~PsH4S~J@<>~9={;OCKO+RJH;=1_HuUe-M94l>Ic>zv<@lmcpQ2; z76*I|2g$k(ztWqmZ!T0>c&_L^uRUYI)MQ*i9pTht?zpWlpuD=#)KM^@ErIe4^xN&DNjVvLZf(KbpvC^*WoGLe*p{->dJ6F-O zI-+UguofFooc_irx#MPzthb3scs%mo>BaKT>j8d1i}4?(Nmq#*-KUaqNX~IIxpc(Q ziiG$zD!-z9@2O)g7_te5wlmqD{AK8C3`7S5ok}(-TZqRri*2zAJyWqb8;z^1cM?nU zZYMVcEETA>nD|xYDu+@EjON<&LkQP8YQZX4w0lUNP!nI%`TI4P5X_DlzjH1rXb_Jr z<|zFQaC6fIxwMP62e%`dm5E6Q;2xc)e%_XrS@glF$drzY%(Z<7H3y==R(cZI344Pp zs&b-7&i>Wk8_l zIVmt>*C`QMHzr|HV_;#S8}WOZNK$|K!z=G-XBdTTTU*6XuGNEj?Mw@!en_4Snbcwr8gKUvz0=C=hfPTjskpxx()#MIU8`W0 z8zkfi(UFPA#>Ih@$%T1drt7$w1X3zUu>Q~0=j-+`f=Q((e^Ot;)H4w4 zU^CSszwF3Fs7lzC#E#fT`=3+Ng?{3$XEimxhyk_v+;Z-|#@QIM!52@Uv`Z;px5CB` zH2`fxrNoD0^x5>6hi_W*bmOOh!c(j_THOKY1O0X>hTZRlpFjJG+N>r4x1ScbNDiLZ zAIK_6^Grdx*U>c=(%BSyIZ*KhES00R3F;ex(70;Z0rKcZyvUxHS&&Yq&1wC_1@It! zHVY4LW9eKXl=Szk`y%cPzQm+(1dZ%I)PY0U(PobTLit#um$NaQ@ zg)QC_HwLk|5Z2$}c<{~l#VBt--!uA~fFPKEgv#JXxaC`X#;qP22>(_4dF5lV*8Y2y z!g!6(6ew0EA+q{U9_7p+K8LmB1~4uzbJSLsmS%!-UT^u|>i-Mq11xb5D%Yti<22ZdASMEHX8v zr9${qZ_`*vSQ6KUwvvfErauzA)q#F=}0?M8~U+KkAa^Wt)aM?_p_WO+`e$+s#j+->&gAOuA`yv@3O^4IR>g;VO& zl+2u~+pHR=qqiILm?QDoGHR}~v>Ei&+)L#CY>WSxqXcNDyqCw(o!{qW9BD+w?+`PP z_j#c;KksSu-e`N9vU2DhlTTJ%YkSRp(jLL4-(_HBU8e}NX8AVdDxtno3;2UqUWN1o z-_j4h!I1nheY z3&!O4Y6EJ4fr!lO=e1DE84}F1(nVcDCwT2Gtc09JE@T_2-wlA%0|}XN7f!VzsK&p( z7Ys*8uQDL;D4X%;;sM3LeH=)P#y;`%9(!F<2=ONYRL9JWS)yXgHla%$ovQ)B2Rf1z z{<&6kGPj?#2RELtuab_At{9QlR4rJO3Z;BsxSJvZ_JiI;u;Ms0NJ9GRmZ1*rDJNP} zp5$s!`X!{|{-30`3DmNsD%_t5x|4rwH==Y{?;YI2z1FqO+I;fY9eRnC7b}-MX>n>^ zM3;zON?f~II8~=~sxjz9rpivgZ~-NW!T`&_kb185XEkqWhxBB;&fU-K6`I4C<`72t zt}Ae{sMbd+M5^0~r`PYOohDkI&y8C9aQjihwpl!Zlb;!e3lR_|{AJmsG4%0(WuBxK!Qmx%UU0K#5RjJ1=tKu!`VT367Nc(6V3jpNWdbJuK<({(QP)(-=Q% za5`mEpE5-RsB*7s5^Ag3c3F^iDOnN7Z4cVTz@IZGMb|f#>gUv_(Qd?RY2VpkjY<__ zYD!6=)GT-a>AA({6Xs&twEkUuy`Caaas9pDoFh5Z>rU)nJk2NH`|(MAf8ow-ggK_u z0OHDF7biM>N<2lXGAQ6^x_VOax-CFnxcQ=3_C<}WB~fSnvNPpg&b8WkD7H+-Xw()>wC~mreLBH~qBE4Il*d`r^kzf=|ugs&xYWXjxVu`!Fx@Jc! zXX&=cc?j8V*Z9^p|Kj88H;TSAL%fqlrnyd`xDQ2Oby)WHlW8ed|13x@((6!!V}s1t z7wYVXz9x65Lc2j#SbK4v>6eIGs1B=lX?K5WBJ#Tz!quM{&n@K*jE@_Kv|lr;j-&Fm zwv7=0CA}F6W60nE%1zg6DC>@&m&xiXE5&Ue3?cui_2nJR7-nlV4kBW)5Z&HD9{iCW zRy)NUZgX1q1l`=TxVGXj>m=`f_sOI^=$#W@I_JImPXt*(U*cGkt z$-cU_5p~v4jK)8+wWU&PG7iVFS$NZ7krK{K{sLeA?pHt(l)DaSs^+-wKP&Jz8m8H+ zv2rdX{KUbAayhRiot41+3E`rYM-#8-{-k^+99b*P~6HfCb#nB^3Z0?k9h zAz^J*x5Z_XlyM_%ZEdv}qXGtOGl7+;K|vd(EipPUkQpRYo4;^+dXmHur2p?jBT0{# z&#jGJz{=J^SftW>ZP<` z@b`#?i`CGVSAv{)gX$H)hp5}$2^OW+Q&_eD|Lj#~R>b3Eu9uaU4|SaP1byq}Acet>^2fJZaU6*6f$!#E zev2VC4uS&qmt|M_i9ME8boDLj>sAAHse&2GWlnn)LWBM1A8oJ_zG|&&peZb@AZYQb z(1^iy^i^dez5@Ex4>6PAX0CsUhw43Z<2vwVQ`d_<_}{#->2`OyHL(1R@Cvj*eFSq_ z6&l5Q`0VBPVes>WU%ohqdW}{wv@*o{fm6Pq;J#PLRm%qH$8z+ma*Kv?^u&Cv4JtdO zEie6&HEY4bMbo0B%9T9IFFa5*XA-#Z#(=vFdX{_SA1t*A^e%jOU`a8!Ffl$}(aAyu zzo+IzSd_%y@wK@P2x31=wm8&}4ho=N)MCc&OmdR^QZ39VYr>KDmyw|lqyBDfUjAIy zQQG^!fL!)NF!!34;s;eZrVVr-*F0wwFPRefxGZ7Kmg^kr1_w3mpJ(fSAhEx1F{y`@ zdTZ1p{*-@jV0Tv-{6Dc2Q2`toT8LAomrN>&-nTYweav+v`1jZt8k}Ou%2(f+uV49A zR@Q0!_;K7wfQHxHWDaS|4Bf!M=%rO zX`deCGx+6^4UG(ib^CK>i8sV1Xth|B2aLzQYx^n~Q~0e|9GNF(UqTJ5;PWwfh{q{4 zkS<3BJm~wP#(ke7`PhDKe|_?@{>*T{{vo&ro4)^kiJ~3=W z(6g9lKFo@LXNX9OA@Jn7N=i98`(qp)&dVXn4oOM*`PW%w$9~LHCA{Z>z*9Tj->i6- zwyz|9nOY{&!+#ZHh?kl^!kcby7~^XKrT${gD$zV&uJNNsSx*yJ=HMfHpk@z6d_YYP z6Yj>ZRX=!zM~ycRBO(0m=hS1U#QIR^VwSXaMUW9uaKmg~LWW}FQ(Nl6_?z$%e*MDl zKp>;RurWbe;XpI`UJQAWK%}~)RdX`*VDokM7go8bYQlYLNIT^ueTXkUQXfuw&+}et*Qyrl-Z|AG*CqQUAx%S%6j5bZ`G0 zTBRhUQ=|l>r3C~8L|UY~Q%X8SR1{RYOO$$~OC=8=-5{k%cXz}27Vr1>dVoUB~k68CT1GP2l zt1%*#+`CT!oy9K2# zs<*pyDKnCQC9tr%s+<*DL>&>gwY(hZxupk`iRkf~?1}BZh&nq4<=Z5<2ohZBSG#GH z&F=_9(5Y#aKSq6&S8@|+f;c7=*mLkNpCHun?kdOi^c4(%XTJ8Wj7lFnbmv8v_dtkH zIb2!RW$(wchB(YIMjK=p>5v)X-Ohtb0ky1)_EP{p}+bZqZ zGN#ikX*ICT4aF9ESjI>u#Lfn*N#wF<*gg|VEUhlrJzQweI3l@`HC=Z;qS6MV@g84Y zXZtl?0_lBCjOAB7*_S=CQC-G{hGgLGj|*cP?KLHZpBOBE8_971gN5NhgFaSMsi)f3 z-agv!qv#FBCwMaiBL?P4w6d`rQDL~p8J_9_5Bz+5^r{51CPZjy1(}ko;T3lTG}PCE zgI;FyE(rErsBolNV{C;h=xHLP(M5oDW({rU33XOKr>JNGxrF*L7yRTF#)3LN{$*U~@M#zYlyzKNAnJBLG#L08KRZ6+dsZ zRvlE&FapXXSJJY@1VArGR5M@APy(N$qzrx1YIXT6JME*4EavJI4AUzI4J$e~ia>nh_T}xaShrle=UBi(0r; zme8|29od;LZBy5D#B^W}as}`kdEMrxySvd|3y2>-ew5rr?`4BeA56X_81KsPQQ?~X zxn>(O98&w?oDnux3(dzI;Jy@zvB|@*Sp}gLb8uE#?(fD#uQ^TST+o@t5LM?BARL{Y zRd33^!DDhjghGzBM$;0!St27VeqqFABY*#{87RUi*>=tX?K{ul|KJE&2^Xk9lPV>v zdYeoWL&GbBR~gae9{jJVr3JeF;Yk$Lcve;KA#A8n97w{iiXhwz&OhD_Ze&BB zfT*Q$`abhrv)JL6BDD&eyUtmgfZK?L_e=cXr#-KoJ06^UAhS%{hriH{Mh3HB!fR!4 zxPZVbbh2+3f)%Q6WV2v03h#5T;=;vsi{zbs`|W+PNEjp6WTDW7^a z96};Iv%aPruAvMrk^F6=Qr?gXWlQ0V8b=S}*!gBGQ}Z3lCmh`RSeCU%DUC4s6^ePL z9xhBnRsd$YB&^UnY<3<1$nL0Pbhf9*IB&s(iVLu;1#j5cHrS)T$6PqNO|Oh4r6zz%1TZlJbeCgh+BRd*hky2(CN^*#5?( z{QwqyQ&y^)%c!$-FM0&qLZLP2_I@qjfRRtAWw^TXT~;ymsynFN2g=W$C4BWU9btYY z9TsJl@8Rj$@eVVXxxhpBtwk>KD})gvEcHR(TSDobdY(jLv@sS>oOZdnxnHjTgtQhJ zlclNyn1H1=*ViqL>wL3Cu+fcK<=2=PL#M%LeVd5|bdTI8Dtrqn_-vGvIugMrdwcFT zQ3+t5Z0raG%EPmJ4Hd6HTklH{78Z zv|oRN(JA`i!M)_K3ga7W#pll|_PQ1a*v&N!at+1cU}@Lwgp11#F@XB;?8rRl+qbpJ zON*_1>fx&May-xJVT&k_wt$8S~!?N_mq9K|qOYi^W!?NEG=N zd7ad(X;CbAQI(8Feo6NKo=dbowC!|p0jFc?+vH8)yQXXYM=VS4zo-M9etCXQ3ncH} zzP^Dg>_k*<)Jya#{lJCK;D2qI27pM4fx!OT{mUS%GKS?M`}kZXx`*-PC@#b_W_!^P zWQ4yiZX4Ue6Rf!#&DWAzQtAImQ{5(sn4d24A?wu02y5QK6ta8G19?rFwO!#r{}S_u zIP+421|34`l{6Vc-Iml3PlWF9e+A3ZECRUFw|1DXDF){L4`S5yLA!`w$o}Wjr?}#xiX0cjDny?m8%7P9nmjXbiS5XcGwbT$`n;zOC{3u@;Xv66jlMKNjDf!AN7bX4vUY; z=sZg0pxZaLJcZJbvcqVWPkwtT@BSqnYH%`UuYRXrxV1F*oq)pVFJM#>NvGG>@!>vw zNcA+Td|OXWL_KdxN`bVma7)m_m zX1m$-b;pq$g@8Z5c-^u@V!uQ{=U9fP!Da{cwE?StkvHnW=fFT&bM$_2+8{-;RJ6Uy4-W917`ry|NCbUHdEj6 zko0pG|J*MeDK`GvwYza3>f=t-10YM$uRHB)K<95$Heq&6W>#lIjgM0`xnbl%iQd$o ziG703Y1&jZISm>I)Pn4}U&TJfoNnaRn0_hio8%3VVU6$(47_ZY`6T)HUv7Gu)bp*d zB=f2|Qk%lO{7?62O!XcjE;p8!i3JIpe2j3Cuo#<)pc@m|UdoZ|#QCvS-P3r}j~@H)UZuBaM} zasF(v<%J6`6k;=71$*uo*m0}Jo{<7DBKl2+&y9y}S-H87L?tDK!Y6@qP6g^fgc=7o zNO*O|YP*xWWj?+n&T1(yWdBlMjE!~!K1)f@pF)h|!J#36IrZLEg1=;EQ?hDmpS-WY z7>Wmnhwq`Jx}I#99LE!w`<54ZEGmqtBadnxV{0$3taLb7I=?60t)q9b5YV%^{q_~( z%MGLPMXAT|-IbAYorXdOH{arux(5y&OVFn-v1ypNQJ(-WfVI=_KCAF@*+|&NY4SCG zJzLPtfY^zDm#{Mfv;T!cHB(J78G;m^S2yXiMRfr+O}JF7+59#KC+6(j+}wuV!&;hZ z8zSG-V*Z!_pN5Z~BR4v}8`i;2`~foa=it2$zV1}*FR0Zc@!k!Ha^QZonCOuE##iZw z+VBzl`+#dq@mt$t(5n0;(Rpsd)635My&p98va+66>ju5^G~|MQAf=U01^_H1|J3Lw z@2^CDkW9jnZRbm>W) z8J(*j`4U;SF9*`Y)s@dmtLV}C^0GSlO#r*?zIxQpQk)jwH1~J&v2Y>V!Zu=@}12YLYc}}Zns=^&MA^sMHDl#B)#KpwKq{{#6B@18g zurDL$>~4(Yn+dl_Si@y7`YTU!d6%%)$XGlG#$k3$3~BBXbN0LaD50%%;F3g*Pm#yl zPxiIWADq*y6P?V@vuw#;ds^YQ0Z!Z&dyK5cEVrgWxsQcu6vp-ke?!g1?m-WGUD)ed zW(z;O%Yh`*8f;g)PdH}&VN`W+LT}>a#7B?eWqWdM4x-(;3lBA+5k1l~v_#nz)|cFE z`*CM7(offgs3~PgfcWjLA)63HXy*s*!TuS#Su(u6!C=q9b|tW}$JngJ?R`@#4W?|% zW`8B}3C=I) z8^KDYgGROF@$z^aFFZX#{@&m|Xm@+5lCTLvn-kRrFxJMu4v~0Apv&3@$N7V=GtGai zZ^-(i)q#m#fv=ccaMRRc5o6@V?p^@>z;uC76!L~_cfRS+gOqAo)`qO6r>9?oF%Bu1Sr@Cebx6}If0YGM&8v$hu zmTw2J;9Pd$Z2h_&EVu&eft~Yn{}5y`htR_2>FH?(D0f4;7ALa)C0U@#z5V^WXGX4V z8;@qw#5#J(b(-QzY&=r=%7GZ0%lYxm+5lw(!Pj%p@sgJigkv^odbck}T<32NXB>R8-yv}~FmZL`VLX38MriaPofQ&3h`JudLV! zuakrcC8H~3rU;k$eXd(e?k5IbC_Qu2HrPOwSl8c@fEj%FsDB>$Km5sW*Y!>XUv`FU zpozaolBGXU6I}pd;PD_u&RE+{BDd-qzTSm6)BHV|{?So3$dz|E?gbW>&Rz_^Waj05@71vrDn($SlvInT*aAphm zr9cEA`gCPDR~Up(oUC>C26?2BEni)55ShTOgQRBrGWDUVk%>tz9#aggQ;*VJ0f!)r zyg~b@iSOZ);2aKsFb>oa>;MmB*22Y~PuB!!YwU&ZTHlU~)6&to@+2>S;L9O)Fo?ER z0LG0qi0-8FD8yr)U5_!*$&ao&f8e{HH)~EX4=j`bJBC~qQrYXQ?(pCD@G5Pkypo(A zq-=Mxyc`w;%#jGBTGJIFN4-m%23rG0P|TRvlb^P_@1TO4dL3-q3=|Mfs|e`vQ4Qe? zh{l|W+z5sYUWe@$4-nb;S#W$6P25EYUWWe}-HL~4I~=I!{_1ZsTKw&{d{)3tUdp{N zK02BU7jCz87(}gWnKL^$hf<4?WC(t5k}M7kuA}{ZJRuyp!90zPx%&G0%v6NN3f`M1 zL52KZZyI+C&%yr{7xx019_#J~uQ3VR-XWJo^ms&XOdb@jr!-u0KoeD#SXfs;XtwFJqn!(Zb^K zE_`gA{Su$%kcXQ)d+f-kCj&cu>4{d<|NI`~<53*h-Uq938?W144V5&=k~n_6nX-v; zBZb{HFVDMr*Ef%*8iLfSDk>C(l`Cd({0UPaY_UM&_E6*ftawH85NfZ{<;Zn*kDaYn zT%*H0f$WO*x2J^C@8?8hG{`D*O35iR4*y1KXpkBIbC1=mL_h4@z}{UR_|geI9h*bj z=Qj}&pbWT|=OEPK%X{^0_dy|hrys)Si7C)>X>x)~##JtPJ%Sth@7e<+Wqf z-Di2Rh>V_PffrQK5V`DbfNIk8njxUA5(rZ7dYMWZ{B1zF3m0W|_(uZh>|#Lvl`J_W1^aMws~Le6GU;Bh zm}~sg$Hj`>f&ck>Z)M0|$Co6CAMc)&#IJ zxp`h8%ZJa!}aTzczCZ>+7^5NIvlH|nJVjEEiv zVb|Ndm@w;JG_eQ$=!;G`%Ptq?y|>G+5@=T>-e;+-AV8VQgtMEId3P;Yi)_K<Iwi)4n?4%rL=WWP{TQ(M;JLIZ=MLSXEU5DU_AXc96Qx|9|EMF)4Vuo zx6YY9A{eSa=)J=0o88{tRw*2TRNe` z!YK)7QZB!XpME&p*lcd;%34qHB=1Y6(yM5rK>R_a@H&##a>7b4%2e^2cdDd>E()VG z&0=PR-Mzh!|H+jW4!fkaUmKsV-3DjF&mS+Vy0^K<eH|cT{(=-2(Kp9FTT8Sk(l6-Z_FWu_ETgD)wNnWrkh%GKk$8&zC!mik8MhI z>EvK-)Z|G}5|8m-U7u^J8-SRujhIAS_#DFa@E~}{=>N#9I;3ypJdv1&kqZ2b@x%G z)}>@S{>QZDh`;=61gi*Q2W`n2hj6vxujgR=?Z8QVhR>riDdW^w5TjlBR{hKPV%>3Y zdZVcNeh*e?T$u!{O>pDiAIRSj&WmccXL*KL%QfC4m)9r*MN3PfoxH{|^mzGx?+Wb3 z*$s2Z;Qc%rxOifHeS<<4PYv%x5l?BYKKHT)U;4sp(z4bsNC4C2zhTZc<@WPRE?|c! z4}RDB!+Z$qiv;Xbt_P|P8LEFv0?&qL=v7*lzpGA)e)=0mn;07l zNW6cKKG9z*s7N5U)x_alH+$eO*p8oOKJdB;(-rX*BO1wJ)U5Db@N8|gIQC^PE5`BS zQsBO=_Fi?j+@zC&%a=_m%)o}ESc-4Br`8(uAKa;#s96EMbJyW>hQ9#&V{wKdm0hx6 zA9;;kpq1!nva4LMt?wAEn)u}0TbMO}1q2vKMvHDVkjk=xX`ZXiYOZ9uy&)L`K2VYH z*_9g+`Rp!}P;1_(=3Wr$)SsF3jP3Es_BOFXql(btgLc+AdBh!VROkF(?O;=jPWeb9 z?ylzR!qat8bX@yKM@w`#&fC?daxec$=1l*L3FsPe-h~QZj*NTWmF6!$1!$H26n8oS z_EER52W8G!Iy(k|t*`xP>#TtmDk zc+^%LQltEZGf#iZ6Xs$T;fi~CdoO$3`--W_3I@hr>jIB{4Z%?2^7bKK>eAje)bq=8 zK)#R!4_*N}5EJdV;iXe=+m9+*e@4F0r^lPLr+dS3yM;|Hot-CD{MzunFoqY+$6tKU zHlXKQ0MAh#$e8~_fuWoPp?3o}cN8B-pl%Km-o@h$|5H~EfANy}_+AQI_PaN8f_q&p zKd|20h>3_O8pvgy@ckMtBhrGKxjr=9fa7YSPZbdw$?^8UoF14e-^oST?f zb3;6|>eYx9IUAva;5q+=M^WPmD?$9+mJd% zg9q2%32kgNCglvf0^1+y>j3wY33@k$1sdoKcRNdO2cNx(i* zdJ}Mp+!wI;Nl9uGKAYRR2h1`6pfnVGi~B@Ov#^Nh2IvRF=GImp{OgWf%hW?mjsHnm z@f1aTgTB#X5`g{+fxzIO1+gX}5vxNFqO$POxoW^dbx- zwAm2Z+IaS=X&4V#Gb$F*H-IO>-Qqc!E)}3Z*keGA0OnGhR{6pQDf^PRQF?Bs^w$*+ zgL~GG7GSZvFxwQ=KUXu+_BoAiSYiL=koQF%Ip##FLlSn6NLBdw)+Q<_$Dqf-!<0Ow z7PAkyJ#KJ`0zBzjrnRr@167YlanSM=LDV0irQJ(`4gac*Q?F>!YB zeo$rW-rDh+cRPgq{sf>oXrElMU07?oTOlDhTs_4whTfHGEbD-eadUo)+mh@w+6ex6 zl+t|M^BMQ|$3bWTt?pp8lWp&%VfOH=N0N|`aGK1r1nGoD*B)b@O{hs9QJd8|Hs`wL zx%4V<1oqHvyM4818)nP{3`n-cbH_>JFFHaMY3Nsb3T--MxG#M{XUN5 zIJ3he%vU_m3gHH8yZ_;OuA@369Z2rvQ|=Fdr5SoR2qQ8GfJ>CITvyyB+N1L0`3_Q` zSK*?KP8moWQ10KN^+G;$D7QoHzhog|X{-tIM`HW&0&a4|ajcE5eUbl%{V;kdXe_f}L;FhNymj1vID#geF52(Ebn zGxs1xI@{6luGmJB3>O8xVv|^V^2;)O7sxtEpqK14J`9jMSSyEo5|)v{kaS?@&GiHh zgaGT>?F*orFr!$1f>Ra#;hRm_*ztXl8Ben;6^WRK1~_@$LCa&=-+(c_2E&w7N36W{ zIX{;f*_X3q3*SerkNUeSl1E&&jWL`BSblFU;CmFGzzKB_WT}%TA2QL)K7r8JS7nw; zKW;wR0h$eDtggP%3q&VJR%IMGg{aj{Q;)Wwv=DUqV|JEmtvo^mTP%>?TL5{2>4~up z7ZekHed}PCfQF<7W8hqCm8MR-UbbPkx!{>^>z#3aFC1N|rt<9K!yNu`;30_B`c`-O! zZ2HpwS>;zA+p3`cdhcOTECk6t2{jtY98V?A&h!P`jqo3QM9KH>Z32^0qPk>(Amlik zZ3$PFCVbxsqtvm+zGjDgH97OSvBN(wm$%=GhVcCr?wH5^Qk;g20Td6A&E#TY+1Jm6 z*Edn?PR6i-`5dUOLUsMub!Vr5mWGy>RWvE9pu4j z$rm-aN#b-kqJfB}0Q{Vo?>dzG3kmh@iQl-j&CCBs_+7tc#>nK^{`$DU4ulA*jBJmH zk^Q1~MMI9NI$aIv>>C>B&V0X)X_a@Oky$v5Iw)G9_4^!{Y(A&(^$fULsXbyk1U0Tv zwW{&wZ5s4vVbwH}TCMmyR6l{i-D#H}ID9BqPTCyv3l@N5u{6gUtmBA5{N{sM|naGU0mj zaFZt>$_c?cX}!x6VI!1@IgP1OCP#}!0jf#oNu&IsFrU}*E7k+1`|wDvT=kmridXTQ zu&J{`)kfQnW;!W{Sveg%ChYRFL{Ti(ieAirb^LWujwY&5zGW*F_8S83yRFuG@0erE zOg^gW6Pi5%7S$}=i7u`xVqm#IWV)f7pXm!CsenIZ|C~aXOpw9#;q!(yE>hWD?_elw z^X#JrscG?p9zY+>LC~!qdG6=;m*}|IdTZXP43n*pe@gu?0%1CF-uQ)oT9rZu&rhLKf1H|BPJYbCK*2<9xs7W(H@ z9PS1_!A~Qt6z&>NlvL2AA+X$IntA$UIMOUUD_>;_*7yr(NItG%>mKQMtV=5h8fy^B z{0*sHFN6uf<;Kk#3N5}fZhFP4j{Aa{n9>mbfS&B=Q5NdR{FA%)yh6T2@uqY~udepi zYiPLlRsO`AxbodVM}H3mldPg&er`!gJ9FA_svW0X8Zva39ixUFVo`Q zKiyuFC_b@k)8gky1F8fo#5pxjjI(cKW+Mvy<9GUDl21CwSDOHqAd&VCc7pk{e;*lM z(f3TraXPIT^ZV+RRwCo&J&v8NGYuwrg1MA0Y3i^dc~&pQ?P)JQp+jh1Lk|S0c(mCa zx|}>KHs-(?zyC&PuW}khM!k~L2`{&@DB35HU{$q!JveBGVLRc4E0s2&l{lm_srU0E({8l^$$JykReRcJUxLcxs$M@40?l}lJ*zrqJ46# z6&uX^4L=iI0)ArMF*WtqcbEY2&ht>>1M}iMjkD_ICmWMDpWjG-508NM{`HRD-jAns zvHJAknw<5%2g((R2Cfi^24CL3vm8LKqZfh7$fj}eg>pI|uem#}u%nDTCX0cssDao= z5g#G*C@H*34KKIEK=~#VSwRH(YNQu8g{TUxm`Wzd`pQIH<)yhAC-OrA=4iVQx z<&+^CSpZi=hK!ByK=h>ABnCtdTd%mnv9lkL*t`_E084^u;8_~$3HYngsFV2mII zVuR-b7LZ5qJ$Q6cgdv-xkn_aPzn8dNF<;t2y^t2`^)4D6nMPo?%x>PU8&5$}i-ie| z@Lx$?T~E?>EH(=26fD8Io7=9;MWxYw_zj?EMncfnnOI!1y&wqXjve)^Q#P_YEJHt> z0EuU;hr_RaIyQ(#s&1JhB%>hLw;Q3}7=%j@wB(^jiN`21#!s1==$%0rAW^Xk(~#0D z*K*d%|5#1xe4h^Z#;Mr9Rs%p1r;c(TWXZkmqGlkl^fn~J)*R}s|1vvWW2DTHuo9xNiAWG?5cB^NAmr6w?=S;2Tz>R z7j$&?%lFtDymzuPxgX4sW|ARQl95u#h^>{M^d39vTRP?L7f@QVdL#(a(P2hk?X|t` zH@VZUjPfwVlkx|zXJ_(L3Me$DAlHJkfiZQf*1DiP`h$$Q-gPF+CWl2v1gOIFwrfaetFZvKZ>$0Q##6X6E{1@C8x~sze&!&4+^*WDsec$ zS?9BtLmV4O{)UHQ|M=EeLP9c{{G6iO=}lMry7R><7TRG){mkTz=h5Olp4BZ*Xq2Mm z`e%H|a4DSrF*2lIcx;#q!U`rimuB%coLSh&ICp-c1VC~183c8|# z0qg6-I7NB+t0I@Ds2Fand!$vkfdXDxI0p8ZJTca8&7xgp?GZ9&U!yoI3= z2TA`2zcpf--$ri4sz)RH!`8H#f<8XZ-S5NvOG7Il$VR~Y81_<{+ua zS{nnLYy8h2lBAjc`h=&Eu~61hN>lj;JftvvP)*9-&&&2t{0FOnD_+jJX@ozPSiJ$A z&tMDf*v^CRb#-Y23R+DWm>ez4Q~^81m?IXRpLjF<2ft3q{K@UL)jQ;6sq5MOS$&XJ z_;fx9iwaSi1>JcSbMVAS85JCCsI_llN}&q{E)Gl>&Qze{!wK#4s=qc5rXL_`efAU} zOoYk=wVng=20Q0Al|nCOJ20^;06#xAVOyTT&uZ-QBGDb2bQFqExd@rQ(&36b_ zV6xkXX-_WE&uyrCy0@A=D%QM@)5Xc@DJ7%nQA7pymX1Rt6k+VF_t|R=pH7*C#-4;x z>lcAF?X;LZ>q@jh&|86l(zu@SXck7xkQkMr>&=ttudzL=fUA4MA!+TI)y&I6s?5$L zLda;l>~+->03qABCEerfD>)%yVJtCytZ~wFNbZ~fV_M)%h$A>U26_0b-2>CZ7j-J2%x^aCjrgt>y9Zv4{(H_%ipnai6lD1IVWdfH|QCrt!Fu z41ml*m<9y=Ba?XMNs7^&xC|$fK*|Q~>$P_YahhQ|dw58&`6H6PBTEBRls%EkabY0QsGN#D3fCOLA}a zI1b!gaQNymB^Kz9UA83~E&9JXFrV%drs(&?0dh;#H<7sfG~hxZcmkX&pU~9)lPQ$n zd)#CNle@jOm8fWG*NIYy77b74hi)<=j&~0?3`P~;8B9(f72p#TB+I*PkX?S> z-%qTks|)(=PUHbRF7V#wxL*J}x-frJs}lwLYiY$~j^kQa=3fOzZoB&XG+7#Yz5q6B zd?|5rsy-lPV8i0upFcrqev1X?gkHhBCViD6&)jdlPc*pPvTT9~c19=lpgpDNt1$oI zUIWmTm_z-EyctyL0pd67rfTsJj(g{3!MmFT0*LOL$Dd=oDo+cOLiUE|r`9{mx5tt9z)~zFO4y*X4 zrn#-KIe!&qq)oF5w}+XuHbPriSlIgso!6v7=6kNn<&ZQFliwj|N4jq10u>iW$GkO37cCpY zWZM(FSB|fagj-Q$;S*gYc&B#Xy0PD$V@f+)Sj?p)x3(VE0WXU$Uuno))`YgRAoa;T zvhU~LegRyp8y_^BBFHae-V8}mDThh+Jt^C04yrFaBsQIa z$qVCeX6)J(bkn?=i{cjAQyYWbQHrhE5yNon$QpAW^_X2@A4qK1yY!bVxV$XCAns*& zg)-#IMO~7uYVP9Pws&NYYwg+arV&kyz9WW;g~98 zMj@~CA*=lUvgI_OSgpX@#lk91tM+EvMplNFtllh)nr>vBqwT zbSeqPf3FtJ0~%1Gc?k^k^n_0CdlL5wii(^>6QTs(SYSLV_0CbmJITTx0~biH=bPgQ)&2lMzUvhl2?~3< z5FyBnwu?1=0s`p%g#7|moE@M%LNu#aO?%#}W>LZKY=r)JcKI~f*uG@k1k z97$47AsOwaG;;U4LL<9jIMTAcNxGfI*tJWc(-7~jn^U~mH~N3*HNL#RG^_MXp_b=Z zI6vpFdaSdBqx~#9ix4OB>u=Yz^J+qC>ACnM zF05E=;`my2EdMsq$~3yab20;+o$ookA(>oIvNex^=)*=_mD>=VX}H|9!u=5?hnMs< zRv&m|k+Q_T6cb_yT7(Swy~)ilvfw@la=&#wb%KBvSNxC{&LU zOtG27cz*d9_1a0_PPRQwkcDfzr_j>X` zNwlzrMG`8a2ZsiQU%ul!=EnJ9BPUT}JprqYCOG64WAnWOMCx6; z9ItN5ZLT*?s790Ew-se&%nqaZ19XW0K9&PqFpiXwcYEp%IyEYAN%O&t*JwF5rFQen zaAB*MIh7VmY>WJ2ZSZV792mlgq%fyM{^(wb`k=KBaGPDDWS0SmJ@68&xXKl0;yp!;(!+qqeSIsHZ7h88Jb zQEq#Z`DMYajEllBr+wa|Nze&n7q?O=RD?#8i9^QfTdcd}7(MHUhzLAziJQKsT2eTn zqN4*57{G;o-&n!?doNYS>%~p!9ay>*jhuoW<$5sa=XPwK2pAhP=*eCg-?L+Nz_dz^ z3$8W5($V*3;f8+x{JFMo-~T3R8)nfVvC0!jB9616t@PrjF#9`$g~IEOsH9u|TMA7c zo|~o%nPym*HIB0qovWaY=jtj){>G@9e_&h7Y8%Y`In-R5*`BnL?zhp!!Dmqf!qnE* z*4oLbr!3ScthfKifFG%m@b}u&K$WAKB2&CJaI1iffBh)0-y*Xw;Z-~>EOQE2FFb>H5Pjmm|m zY)XL6Q~JYL4rsZL*m;eQS{0>OE>z;JelC~cyW3`4LwFIve0w0dO^X1ASj)n~g4sy_ zGwz18D$cqmcqcg0AXOvix#iUuWaZ=IBikI*$!Qw<;q&gjJ8a}okj%gokR%;|#mHT2 zKs;HL`>eAdY>?>IvQ{gH$DSogGx$2qQ;Y4LlcSuT#Q_stFw7oiuC6;VFU{{^Cenf` z%l=mk)*SWDAOMX_=j7BKT(6n!Km=pM4fqUb$ETKhRciw312npUWE)pqf@xy{3^iqG zFsyz}x?N&U68Eiy+AEr%B;7*!t&!+U#*??yJ2)u2y)2H5vfq*DJbX^Q_zTVqr3#}t zOT_OxQ}&`HdhvH}CLm8qrgcMJUCI;a*`r=+r1{#KrpgfI`eKxEGR2jIFVLHov2n4X z>XK2Q8fbIWtW-Fs#bjrm12HO#SmMx9J+$+7zhmH&>EDJQFs{SSbzrYw>WNLT_v9mv zOgqF-85ge4i(ND1X7cG#2CbN6EN_=ld3|=!%Ox&pvg%9?QO5#y%gZ}2g5p<|F1yN9 zEZiCrS6GkiFKPTAl{U{b<**(OX0T_M9BbF&>4&?@zh&r{KVmT4jVkkLV0;i`_I2-f zV`I0vGPW=-;Mt+XWghnNCv=toi}DKAoP16!XGELI?PBb{J?tKR%77~v-(EpMTt4IP zKq&U&;OXg^u9;lvosQb`XOg~@;aC8_JA7w~Bbzq6;;WHK`qEhs3LJq?ojpB;zC(%R z&cP|~6r0zL4^kBbz~$Y09IKQAWV*Xd9Wnu!(~hD1W20>=Nnxu$Rgd$BUFvEdr3S9; z6rIt|1d^p$U;CMtYwU~ABUNzIt1=L8XnmOGUvHaCx&1`BU1{1XmY*ps3teO60JZ~KMo;5o*3Hk*# z@AhF-&1&Z4FS8(cf@?l6tFzu}t}XTui<0B$xgtq)p#}2BQ)%v9&WSiRC+ap*NWsP-p z6H!jTObHKUp!8LZ*VcSo965(-_JZLivg6CS2EtNDN2e26ki>S;ntSfi`M~7vRV1k% zYRrL4c`q}XWelvc(lchJrKnQT5)4 zXg7s9@pqQ6=F`&UNaX#16-68kXt|i1sshnltOsAR{!9-pFE3+}{C5}VY8`+^V=qx% zQ^Qm&|I@%69Q1)%8057>>ow@aeK}q%0|fr8v-(Hbm~*Pr+LQolya?4?7&DLP>^ zQ%I7DhtTfvyqpvs<^`r>YGLdkkwUc$h!~@GdFC^h_QWM5xZWU9U=vW8ts;9g#?mG; zFkI)C{Smdv*p~z1+2s4Yw-!WjTtvR&*O;K+O-nFmN;5s}iYVOSkkBNQ{)qjLB~~!gnetMPH;F-X~<>T|~ATOM`5{LD}B+-L?G*|t$3ur0pZ@*j{3#%{fus~MO!&Kq{=iDmqfxAh2L zQL$y=d*EgzoSaPjzM(FGC?=NT#IlZzrZvcoY>&re9)gp}*Hl7fcz)%_#ltRUn(m&q_I4$lf3My1EYFD{@AB`jj9MaUa)W zx>!(3%Gh@QNyCpHx_c@1;7jR10A(k()rr$g3@e-Iw_{=`diw-S^Yc_e&H~w+l0mMn zt~m-E-(>o?Vc5|=5=~V<-xPXby@=r^g{PvX$sAFoT}7}7 zGHtDdGv*UbWN+!SPS950AbM&;Xz;0U*nPZQl=bc4Ew6pc*s^Tn6EDBf8l$PqRw89K zrcituWT4ragjt_~IR)Dfx!b!9&|3f;!zoY$KCi6de%0&O*a4p?Xa3cA!FvJ`bwd__ zZ2Y$c`GCUKVkc!FyBlWtBsSuG+fIGc7%~!DU9r~q+MRjb8DeA@xJ~T^i!-8iuV2}b z+_giVGzTU7c)o(-#$%|Uy6rxmS|F_f0`8Xdd(G2NdMC`X?F|UdRr#MKC6O4sG;Z^W zw}_=ty<|h(Jjx*BDVLpoplnZe`AS596pk%AvYvco?_a9YqWDgJh!EWTy~mD`-#Wj@ zPB0s(lp(xBZ%=JV_i|Y#^SAM z9ly-;V7Wy(H*30Q-mcFR;8o0j0)+*c*`vP^?g0$9z*4h=FxddVP)T5D)#ovo@*{ztB)|Y@2J|0W266g9phw@wEDl< z|H$zw`gS(Vr{wm6qh{imScuoYdKr-(i~R><`jVq^xrr8NQ7X0*O``LZv67tm5kdm{ zbA(4UyC?nBkxSV5FNL7IF9P~qmD`f&gWiV58p|D%9J4=*<0Q^B1JCGp=~daIL<9R4 z0Wyq#S@ePtlO0xUs){WhBiB6~J~Wgq%x z5W;c@2zN*7hqjgaJy7xm7?`Z0KRp|AQd{UpH}~?P;6aPw;t>^G<_T|~Z>2sgs;qAKXh9tXfBezn_2-wJt_gA1hAoM*6Vo54aTryOJ@~dR zE&wM=>o$D&H(<(w<*0$}F3EVcgQw`!QDILSwb0M{W+;OlO0CM4q2}@93X#T1{Lx;@ zunyzbu)SxMeqrEf9Z})^hgV!ZmC5Giy*l`T9wjy0aHQhqyQaU`4Mo=;zV$P__(d{d z-f8%+LzUI0Vz0)&)cCYg89(+^Z^(Aqev}Jzi?NDO={ou z7SVWJO8Z&sUz*bBvyT7d$~LV?X`=T&KcsI}e=Fi)PBAKuz9v+ln!fz}+Gn%Jh&yCN zryyw}{7yIbv=g>hZ+N2afBL(4Z_6yEb7W#7HGl5lxw2=ev()09*ME0$%YQsE;ez@I zeJpH0ct!`*1T?UvnNsg26uGqPrj#qdHHk7a4#p{yXB;JR?6h zQE`j^kEFAXtEzb#_@TQ)x}}ju8l&9SfqO&X>~SISXr`ajulvre}!`SbhFJ2-Pbd?6phmjG)9s zO46mXLIS$S+FWC!qb!q-Ti=h|{%z2$pd?8aIr;IP*!M~=JC-}vV_dcknsNzVXxKzQ z?-#LSDP0KK&bE1^gU!r-#PAHA{A-<5Q2dlr+$que%ORoZ0)Yli!Bf-eLFRS0%k&6p zG@%Lo7boR_;ef6TzlXUB>&kyAhwNXE&?b5y5d@;RM40n5v8f{k{TP$yG?Nc`bK`w& zY;8$5JP1%d?~-CV{$)~7Mu;_-8Wv6+kmdizvtFT-b_KkA`O+E4yGZBDYoyt5Vd}J{ znuw^VWxw8p?VRQYgTqUD1a7%c)Aos-{y!)>BFx;E^KFz*OG}Hb;B5B&*x-M2xZs38 zrRrv-U`m1oI5^;IY(CwrT$+`sFuaiy_9)g7?;X2V3quB4^C)Ln30m_IJ#pPR;j1lD z`K_kCfW!W#IH~mc@>g$&Wmy_jFa_X)*eas4AMH375+xH$>AK_4;(!l3ar#n}7m;E-w3xa5I8jT8O8 zzt)_F7@Tu}Q--+O?3qG$=~wraVC4$U3!9_~EQ`6H^$HCK1O)%+)tYZ&6)Y3#k{fZ? zQN_jLRQ|q#dl4*=LHpqW5T+&DZR@N7kJdxSxxL-7$5cJV;*4S8AFMI32z|Hsn>j#cLcPIE_5vp$wBe(dU+ZGneYrZHHh1T?rNd~c9= z9>__a+cM*?VFkJutO7&s+kt6a6S5S9##H;-$LD=romjv?a^|T@Ka1p_6u%on*6;Kr z`!1U->w?DI$KM-0x3S?;uIYj*CvM&b$WFqRyMXdfnYcoO;Nt_R^^gx7kWVANn$@@{ z?pI|=UtGeg@i6pz=ye5j*(~y4EC0Uc?|{xYCU>p%)wa zSlFt5B44eqM&Bm~l>l*$5B9EWj^Dk%T7??f&TwAdow1YDhuL_~8SF-FeQT{m8STRh0a`?sTdzR^HvB@Qb zMJbINSjRkj5PZW|;{;V+W`Em&P++#G)N>KHI-R`SI%I3PszCa|+xZotXSy+M0X|Qv ztJ>m6nS_o<5d%LUiUA`thGA5SG&5#k3`O~=eCj7+$+%$cHoi{KVC|Haksj0aaEo@}bzP?wD5qfG7V1M7>^ee1@B zWZT?2ZsARIhpS(+; zW+`L$v>G)B73cb9$K(E6U-Jrk;98O@G0b@Qzy_Y_!10E7w)c^W{Lz#>`UU=yKygM% zF5k*W`_SZ;>7x$f`NV~pwm3(yqbb^ z>DZ7spj}jTB1PgDednWKc;ef&THbB5@_+US@OnahlO4sUCY`n~;VWAy0&4JKviGY< zJuq%wA&@KzYwTLeJ|tA$IWGv%Q-2G|{ZPDHUcGvarDzcVa1u~RchD`$u7xmFwd<+= zEewQ`yWZ6v_m;o-!@;}kQWI4KF(?t>UyztXJtOtjDay@NythqAsTM61%bFs1K$86I z?%fdVeQXSd=3_6ED_3oQ9EI-U0KzPEx*z`jfvn;Ujes^KTNM^ku>*KJ^+&Yk@csiP zzqHugZ|u)5mIqO;!FokcZRp`tl8B``p(l)&c{OgR8ca4k+ss&FWjZWn6_RPwL0aba zO4I(of%-|ozbwFhyxF}#?@A~=Gp$!-oH!;p_C6@K!O8p6te%+@*VETbwZKWS*8?UV-XvbTM<*Zw~Lczvdiy{vRO-r!J5m?d8vi2_k4l5$t<<& zSi)uJR;BRdArLxR1a*73xg~Q=DP<|zu1}HUI4hs#x>Si%4b6nFW{X1+uEp@y-1z?~ zo`Kzi4UH+Y%ZGf;bPuy+(8~Lsrh6AWw;9UE&H~{uf0=$_umQ3w8d|vbLz=VJt@C*= ze?CF4$_`EQqV6E{GPaed9lc1lqGmc080k*!Uwl3gY!>e=_8a9RHO*qJ^{pjRd04Df z$B>`%-^(BS{3nHCq&Y}lOorJ?KcUd?vo7?(;<<~enE$%KYTdt1cGhw7XJFk z<$bAKL3w#{do~1y&YfeyZ#N9h#nxa0;I~^|CK=$7!0J>@>3Tc@73t@uGcL-`srg>@QM*_EYHP!t&3b zt-ZR%uz7KEyI|hX*OW2z$2tbX9ihXE|m9Z>o!@ndR zKqF>t0GI0brW&)Ckr}E+%Ls?yRbkAZT&;G`&iP#zE1R-4 z;=%h(mg+?>qVe(GsoBZ<-Rcki2FzTkFD0N<<=Yd{6t{8_PW8C=Z6k&@z$YjNWzl&Hm4yES!&Xfj?rjQ*Y`wehQhT9gu{-z^B!YxPex?*+t;r((})!=b(D#9 z9(VZtdMH|t-%QHfdU#?bsN*Y)G#W}Lm8__wc`c0n@AR7qHjkp$X~IeTn-`F{}jVOcc|x3qvsT(`D(ph z)*3eynpB^ezye(yUcCrGopN$=AvzrLMQ~EFWZoc~k(t>C5T&Z(Sv_?*^MzM8^{TE(WU4d|Gk};x?{N&Ze*gZseJLX)CA=T0fe2lP zrFS`LRm%X(42QH>x zxD6b?fmJ=A4r*8|R;GmDq$@HPyDrztAPWwVNW@7L{)#S$@VG-FP$my5|d?&E%<D(*c!}N z--AeJQT+oi7hIhS4$h*dAG=xJr9j9)w9aV6_Fx5CqWF#{@08U_<{eipv zh-c1;K>iNaH$e0Rn5Hl2$)>Gyd-DG#_pS$bEa_ayqgMbf~oo((53j~GezOy zc!L^YoZra30LooTN5ONo8eah3#^47G5Hba+^}E-4dT=qBA`&;dY976<;`(rtH-DC#x+W111u)zLZ1y`a!^kdG?}b z#Sdm=WT;=G+4`#EKdt1K+g)x5hF2~K^V2t)PZ!kKWCLi0eCf+WWEG@v*-^&YyqUp| z)-63=Ti2~Z`5k=@B0Aacyq*r(iRANgTm^a)RWmt{C27}8BRFq`UyjX_C!G(0%K1FI z$;~Z$$4J;rRf{p%iV&ykTq=)Z@L z3`*T5+V&ZEP;e3!id^w|VSJoUp4d;o*r_cn>`GE_adswElSl?lNQU7=#NO!vG`j}? zq571Tk#Tt#1ycD5GM+>|PxA8evie!W2=Xm7G-ejLnc^{!kt!)8bMd=l*{;@<(q`)m z08U}a_=V>DGCA<-kFfM+2|&Okx~df70jdW}y}~ml!W%g+<3_{^>O4MUtsukFnc@EY zw^X)GqNGD5*B0P#*U`y|xw12~7nX*+#$?xqPx}7*r#)-mG}F`4N&$2S1)FIKd}Qrr z%5xW6_LW(q7agdFIgjzeS@dBZprG803G2n#8CFL`-(9ER2+RwwW%JIQ_w=Y0K$euV zkuTfh!run-yAB4ZoK8r4r)h81Ua!@Y9utY?i+&x@?f*akmEF8LcPY|n?_(iziAA;4 zI-hd)DNdy7s1Q9||6~9*l7_2XP&saCm$OylRG<&i?+l@F3Ok%yL-M}-hT?O_;+2;6 zSL>mE9QS^+U$Kcd9PBcFxCu{y&Hhe>T)mhKBLu6$%h~y-+eu)$Ao*U4K4JkhtJ(eaO=RsKjGNf+afyB|q|2!r9 z*RP7ODI%rw$NNtu!eYQx$$aop3QQ^yE@Asp4ukl~b`h zjhbGhxA;#Ky`C(;<(0Fu!V3@8RG-4npOf0hcltKymWP#UwEUHAH-r{x7$Pcu?gL7W zVpt+mMnd9L9w>*0hhbm60fBwGTG`e7MDi4#wzsTd*=(_UrplE-UOL|eFpnZI9$8^} z;z~JVO%Ml`xc}RXbZapOLWVR1axdE;ZtY#r8m9_Lht;w6TA}CwIBP-gsYU;xclFw8Fn#{>~_4HtbS;J2VSH7ouktUZifvixz z2xzWM|A~_bj@x;ORy${Y39y8!;Ya#AwG)iH9^QPH1RT~|*q3E=2>brQfZe`o+-{B| z@*3A>h%;U<8}mkPp66u)X9R@G2<5x<`cLfvSZxNid4)6IDDId%hWHs(3oFbq_Kb6J zw~%w)d@ zW}ieK&f@E03TAUn|8!64HK44$r|ACM^sT4wONo9$gM|Nlk(QCUuJ=gk?!(E5SBQCI z=hc#H6h=)!WM@QcbfhgI>(8}%8W!a({}t_k*lon%%6qvDUgN;IwhlbYe} z@2^Mk5ou{z*$nGU#X8D6vTNqi`y((P0s=SuY0wo(uU?tv#h?)XgKsXatFrj^4qaL= zl}_ixcgTqA(~s+Q#4`up++!`}sHi4hb1h$!`R|znXFn#xU*P1s!vY5zG^L=Z=F53X z>v6H>Enw!tn70zCE1P!sJ8gcwwZ6hMQ^+P9=tP9wFnW%OX;{DVTl zO8S=t1_)3)H$N}rQCw->No{=*?+M!X!5#b44h#}Yq-68>(eODiik!fP{sM@#g#gEx zhJuhE^#1Ti#x9BB_zGa)BlUXU*0>94=P)XW*9+x8K#3kv8D*fSPe#!<&K+>PpeD8u z(|JNxPw(`8Dfy{bXzV7G!}H^R+IJM(pBf+%x}AsR0wq@$bf4l z1d8ODX>HB@$7f-7mZw6hMfJ#M9Tpex7ae^j<(WEcd2@GGGlQl(S7JU*eWrUDctdwM zuI`JC@MpY-MTy})2lDOiJol%9B|lgkWX*!apggayvvaIo?(YvP8o0RU59#mn(wumY zFEu5Mn%FstIU(ck(UwYX)hohEBetbRQNVZ4n$Z8)U=O9);>i<6Xc{XLj6$C7?;k%Z zawa}fje(G*tZB}a>PU8R{=ERjdOU8S(4W+P~@fb)s`4ZihIPsp-AbpCJP9KM2i zPx;v&2MOTY6lpE`1_T+N zx1yZQUBOp${&S#Y7|qtK3_l#?F66xZ2IuPPI*4-Y>~`2p0)HC?cBVEcV$i3S{##=& zS-BNwboa$Bnv2@&d)7H}D9h&)Wb`ga#c#%*as&qvezFW3mJT0Yx^q|P7_TN9t&K{| z|9x0%@w0SqOH#uhhTdDXzIK6&-rT}#PwvHAYI)wrL@a=Mc6;g$Hs8|-4Z5x`7*`&8 zpB~>M@91+a#SE*;0Tb>ar%^qO_fc3}6(Dc1)adG6sy#k@En+vt%CRsb4=gBEl*n7Z zUXvgT%x^q=L@|(f@yL?S!)aAd+hvHGl$lG~vN_bEK}NwCLY@0uq6gOGjqXL|0Z~e- zT^40SOz}~06Yw-}M0c@RF&O{Yg z*@^yY2r~vwA^zu_+3^1?sd5X%5Lk*Ih3u6R$11WT&P)OoiHB*(f^o|*<42(<nv6_ zgtGyXEdLZ-#MHgb8rz=TDXo)De8gVD1${@Il}4h}_@u&aL$Ug|QewYLMY`MEU;lZM!$ch0zYmFKAMemN(ugV4vmfnni8NeyJFcVx_` zYe$kWlmCT)KoUlRL?qdHe7aXd2C9ENu3=m+UeUL2CM=yPrC3*78P&z#x`K9}i-s1c z*&jD)S}lhmFUeMGPcU@3c z@jl@i;Z3mPi1cp3$Wuq(IcyKA`x8@>r)fvO4@OFiMKw=phVWk$MEPESFoC*(|5LbF z9_KsOrM5Tk4Adw=r!bsN3$za2en3a)=6Ns>JBWh1b{|liKVq`$gNvuwVm22K`t^Ae zpmM&evb5@I$y5u~KGFR4sF28a_6uC0Du&ncxGW_lqv2GwL_SwuyxTnis7Sw`gQovt@$pu ze5fR&YwWd5`Z>usgIYidP5U zL3lWTu6BQCaj??L{((MrX5ep6S-ar$nHgzgUkc?)=Yv4D!R5s`ME;e3d;(al+Nt@! z5<dy3;ML{*sBaw&=MNG@d zXQ5e6U83x-4k|)|i#(^`oJ0FhxmPtVkl0H{s7?{9DW@CUJI8wfZ%*bEHAdH?qPWc78=oI>{(_1 zpacg0#?h)GUM=k0Ee94KatdjW8c$2h$t8*A&t|xCbx>EypUSkn_!VYi{&uAv6(@Gp zCe0$;zgT`n_sr~p{-48tPi%*iSL%fX$G6hzawuI&rZzH~{U(Q8eSITyt(dw55i>V2 zunw3?YA1K2#m*uJTt5!6573pq%i)Zj`PVlfxvnv)7WiJNxI`TJXy+=^SAs z5G!c{&mAB*v98z&zm8CFQSv=E+Muf<^yJWISGu(E!3$eT)|WA`IOIkDOe_-S=1E$* z!klfWn@Vq3vLVG`C_!Pd&p)+sD1A$9G)J<{}dR z3^^$&9wOp!Zb{A&dCP^DO^DB)r+sD0Oi4J z_v|;3=%GZ>515QOQtRS_(fx{>28By~ zc?Y27B!noKp6=AYo}#76P~%o6j=NA~${5bU&1fj5Ab zOF48$m-nO)A9)V84V$y<`j%INY?0@uQcC$L(`s0R@hec5975Cz%-WkiyrV$qcAh%uE}@r|6R)L=a78=Ixjxfs2Po{YJEpKS9lqx(dWp#PUDKFDh%A``ES(n$M_=7X6_Unddr6}&qrY2?e%@hml z3XbNMbvh9V+J;9*EH?L6`WhBcn}v$1wC^ZX5+L9)v!b_X`VH5=2Cd6`Zc*Gg`mIRu z2OMZ$;2+PcgB{?#Utm8a&k4U3^tB5hQPRSE*3|?@Wd?@$orbhOmRN+)5@a+DAw~BfvTdB%M6Nlm5J?!j7e{VE~@iW*2JOUo`DDSxxCzi3`wDdH1T6hW8 z6(cCBvpKF&%}XJUWUP6r)rsgzl|moi`67?FPaG;qZX(_57Ex|XH(OZ35X~ql<|3#Ca?*vjo+Gi2)MJ`u9H3K1m=f5y%nwzJ5 zvR5)Y8g)$O!at8mL<9)e;_8pE<#k&S#n++;U}^s}$49;&InRUWTtH$UlQ5#{ep-+s zJHJfN;W#hF!qX}a7wNie)l8Idjjo8p;J_@P%1G=2H*G5z!stoDoGL#CeR%`L;;-Sg>P z`}Mro`NFU7#ABF;cxN|<8~tKbums(LCwv=~5r3GK z^Lk7tu=w`WkmfT_YiF_Yk1eVLrxmtBw>9z244;!C$Sm*HC#yl*0~?kzW8A0%4tt@=9 zSWzhuO;?g%l9n(=Zh*(8H>EZPO8;nJDq08OB0qZa*4yP=x*vA1@5`;QEvb2LL&rf+ z_3Qzj85YkhTYrzKlpY)vl>Nw~bc zM2)GHeoy#5>yyUQ2ssF@v4r&co6e0I2m;=-{uaMZwhtI7z}YMTUyg!d0+Ki6X^du^ zD0*eEhamC1UK8pJ5Qi|}@3r@ci~r^_dllPgUEo0rwW%~Xr%MxVzSsx@haY*LnI0KB zqI4ph93lmtL3nQLTh_MuNS52{*N-L+;>1d+AOQZB&6W|_8Sjf{;eBd|e@}zyS)Oq2 zs9ra})EnhGo0R1gJPUp)t?%9NltBx160)+UBPg2sKgqb`8%yu?WkF?*-QMPA`Iw`$ zcj6t9ev~K(>9`guVj(*VnyZgje0+GA04}_RH{?h~tlEfL>PwE-(`Hr(B0)31?4~8+ z>#85Vwa&eBKY}bE_0wabHxlR{#LN2N(QTv5XNm@G5LGhyJPXV3*Bz!7^_n5=(DnjB zEP0qCrLG7+%FEwp?FAR2CGa(`kbEfnP-U=%rHL8(wz4v@H0MK5P>|-CHFzec1FvBD zXubj`8-?+XA7r*?ozgp?T=H13NbyJnz|11s9bpl&aY=ikGimrPcMrI*Py9nkZSu#{ zE#fW;x{^?q98&L;N}rlSX0`lC)R@J; z<>=w@oE)7P5{q$3mc|GTml3nAogD#T5fL^NgpPqhc3nJmlWw{wpueJN3yYS1hq)bT z#?oRp{|9Jv8F6t=Qcn^nyl9WrM6IjV5NySmZ!;C75J`b_LsI8H)k;dws|KuhPi@TZ zWfvtCLhfw)N36=ttjyXBZV+C17(HLf1iyvjKdIC_EVb$=y(lKc%xZ-X#+!cG<{XH@ z5|8ms?vL{Cb*hb93| z`yrOpusAwwkzq6>hUT*1L(P%Zjy05}^!rP6%nzJ#Keea(y;~!LW)_>t5ZC{4+;Nyt zCBf1HgnB`Y<{RQyJ6F(CmMfuGF{e7d)$8Qv94g_jp(4q>kVxA!uIuN2_V{Ii*lviw!JOqc}C1Ox;WAf}o-$WtI=5Q}AEXSYIDzdUlGV@8jD;sQ5hE6d>) zBa99tPNAWp(Sr}q5uVx2XeL3&|F^!bYOrZY_z+v8o9G)}d&w0L8-Y}VWh?*<-1*ooWAK%{Eg6CJ@YUQA~HSSqK;_=Ugu zOqVb28I`|ZZkMw_r^5hhC9~DRJ~>6&_#H=@G_5ubP>AP71X+T<3lBbcW0cjW#RNr8+S=NH)e@a0OxPbBz-7#;=$$E0b)`ja8jxU4c;GX}h6p&fR*9hb z)f!!+pw9tPUK&WVIx|txJD_muiX4Th&IIulNefM zIBm!Gl$fnw*g$_xZn0hiXJvsv^U+@ z*+guD3I%E|@WmL5j$1ZEXI8K;jR}oG^`BW@UiKbnwj*Rdh}^AcNPrvO&Kp&kob&F< zu~k#ILPVnGSKSRt7preRXnMEzE!Nh=pJ*!x6h9w6nltw2)p)P%e#nK!0Y|xSCW}-ZY6`QRtSpwp=oR(( zhbYou`wHZedY20@4$_+wV9ArOEbA1(@A38Zk9o4k3kwYBbm$&-2?@D<;&^GWpu7R@ z{ENL#+)5p`0e6eoc3sDcD7~+g94vNHiX7ftWo;UR0(YgW=@hJL`LEGEFnQOejBm(0 zp)en`gg@34g0TNKMOP1wDOiel{FUfSzpv^hh0zdmD@#Q4Z^>KxYN8^$b*Z6pUy6y? zK`3V5teF-`P?**_?v6ixB9EN_`M?$_i%`AoDdlw?<$POffOlWrJ!1;rkisA+VYe<5 zfuvu(f0YI(A*~=LM-bX1GVcJ;D2rUS;`(*{GJNl4Kfp9FKl54+B3!-N!bct-YAB&X z2K%BS(^>GV(c$knCYW%nXX6UF|XjW0QEbqI;rbQqhqVT3Q>Y|02Qw z|3e+Ii`r?}OobK{(HGidxau2(K#*-#X(KszR+9 z0{y!xD&%Jl$PIuA^g%Ya71kzQ9G*47-tva+8tq?BWq({Z0) z@IYEcd2JS&!g|?+EOsntR^G*h&x;OLETjb9vP3xFM@DkTpWPiOp2RW>hnwrs8Jp_< z;O{@ePz&UYt{sZaIJXJ%VM6nNL;MdL$;q@HgmzVd!@y!VhtuUkSV)KkMsw=mzO(!n zc>3ZV8E{6kAMX~oNjI6u9pKocC&DRJ;P*}r(w^y)5xYL#+5{?vb%)2>F9;F6nK}wV zaoVw`PZU3^H~^@26%L4}9jKl6XTi-G|M=z{7?K?~Bbicaw8*wzTcsK(V!Sxaxn(Dg zv0Er2VpGEMGenim+w|&FF?RN`$^kDhCGG(b?wBv+ek z>ylJ!T}9DPJ*-8m$66|W)zDs=o1$bfyeBk}@#{=P_7s+oR|DR<`IPQX*2kxxHA-lr_#%&1OO1dUAw4|4@M<1!q$W(+1 z4g$=&5esSAoPpybX&kHX!s2mwuyqgAsp7D;M_=gMrJ9ge) zOX;jaWtnHJsQyak`!IHJ;ky)Z8!I1B?rgW1AX(2bJ=TMsw+|Ins8YR)E~{0w3UQ)j z%AP_roL~d8S(8+osdO{~PM@vX3&J_9dzMm;gf2bKFO7GJ(?8gW(D>1!Csy zOsN8Tv$e1E_9$z84}+6W4ZHTlAM3>ApitPoDr1e~cH9$Weu9i7l|th?s$DItUn6 z3jh#6nYvlXn*;QgZhH8iC#DeF_(2WG(H>Q#r(Kl-)r>X&i#vmP8o1Scc`a&E}{+LL*}X_AZs^e>D7;nc1BM#q|@2roS4tqm`i0=VlwyoGA_MTquXN=(c^ z%Zo{9`97+De|!5IRHUM|w!RUczjQ#F7TwE2mIg6MwWLJn*~v`2O|!FTpJ{ylJBSc38lFt9of9F( zyG%z%p4n;?B1>a%{AdI221heSUgLgT$6maaZWb48zTvjw)Q4h>yek{i))vj^CskkA{=^)(vyoDc2ia8Yb z1ZI34D}6=Thc)Z24?EoHx<{4@uLX_B z3Xg)(yqk|22{-#8FhR`b?P*1Mjh)?rscKc4=NGa*=s=_ddKQ(3uPlqMm?@;H}L>#1Vh$BuI=&m9*8)bZD-A2U8mj zfk|fk*BcA-1a@}z4*`VjMurO5zic$HCwLpImKIkUD&q$JJ2>+lJvype$P#otd0p>5 z&+_y8(l`mb&~wa79x^hr`RHi3hD6uoF+cG%`14s>Mi^B_rc1u3vT$<kKQckqY>Ksz<==wu1c|aTXTa)OH-3qc!tNS zlfK$p9O|-6YBmgzBi))>T52h4C2!Udy!-d>V-WCj`CR=iD&lHats7M`eM0uQMY!Ic zg82Xi;6VOWZzKES77n=HO=>gtG$t{B5FX@YIyZp;OWL-|2GIAn16H3kwY70yW_sG$ z@!%a()Mz$OgS1xtuYN>deu3Pgd1U`rSXdZ~`L{ktIUP;SRnEZ28rmlLU+{OhYV*Bwtk#@n+aQ3(H(!(p?!V+ZAn$5Lt^{H?&-20 zY`?zN>E!g&64W+lUVPJwj|6zY<{zc_T=9KKvf!@Dr5R)!DfUJjW8sm~}l2x45(*q!9*nv2WlN4gF_L zy1y^+=tpG)1uF$)WgF=%YOhxUi!6<6-#iY(9B({ZhIzcVIQgZ0e_?`%Bf*NxAl@byr4URPUO;CKF!dApD`B5PW1jq4J3v zCO*&1K%PWzI&)qy{?R`q33g(HFTO%5)Hg*2%X}zY@UlMsByws~X@;mGcwk776vc1} za=bIv$KOy}d-GYu;+RTeXj}Bn{45)+i+BcO8_)^vfp)nzf@=0YXO-Db`6+cXikz_& z<|x76wbUO}UZ1Hl^3hGy@b*!>;K2-ETLJ?PJ3GF^n_p4kADRX-pnC_%bSXv0t#GtJ zb)PjylUl%d6op@#Aeem4dalSQ^&NFH)~#SAp)8`5D=V~a?d+`%Zn!I@{lU8Re5S0F z#}zKZ8yEGEz6bV;B4-L3$_Dxtw}w(|Bh=elte#Jzg#rn+QdL5#)u9)tujewt!mQDU zznPgDc>A0u6vOFnvNe=kBj>kzDn446xkI20nbBI5ZS&hT&wJ)2`65-PoeCj|uh*ZZ zVEVh18QeFqzI;JU?!~!$JGpY{^JNTb%D^&~8v7o2bn>GJ@-2+CccuC7WpI3Nt2RVQ zwYB`&Uw|g-JxaW_ns^&!al4GSPiSc0nS4qy@fUgLN!es%3)NbUFrD=9C_wo&;3ztA z=(@lad2Sv)3F3_Cx|>_uR=oRed{OevP2Hyu(M`0JAQF4w_q;0Bb%Wn_Fkf~1Cf**e z92_w0O-@p7yi&3)pf(Fd0+wevF zg$9DdrLOS3=t|7K=7~0k>ZvN{KY3MVP-5fNc57HtFk))()45IsE-JB6HP3R71kNo9 zY4Lu1rkJ^hPP-;e{}C-%7){pA5^tjN-&@3M z$9$x7)n>Nr*=X`A{c9={R$1g|4^jR6kYtw9Y?1>v{@RSZHiJd<+A01U)$2-aFYqlx z9lvE_1T}`UNR;doH(b_zH=P<79Lzxm*F`F6(|$<(93OwcuD;LEnxU~QFXx$ZV|o1Y znNipuvu(AMEcbV=;Y3;e=tm#FS-|R{qtoZ>pHCQskZSGW=a>B%zZ~#>(XnFX;8n>% zruzd28t1rdhmPP${nzJ{*iQ?6xmlqFF9+&iMamXe9 z`cb1zJ+xo-dk*_%b!2x>Y_ciNtUsJSC#ZRI-Lxw66MEx5V)kT>wfohA{8SimL>wDx zGpY?UrK(Nwjnvi%#tBMXl2T6b$BOTCruj~ET$}}w>4b4N4_M%z+u9a;+uCv~%3d$` zs$)6YgzAZ*^xOGbc-wbpWm$M)6&q4mj$|EPC6<2Sy!|jI{=ZLnlYw1X+ywu}(s{>I z{l0(veeCU6A+oafmXf_nvW2Y7B4w0p$KKhB%n%tVTPajzkA#eJWPXh7Z0C3RK7N1n zh|cMp_xpa|_jO4t@O6IFN0 z$Ev`34VG!c{G@9)$1K!qw;JrHR7E5!b+WhW0nV@X6~Mw_iT;yUEqt-QpRP|dPK+b2 z1nk|HYfWnEEZh>ZyS!H{JYzX^q4hY^EFsy|E*hMjo4q|gSfmO7Nejc@MTO0Ym^Ys% zmvKsdzbE)FC_`%`fSD*vIsX{OL@G=@tF5^I;6bctw~PjDGHvSzd=}T!t7)M4c>5lh z^y)=9ZR{U$T{vOlx|_UPoFguEp$SsYh%kCEk)Voh1s}O#7hhwVd?j2zq%$v1`Mni$ z%jEPS8^Pc|orvj4vLeLxo|l)D)YYpZ8npydJtrfa@0M|5?*$~jFg<%TZ!hF-bB?7c zMX$vnVdvVwl?!Fd%883=E_np!F2A1BT6oQp_2Nb9r+|ZMWjvE{l3VztnWDvK<$fOI zq56VJcnTL85Prbloa#DA8m{}mHxVKX<-b3CJ(q&y+j_quc>A>7qVivIt+)G!Tl9}x zB}w}mUqNlgG9a_tqVAMQzV=~oa&mGw@0{NG$iv?LqVA(>6D4KE^hmR#gHjQ6gMAO= z@3o1<##$XI?jZ~j>vfc}o0pdnhiuu)P5j?J8>ng+iDIalX&1XvZCdW(>(@+jJR;K6|N(vKf$FUJe-A^K=l z4I)LGuA55on<8DAeXc>bT^3};3>Iv9Ui=LFvEy>Lx7h`rLVygz=c_ z{`2&^Lusx<$D0*AXT=`$;|9(4+)Mc_H})a{|P!+oLe`b#yFcT=kZr< zL;QoxG7=BI|$qc96G7^=q=BsFJXzkyH#sl<71!wrB ziMA0WDKj%Ond35wV)dK(Y-`g1KI!$ay^3XO*vPo>bHBTgUMV-d1tRG@;RR{mTrF`q zxdv?5{lQhamK^s?f=Aqx2uELUFF%ZV?xc28`Wj4m26x3mbxzroxwd+mE(=RUnGxmv z9o^g)|K+Xym-p-TTrA_SsNNmf#L#e}uv)9XJ{Yd6~g8Crm z4>Bk9gWnl#jg;ln#-8M-w}1MsgU!sK$ZaA(9K}qG`ofjIGKAsrF(6LkmjTS_qAosw zl_v-vJmN_6nmTCcjC7xvGzvv(NGPjuY9Cr+EJ@&9t^6eV`owaYGMCTS36qN4{rK*k zhYA(@oAGMnM?BkP&mC~XiWfT?OM7?&zew6&{j<`nEw%B)$>|lgzL!;%f9U9NxBN1F zckh8tD=u8MATyJh5b=a9tS_Y|g}jE_YAyY*vYrRTV!&@vPR%)Gk~{}hPZ7Y#7HsFZ zEOliI^v{b63-pl+F)*%18;jXg?xd&NNYa)^rIF&e1{pG^{k=Cf@ko~wm93E)d-p1n zJ`skC`D-unVC=WMZn1C=x4rARj;W>?O_UM{@a3P4viXmsc6y)j2YxVC$-%p zB{(iw8zG_jyM4Fq_O0xL24CmfS+30u6JgSOT>iL^3m-Qm7IY6)O`V;bXurvGMl1g_X0TUqbn+rd*Tz;SUMab!!ZiM=RprehT&1L(+^tro zCv+^OF5knfd2yz#OnNXmMS$Z%3a2m;J?_@NKNi*3S{^vnP~pAREz!2Ii`cx8Mk~J< zOBE2EHWB-6<>S>iI+;6{MKhainC*qp!HW&u>|$Oqs)xpueuFhj5iL8rE4Om1DLZN7 z-~@k9gmcA<)1G5E4rap%yfmhPw}m4*Ol=9qLjoUqklJS#G(VJi;2)BLTLowF*6s=s zs&?SuaCZd;s{D(s+1hlz64%Vt(c>!$&Shy=cg;8IThAYlHMr%xcp=me%}i)ssOISL zEZ-+*w$Fwo*AtF9l|!APuThXS*CN%3w|wIOo3Aq+j}cbUzH~Kec*E>qOd-ljZ*^;G z%2Go36b!tf8VLhTzcRFe1@s&V`*8h#&lJs(SAQPily>}aD`}wtHJG-X?Ye*cOUv*| zL{n2!=%h0>BCW!9i2ec^aP_00wZ9tvx=Ieh6_3x*`?r@|jK*UwS3)`C=R6U1h=r5% zpmq2;87l%x?2|04&Op? zkWg-_KvF>A>fc^)p##z-?HrTlR#k+iS3fgPJpN^{6ZYbM)rWDLH-SNhBD(^AwzF@z z%`|T3>Cf%ipZi0^6EPT+f^S-9Km+l{E7cKS#>K62h!pYZ$ivTQWPULbc5l%ih` z&lxw{uIj3;VxpuXqs^B!n^ZvD;e~l7{#<%H~c30g2V~!_=CvrJVSGZ zpX(i!CObZ&^b9=MnxDaB=H+Q|u8j{;%hx#bwH6xW1>6Y=Qp~`%XzDHRKDieZ6eKS} zN*7!9OPweI7`E$BAsm5(QkAobrM}3^tJ1Vxv&@?%z3NAtd9gb6GgC`s@Fb0g-)(Zv z9eAJ`LWACcfjJlJt4!Qgpxij``>|89@rZj`&G%T9$)0oGrxsts5HeN%FPyhvAK{2K;5JCxFQPFLK`S1LdJrk$M4j5lD5+=2kq9CA#kkYG z7IVA!>$rn~U^_mn#{v$Tv)z&DV(NV*;X4Q%Vu&GI2wMwxe+nXuA%C(zw&Ui{ZZy z9=U5X&F%p!2T$QC+v_{*t+~G$RPVNF{YoRmj1i6JPi!0GVOk_QM#LZKS?=H1Pvpgs zYHS7QaE60+r77vr^`D<^{3&TI{5zS;8asASw9kuxl?OWe$<2K|=duW~A(aBOmWSzB;SeAC#!QtL#nBoBat9$;2TV zH|g`fjRW44J63hIwfQ7(!b}oRL#H|5K5c)6Y66$+TKBydOiY}TECjY9$yYgK4zm7S z+8!{hq7Cb;cuBH#7sGyVSyKaIaVY@9>nQli!S%(phE+y3U?!2_O%Zz=J~ z`l^7Zl}=_jU3-Iw3MMJyVU%5?me;~cXDmpEcur}j+l?l9kbM|ELpQsl;ZacyFphsX zC%za8N0kEYsXI|)1A2DOKSn3Hy12P%&{z;ppgyVs=B`ak(Os&_OZA2$PkfjP3UdR( zNI$-=oL@nZ@{?2O?WWNv_IQD*OZRnMUES)JqgcW`Qrys-Qo+w0kB}S{nq5b>oV=~* z`QS{4(~vh@`9CHm-e*^wU2ob_myNsOAqQ%rJd-QeixtNmUz<3MJc z6M9{N=ArB|WeXNU<9}~j1LFn~gPv`Ecssl1Ffis&bVh0|VERd-gMCO_jvyKzE1|b# z1IZrNgWIDr{C?n6?6q`l2IQ0o8S2Jyoa$x$oH9>!NeN}Nk&85YkAY^pD%Sb5ZE6sR#&fAeS=pN4 zVdAGl-u`c(qQ^--P3Ak*gG9v*q$yD(eD%BzPjg!HLce;`NTlYdKx~KqE*#22IP)V8n(966Lp?w)_&FE=jCrIV{?TaAX8GATt&j@dKCAED4kU ze0KgEEPe3A)>$M)D%PIBJ>#s!U0*+oPZhx&$^HwnFGO)Y=B*QNU{?2}6yb^X(t2*S zd=d7CVvIBJoC`_4R_wl`!X@&#c^J>sCN!)Uv;rPlu3~h=@#Tvek9KNysk5o9DYDy* z=-VZ0tJ2;&E!A#gdX}r;TYb#*mSkr@3CARP0^Q*0CGJEM=AFNz4L0-hdTUFC#JP0% zk_X++PpmBL>J>dK27*ryq8OqJ5aorgR6bhQ%$@276>q*|LGP^&{8qBA{RV=4fiOfq zTtA&_J2;^hhkC)GJOgi4rJ$0$Khm%PKT3xAZeiw0YKwia34$!|2Em?D0@F=4McGIY0L#zQuKUn3Ol zFuPFAkFVV2My?lGKZlQ2pU@MjY36PB6K`_1j4{5m$UdPtb?dQO{Ddg+>6?=z^Ju>&a1?>2V+8&;LV`j`g1 zeDQ)st?=N#;Szp3JQ{LEkU|YR442Fd^imI!Fh3?w1zPFi+AL8&JfcxAS2MIe!pmhy@j5^wyz3x~7vT4O^UMJ0 zk$)ykBCNEHzYzA=U?HJ6b>Vx{lT}M!1!pxr)5Rg^zkEl`o}`Kl;ga5Y^Al)^2(@X@ymhz!?Dj>YP%<*FP01fAOFNlU_09rttMHT zii;hWX}bC(k8`mcc*=ia5MvYf<~onAyPjJ59q#<+neCx)MR5oW5oP*pjq`y1*TY1u ztN|F(C|Lp9sUsTf-i_CPU@moA=jKHAj`C5hp8%A~*8-Gi#y*ETuid@8Qok_`AHUjw zUhN_2H~h@yOvmo1fc}=+JTA@rFWK zzvAo zzfE3zGTI(k)Ab^Z-$c0>!3_U*IfQ~zU6VcDBQiisNNs9uO+?RQkR>R737V@cXXPwq}IrduU6h$8W;R+{T#zTro*xxNm%_wCZ4;vHO#S1z4cbDivS3F)Fd+Nxq zw-ufG19L6;IN!_YMR45mI5vh+&)m9m(v+tZ9Q2uKk$L+C4mz)wz^|UDpDc0E3)Hn} z@14;V$9dR(MwS~;lQ$JQ_UT?n-*HNO_jb57{{ru;A}~d)qcRDl06La6rvn+=BY*$f z7p2HJKa0c%1dqXo#1;AFIEyHyZMDjsPiTktkKUDDCX&?W!`1%;KQ(Wl%#r$ENJV`I zAtrvoh%5Nn#nmHE_XGE+IG=1JR~t&0QD|`E!#0IsP)U`bI7g zyEW+5r@NqZsgyumDC)lS`JWntDn6&jz9_C6Du3(V?$vc+b8qxT;qkrx@Z$Ho2naEH4>8t(ihDoiv;EGz?B`=i!cN|B!f zslIxJxX!D;QDJ2Gra|u}wAeTu9&0ikw#_xTnwo4|M>1R)VW{YQ-G6X;a&-Q9Qbj|P z#5{Kfiaa!}Zqe^I%#yVRa;14&X~cVgKdD<=-5}m!;ja_0`A377>Qb)Jl_rtsC2sN9 zKbj7kTgY2=qFD^(J`NRs^f;D^KJ6H~Hovmc!{`T1-q@_3Rjxjr|19tJxD13bUy}a(eca*3CW!j!884OTH<>UP%9=`(F3->wqALm2bKQg~SEs@Gv+x zf#tiF*v3np5=@qfG!oC8y_Lj$`pWGG-ncm9^OcdsFTh(iRmlE!57~!}{MG}={!3cA zmWV8uA^I?|5vvGYDw4keV@KOIR?YbYW&V|^sxeu{t;eNhPMXSl2pXA!`!0Pv`TdGB z;psHsn2}a4Us>2X@s%N$tSkGIPa!$^iTdARSeRfA{=JL|5CG=&Kr#o|w?k-$IS!%; z7pb3l7K&#dM$zhwV{^Qx6rP@3!DE%ZR7uA##wCOJ`(O*o;~pKwgq<) z^KOy85u6RRQ2@>(M7u-qvcJd+S4_LE}>+5ZU@8IDw-*{J85#P3Hz2HQx4c zC_jxDo-4E@t{=<(9ha6{Wk@mb^n47GbOf=gUw?feNOKFVYN00Y{N%ZoE)lp{rg3IO zrBg#g5)8K#11+3omZ}DneOKC?$Wm~FW7D5c?Q(qoMx4H#{uGR3E1sz+h6g7sHEQFV zCnP4kcBy5}p`~lh90x;awqy8|pH$4cncpXgkM~Q^FA8F~S$<+^YBdD&^0L+ntmS*< zWXL{Uv(Wz4u%52_)bSphFfklQw^f^W9=+%2W{h1=i! zlKjASobEU;_|$h}lG)1eGm|s;nlF76f}hRo%~X0z8t!;51RwoNxcFX>VW+8LGe?LD z51k3~BL*sn26NX`mf20`)E7bHIwn0rHxG~GW#+2u@1{_+U>zXJF4Qkug!;!DaHYG) z#spm5XJxWKnD2RJWbk#f`DfQtJOsMP&bFyt$!bK6s<#P$4xEejLh$Hk*o>u)`pzB= zeYg=jFdBAu>)e>rB8G1H9BdV17qSv%pen>KOO>P&vKzM1Rq90L!hR%p i6M9DNg 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

    vva;0uFJ9r^8iDYogs0|7mHdN$ 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 From 2a93b708b384bd4f5057b9c71fb8b8318708a9f5 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Tue, 28 Jul 2026 23:14:53 -0400 Subject: [PATCH 03/55] GuiControl: add rendersChildren() and applySizing() for the editor Two script accessors the Gui Editor's properties pane needs. rendersChildren() exposes what already decides whether isContainer means anything: setIsContainerFn forces the field false for a control that cannot draw children, so offering the switch there is offering a switch wired to nothing. Reading the engine's answer beats keeping a list of the nine classes in script, which would drift. applySizing() re-runs the layout against the parent's current size. HorizSizing and VertSizing are only ever consulted from parentResized, so setting one left the control where it was until something else happened to resize the parent. Running with a zero delta is exactly right: the modes that describe a reaction to a size change correctly do nothing, while center and fill -- which describe a position the control should always be in -- take effect at once. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYHdKkn3xVRLjtr226EACJ --- engine/source/gui/guiControl.h | 1 + engine/source/gui/guiControl_ScriptBinding.h | 38 ++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/engine/source/gui/guiControl.h b/engine/source/gui/guiControl.h index 2233ba8a6..5b1f96a33 100755 --- a/engine/source/gui/guiControl.h +++ b/engine/source/gui/guiControl.h @@ -378,6 +378,7 @@ class GuiControl : public SimGroup, public virtual Tickable /// @param value True if object should be visible virtual void setVisible(bool value); inline bool isVisible() { return mVisible; } ///< Returns true if the object is visible + inline bool rendersChildren() { return mRendersChildren; } ///< True if the control draws its children, which is what makes it able to be a container. static bool writeVisibleFn(void* obj, const char* data) { GuiControl* ctrl = static_cast(obj); return !ctrl->isVisible(); } static bool writeUseInputFn(void* obj, const char* data) { GuiControl* ctrl = static_cast(obj); return !ctrl->mUseInput; } static bool writeIsContainerFn(void* obj, const char* data) { GuiControl* ctrl = static_cast(obj); return ctrl->mRendersChildren && !ctrl->mIsContainer; } diff --git a/engine/source/gui/guiControl_ScriptBinding.h b/engine/source/gui/guiControl_ScriptBinding.h index a6c5d544d..6e1544e0f 100644 --- a/engine/source/gui/guiControl_ScriptBinding.h +++ b/engine/source/gui/guiControl_ScriptBinding.h @@ -373,4 +373,42 @@ ConsoleMethodWithDocs(GuiControl, getTextExtend, ConsoleBool, 2, 2, ()) return object->getTextExtend(); } +/*! Re-applies this control's sizing flags against its parent's current size. + + HorizSizing and VertSizing are only ever consulted from parentResized, so + setting one leaves the control exactly where it was until something else + resizes the parent. That is fine for the modes that describe what happens to + a size CHANGE, but "center" and "fill" describe a position the control + should always be in, and those look broken until the next layout pass. + + Calling this runs the layout with a zero delta, so the modes that need a + delta do nothing -- which is correct, they have nothing to respond to -- and + center and fill take effect at once. + + Does nothing if the control has no parent. + @return No return value +*/ +ConsoleMethodWithDocs(GuiControl, applySizing, ConsoleVoid, 2, 2, ()) +{ + GuiControl* parent = object->getParent(); + if( parent == NULL ) + return; + + // Both extents the same: parentResized derives the inner rect itself for + // the two modes that need it, so the outer extent is all it wants here. + const Point2I extent = parent->getExtent(); + object->parentResized( extent, extent ); +} + +/*! Returns true if this control draws its children. + A control that does not render children can never be a container: the + isContainer field is forced to false for it and editing that field is + meaningless. This is fixed by the class and cannot be changed. + @return Returns true if the control renders its children. +*/ +ConsoleMethodWithDocs(GuiControl, rendersChildren, ConsoleBool, 2, 2, ()) +{ + return object->rendersChildren(); +} + ConsoleMethodGroupEndWithDocs(GuiControl) From 543367257211c1aa0cb2b66caa8055647a578530 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Tue, 28 Jul 2026 23:15:21 -0400 Subject: [PATCH 04/55] GuiControl: name the sizing flags for the edge they hold, and give every control a profile Three fixes in one file. The sizing names were the inverse of what they did. From parentResized, horizResizeRight has no branch at all -- nothing moves -- so "right" pinned the LEFT edge, and "left" pinned the right one. Reading a Gui file meant inverting every flag in your head. The tables gain anchorLeft / anchorRight / anchorTop / anchorBottom, plus "scale" for relative, and those are listed FIRST: ConsoleGetType returns the first label whose value matches, so the anchor names are what a field reads back as and what TAML writes. The originals stay in the table and still load, because ConsoleSetType accepts any label in it -- every .gui.taml on disk and every script that spells a flag the old way keeps working. Note that a Gui saved by this build cannot be read by an older one, which has no "anchorLeft" and falls back to index 0 on a miss. The alignment tables were declared with count 3 while holding 4 entries, so "default" was unreachable. That was a bug rather than a choice: a control starts on DefaultAlign, which getAlignmentType resolves to the PROFILE's alignment, so it is the setting that means "inherit" and every control has it until someone picks otherwise. Hiding it meant ConsoleGetType could not name the value it found and answered with an empty string, and there was no way back to inheriting once a side had been chosen. onAdd now falls back to GuiDefaultProfile for anything that reaches registration without a profile. GuiChainCtrl, GuiTabPageCtrl, GuiSliderCtrl, GuiTextEditCtrl, GuiInputCtrl, GuiSpriteCtrl and SceneWindow never set one, and a null mProfile is a crash waiting for the first thing that reads it -- a chain does not even need to be rendered, since adding a child runs calculateExtent, which asks the profile for its borders. Doing it here rather than in each constructor means a class added later cannot forget. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYHdKkn3xVRLjtr226EACJ --- engine/source/gui/guiControl.cc | 97 +++++++++++++++++++++++++++------ 1 file changed, 79 insertions(+), 18 deletions(-) diff --git a/engine/source/gui/guiControl.cc b/engine/source/gui/guiControl.cc index dab4a98ba..66f187593 100755 --- a/engine/source/gui/guiControl.cc +++ b/engine/source/gui/guiControl.cc @@ -53,23 +53,32 @@ IMPLEMENT_CONOBJECT_CHILDREN(GuiControl); +/// Counts are 4, not 3: "default" is a real, reachable value. +/// +/// It used to be excluded, and that was a bug rather than a choice. A control +/// starts on DefaultAlign, which getAlignmentType resolves to the PROFILE's +/// alignment -- so it is the setting that means "inherit", and every control +/// has it until someone picks otherwise. Hiding it from the table meant +/// ConsoleGetType could not name the value it found and answered with an empty +/// string, so an untouched control's align read back as nothing, and there was +/// no way to put a control back to inheriting once you had chosen a side. static EnumTable::Enums alignCtrlEnums[] = { { AlignmentType::LeftAlign, "left" }, { AlignmentType::CenterAlign, "center" }, { AlignmentType::RightAlign, "right" }, - { AlignmentType::DefaultAlign, "default" } + { AlignmentType::DefaultAlign, "default" } ///< Inherit the profile's alignment. }; -static EnumTable gAlignCtrlTable(3, &alignCtrlEnums[0]); +static EnumTable gAlignCtrlTable(4, &alignCtrlEnums[0]); static EnumTable::Enums vAlignCtrlEnums[] = { { VertAlignmentType::TopVAlign, "top" }, { VertAlignmentType::MiddleVAlign, "middle" }, { VertAlignmentType::BottomVAlign, "bottom" }, - { VertAlignmentType::DefaultVAlign, "default" } + { VertAlignmentType::DefaultVAlign, "default" } ///< Inherit the profile's alignment. }; -static EnumTable gVAlignCtrlTable(3, &vAlignCtrlEnums[0]); +static EnumTable gVAlignCtrlTable(4, &vAlignCtrlEnums[0]); //used to locate the next/prev responder when tab is pressed S32 GuiControl::smCursorChanged = -1; @@ -148,6 +157,22 @@ bool GuiControl::onAdd() mBounds.extent.x = getMax( mMinExtent.x, mBounds.extent.x ); mBounds.extent.y = getMax( mMinExtent.y, mBounds.extent.y ); + // Nothing below this class may be left without a profile. Most constructors + // setField one themselves, but several never did -- GuiChainCtrl, + // GuiTabPageCtrl, GuiSliderCtrl, GuiTextEditCtrl, GuiInputCtrl, + // GuiSpriteCtrl and SceneWindow among them -- and a control with a null + // mProfile is a crash waiting for the first thing that reads it. A chain + // does not even need to be rendered: adding a child runs calculateExtent, + // which asks the profile for its borders. + // + // Doing it here rather than in each constructor is the point: it is one + // place, it covers every class that already exists, and a class added later + // cannot forget. GuiDefaultProfile is created during engine start-up + // (defaultGame.cc) and is the same fallback the TypeGuiProfile setter uses, + // so this is the behaviour every named profile already had on a miss. + if( mProfile == NULL ) + setField( "profile", "GuiDefaultProfile" ); + // Add to root group. Sim::getGuiGroup()->addObject(this); @@ -179,27 +204,63 @@ void GuiControl::onChildRemoved(GuiControl* child) } } +/// The sizing names, and why there are two sets of them. +/// +/// The original names describe the edge that MOVES; the field controls the edge +/// that STAYS. So "right" pins the LEFT edge (parentResized has no branch for +/// it, so nothing moves) and "left" pins the RIGHT one (newPosition.x += delta). +/// Reading a Gui file meant inverting every one of them in your head, and +/// picking one from a list was a memory test. +/// +/// The anchor names say what actually happens. Ordering is load-bearing in two +/// directions: +/// +/// ConsoleGetType returns the FIRST label whose value matches, so whichever +/// name is listed first is what a field reads back as and what TAML writes. +/// The preferred names are therefore at the top. +/// +/// ConsoleSetType accepts ANY label in the table, case-insensitively, so the +/// deprecated names below still load. Every .gui.taml already on disk, and +/// every script that spells a sizing flag the old way, keeps working. +/// +/// Note that a Gui saved by this build writes the new names, which an older +/// build cannot read -- its table has no "anchorLeft", and ConsoleSetType +/// silently falls back to index 0 on a miss. +/// +/// "width"/"height" (both edges pinned) and "center"/"fill" were never +/// misleading and keep their names. "relative" gains "scale", which is what it +/// does. static EnumTable::Enums horzEnums[] = { - { GuiControl::horizResizeRight, "right" }, - { GuiControl::horizResizeWidth, "width" }, - { GuiControl::horizResizeLeft, "left" }, - { GuiControl::horizResizeCenter, "center" }, - { GuiControl::horizResizeRelative, "relative" }, - { GuiControl::horizResizeFill, "fill" } + { GuiControl::horizResizeRight, "anchorLeft" }, ///< Left edge stays put. + { GuiControl::horizResizeLeft, "anchorRight" }, ///< Right edge stays put. + { GuiControl::horizResizeWidth, "width" }, ///< Both edges stay; the width follows the parent. + { GuiControl::horizResizeCenter, "center" }, ///< Neither edge; stays centred. + { GuiControl::horizResizeRelative, "scale" }, ///< Both edges scale with the parent. + { GuiControl::horizResizeFill, "fill" }, ///< Fills the parent's inner rect. + + // Deprecated. Accepted for reading; never written. + { GuiControl::horizResizeRight, "right" }, ///< \deprecated Use anchorLeft. + { GuiControl::horizResizeLeft, "left" }, ///< \deprecated Use anchorRight. + { GuiControl::horizResizeRelative, "relative" } ///< \deprecated Use scale. }; -static EnumTable gHorizSizingTable(6, &horzEnums[0]); +static EnumTable gHorizSizingTable(9, &horzEnums[0]); static EnumTable::Enums vertEnums[] = { - { GuiControl::vertResizeBottom, "bottom" }, - { GuiControl::vertResizeHeight, "height" }, - { GuiControl::vertResizeTop, "top" }, - { GuiControl::vertResizeCenter, "center" }, - { GuiControl::vertResizeRelative, "relative" }, - { GuiControl::vertResizeFill, "fill" } + { GuiControl::vertResizeBottom, "anchorTop" }, ///< Top edge stays put. + { GuiControl::vertResizeTop, "anchorBottom" }, ///< Bottom edge stays put. + { GuiControl::vertResizeHeight, "height" }, ///< Both edges stay; the height follows the parent. + { GuiControl::vertResizeCenter, "center" }, ///< Neither edge; stays centred. + { GuiControl::vertResizeRelative, "scale" }, ///< Both edges scale with the parent. + { GuiControl::vertResizeFill, "fill" }, ///< Fills the parent's inner rect. + + // Deprecated. Accepted for reading; never written. + { GuiControl::vertResizeBottom, "bottom" }, ///< \deprecated Use anchorTop. + { GuiControl::vertResizeTop, "top" }, ///< \deprecated Use anchorBottom. + { GuiControl::vertResizeRelative, "relative" } ///< \deprecated Use scale. }; -static EnumTable gVertSizingTable(6, &vertEnums[0]); +static EnumTable gVertSizingTable(9, &vertEnums[0]); void GuiControl::initPersistFields() { From 830a8b9467777db03a029f72ca9c6c0d28fac245 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Tue, 28 Jul 2026 23:15:21 -0400 Subject: [PATCH 05/55] SimObject: stop persisting hidden and locked Both are editor scaffolding rather than properties of the document. Hidden is read from exactly one render path and only inside an edit root -- guiControl.cc renderChildControls guards it with isEditMode(), so a running game never consults it at all -- and Locked only stops the Gui Editor selecting something. Saving them wrote a working state into the file: hide a control to reach what was behind it, save, and the next person to open it gets an invisible control whose only clue is a dashed outline. They still read and set normally; they simply do not serialize. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYHdKkn3xVRLjtr226EACJ --- engine/source/sim/simObject.h | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/engine/source/sim/simObject.h b/engine/source/sim/simObject.h index b7d3bb4ca..11c74f560 100755 --- a/engine/source/sim/simObject.h +++ b/engine/source/sim/simObject.h @@ -272,13 +272,24 @@ class SimObject: public ConsoleObject, public TamlCallbacks { static_cast(object)->setLocked(dAtob(data)); return false; } + /// Neither flag is ever written. + /// + /// Both are editor scaffolding: Hidden is read only from inside an edit + /// root (guiControl.cc renderChildControls guards it with isEditMode, so a + /// running game never consults it at all) and Locked only stops the Gui + /// Editor from selecting something. Saving them put a working state into + /// the document -- hide a control to reach what was behind it, save, and + /// the next person to open the file gets an invisible control whose only + /// clue is a dashed outline. + /// + /// They still read back and set normally; they simply do not persist. static bool _writeHidden(void* object, const char* data) { - return static_cast(object)->isHidden(); + return false; } static bool _writeLocked(void* object, const char* data) { - return static_cast(object)->isLocked(); + return false; } public: From aced3ac2ba4a908a994b47f1cd2720bf7f91540f Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Tue, 28 Jul 2026 23:15:21 -0400 Subject: [PATCH 06/55] EditorIconButton: keep the disabled look while the pointer is over it The engine delivers touch events to inactive controls -- GuiControl:: findHitControl tests mVisible and mUseInput and never mActive -- so hovering a disabled button ran onTouchEnter and repainted its icon in the enabled hover colour. The disabled state was gone until something else forced it back, and the button read as clickable when it was not. Every hover and press handler is now guarded on isActive(). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYHdKkn3xVRLjtr226EACJ --- editor/EditorCore/EditorIconButton.cs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/editor/EditorCore/EditorIconButton.cs b/editor/EditorCore/EditorIconButton.cs index f7c48db9c..06d594407 100644 --- a/editor/EditorCore/EditorIconButton.cs +++ b/editor/EditorCore/EditorIconButton.cs @@ -29,25 +29,47 @@ } } +// 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 colour 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"); } function EditorIconButton::onTouchLeave(%this) { + if(!%this.isActive()) + { + return; + } %this.icon.fadeTo(ThemeManager.activeTheme.iconButtonProfile.fontColor, 200, "EaseInOut"); %this.icon.growTo("16 16", 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"); } From 8bc561245b8fa1152a46fa3d48416f08c11db872 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Tue, 28 Jul 2026 23:15:54 -0400 Subject: [PATCH 07/55] Gui Editor: replace the inspector with a custom properties pane The Gui Editor's Properties panel was the generic C++ GuiInspector, which reflects every registered persist field into flat alphabetical groups. That offers a GuiChainCtrl nine text fields it never draws, and a GuiTabPageCtrl four geometry fields its book overwrites on every layout pass -- editing either looks like it works and silently reverts. It also offered each of the 34 TypeGuiProfile slots as a flat list of every named profile in the sim, applied by name, which fails silently for anything created during the session. GuiEditorInspectorPane replaces it, following the pattern the Gui Profile Editor established: GuiEditorControlSpec is pure data describing what each class actually reads, and the pane shows that and nothing else. There is deliberately no Show All -- unlike a profile field, which some other category might use, a field hidden here is one the engine provably never looks at. Shared rows are built once and filtered with setVisible; only the class-specific sections are rebuilt, so a selection change cannot free a control the engine is mid-dispatch on. The always-visible header is one shell with swappable geometry, text and value blocks rather than twenty-nine headers, and the geometry block keys off the PARENT: a chain, grid, frame set or tab book writes its children's bounds itself, so the fields it owns are shown inert instead of pretending. Supporting widgets: GuiEditorToggleIcon (a checkbox given the whole control and no caption, which is what a toggle button is here -- and unlike a button it refuses to act while disabled), GuiEditorAnchorPicker (edge pins, because the sizing enum names read backwards), GuiEditorChoiceRow (a segmented control for the two alignments) and GuiEditorDynamicFields, built fresh because the inspector's version hardcodes 3.x profile names and registers a fixed global name that collides on the second object inspected. Profile slots follow a two-set rule. A slot shows a picker only when there is something to choose -- the theme's members for that slot's category, standalones stamped for it, or a value the theme does not offer -- and the picker then also lists uncategorised standalones. So "Any" means usable as any control's main profile, not usable in any slot; otherwise one uncategorised profile would put a row on every slot of every control. Three staleness bugs found along the way, all of which made a freshly dropped window show pickers full of the wrong profiles: - the brain adds a control (which announces it, and so inspects it) and only then applies the theme, so the pane read all five of a GuiWindowCtrl's constructor profiles as the control's own choice. It now posts Rethemed after theming. The theming cannot simply move earlier: the control's parent is what decides its category. - setTheme refreshed the explorer tree and said nothing to the pane. - fillItems re-inserts a selection the new list does not contain, which is right for the Profile Editor's font list and wrong here, where candidates are recomputed every bind. It left a ghost of the previous theme. Also drops GuiDragAndDropCtrl and GuiMenuItemCtrl from the palette (both are runtime or child-only constructs; a menu item does not even register GuiControl's fields), and gives the editor windows' sole children "fill" sizing -- their extents were measured against a 20-pixel title bar and the default has been 28 for a while, so each was clipping its last eight pixels. Covered by three new smoke suites: inspectorSpec drives the spec with no UI and guards against a control class being added without an entry, inspectorPane drives the pane itself, and inspectorVariants drives the two-set rule and the real drop order. tests/shots/inspectorPane.cs renders the four header shapes. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AYHdKkn3xVRLjtr226EACJ --- editor/GuiEditor/GuiEditor.cs | 15 +- .../scripts/GuiEditorAnchorPicker.cs | 314 +++++ editor/GuiEditor/scripts/GuiEditorBrain.cs | 17 +- .../GuiEditor/scripts/GuiEditorChoiceRow.cs | 138 ++ .../scripts/GuiEditorControlListWindow.cs | 25 +- .../GuiEditor/scripts/GuiEditorControlSpec.cs | 847 ++++++++++++ .../scripts/GuiEditorDynamicFields.cs | 274 ++++ .../scripts/GuiEditorExplorerWindow.cs | 11 +- .../GuiEditor/scripts/GuiEditorHeaderBlock.cs | 392 ++++++ .../GuiEditor/scripts/GuiEditorInspector.cs | 4 - .../scripts/GuiEditorInspectorPane.cs | 1199 +++++++++++++++++ .../scripts/GuiEditorInspectorWindow.cs | 109 +- .../GuiEditor/scripts/GuiEditorToggleIcon.cs | 142 ++ .../GuiEditor/scripts/GuiEditorToolsWindow.cs | 2 +- .../scripts/GuiProfileEditorLibrary.cs | 24 + tests/shots/inspectorPane.cs | 126 ++ tests/smoke/assetPicker.cs | 75 +- tests/smoke/inspectorPane.cs | 570 ++++++++ tests/smoke/inspectorSpec.cs | 322 +++++ tests/smoke/inspectorVariants.cs | 331 +++++ tests/smoke/standalone.cs | 54 +- 21 files changed, 4874 insertions(+), 117 deletions(-) create mode 100644 editor/GuiEditor/scripts/GuiEditorAnchorPicker.cs create mode 100644 editor/GuiEditor/scripts/GuiEditorChoiceRow.cs create mode 100644 editor/GuiEditor/scripts/GuiEditorControlSpec.cs create mode 100644 editor/GuiEditor/scripts/GuiEditorDynamicFields.cs create mode 100644 editor/GuiEditor/scripts/GuiEditorHeaderBlock.cs delete mode 100644 editor/GuiEditor/scripts/GuiEditorInspector.cs create mode 100644 editor/GuiEditor/scripts/GuiEditorInspectorPane.cs create mode 100644 editor/GuiEditor/scripts/GuiEditorToggleIcon.cs create mode 100644 tests/shots/inspectorPane.cs create mode 100644 tests/smoke/inspectorPane.cs create mode 100644 tests/smoke/inspectorSpec.cs create mode 100644 tests/smoke/inspectorVariants.cs diff --git a/editor/GuiEditor/GuiEditor.cs b/editor/GuiEditor/GuiEditor.cs index 5c1440a21..2095c0aaf 100644 --- a/editor/GuiEditor/GuiEditor.cs +++ b/editor/GuiEditor/GuiEditor.cs @@ -26,7 +26,6 @@ exec("./scripts/GuiEditorControlListWindow.cs"); exec("./scripts/GuiEditorControlListBox.cs"); exec("./scripts/GuiEditorInspectorWindow.cs"); - exec("./scripts/GuiEditorInspector.cs"); exec("./scripts/GuiEditorExplorerWindow.cs"); exec("./scripts/GuiEditorExplorerTree.cs"); exec("./scripts/GuiEditorSaveGuiDialog.cs"); @@ -51,6 +50,15 @@ exec("./scripts/GuiEditorThemeApplier.cs"); exec("./scripts/GuiEditorThemeDialog.cs"); + // The properties pane that replaced the native GuiInspector. + exec("./scripts/GuiEditorControlSpec.cs"); + exec("./scripts/GuiEditorToggleIcon.cs"); + exec("./scripts/GuiEditorChoiceRow.cs"); + exec("./scripts/GuiEditorAnchorPicker.cs"); + exec("./scripts/GuiEditorHeaderBlock.cs"); + exec("./scripts/GuiEditorDynamicFields.cs"); + exec("./scripts/GuiEditorInspectorPane.cs"); + %this.guiPage = EditorCore.RegisterEditor("Gui Editor", %this); // The theme library and the applier are both wanted before the Profile @@ -543,6 +551,11 @@ class = "GuiEditorThemeDialog"; %changed = %this.themeApplier.applyToChildren(%this.rootGui, %theme, %overrideStandalone); %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)."); } diff --git a/editor/GuiEditor/scripts/GuiEditorAnchorPicker.cs b/editor/GuiEditor/scripts/GuiEditorAnchorPicker.cs new file mode 100644 index 000000000..8d095ec98 --- /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, 72, "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, 72, "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 = "GuiEditorToggleIcon"; + 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 20; + Text = %text; + boxOffset = "0 1"; + boxExtent = "16 16"; + textOffset = "19 1"; + textExtent = (%w - 19) SPC 16; + 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 e5c45b128..b21ce25d9 100644 --- a/editor/GuiEditor/scripts/GuiEditorBrain.cs +++ b/editor/GuiEditor/scripts/GuiEditorBrain.cs @@ -30,14 +30,23 @@ %this.addNewCtrl(%payload); - // A control arrives wearing whatever its C++ constructor named - usually - // GuiDefaultProfile - 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. + // 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 addNewCtrl, because the control's parent decides + // which category it takes (a control sitting directly on the root is the + // Gui's backdrop and gets Panel, not Label). But addNewCtrl 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. %theme = GuiEditor.themeByName(GuiEditor.themeName); if(isObject(%theme)) { GuiEditor.themeApplier.applyToBranch(%payload, %theme, false); + %this.postEvent("Rethemed", %payload); } %payload.setPositionGlobal(%x, %y); diff --git a/editor/GuiEditor/scripts/GuiEditorChoiceRow.cs b/editor/GuiEditor/scripts/GuiEditorChoiceRow.cs new file mode 100644 index 000000000..dd04d4d6f --- /dev/null +++ b/editor/GuiEditor/scripts/GuiEditorChoiceRow.cs @@ -0,0 +1,138 @@ + +//----------------------------------------------------------------------------- +// 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 GuiEditorToggleIcon, 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 GuiEditorChoiceRow::onAdd(%this) +{ + ThemeManager.setProfile(%this, "emptyProfile"); + %this.choiceCount = 0; + %this.value = ""; +} + +// %icon may be "" for the entry that means "unset" -- GuiEditorToggleIcon draws +// nothing when its frame is empty, leaving a plain button. +function GuiEditorChoiceRow::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 GuiEditorChoiceRow::build(%this) +{ + %labelW = 68; + %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 = "GuiEditorToggleIcon"; + 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; + } +} + +//----------------------------------------------------------------------------- +// 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 GuiEditorChoiceRow::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 GuiEditorChoiceRow::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 GuiEditorChoiceRow::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 GuiEditorChoiceRow::setEnabled(%this, %enabled) +{ + %this.label.setActive(%enabled); + for(%i = 0; %i < %this.choiceCount; %i++) + { + %this.choiceButton[%i].setActive(%enabled); + } +} diff --git a/editor/GuiEditor/scripts/GuiEditorControlListWindow.cs b/editor/GuiEditor/scripts/GuiEditorControlListWindow.cs index 321cb1d6b..0ebc1f9c6 100644 --- a/editor/GuiEditor/scripts/GuiEditorControlListWindow.cs +++ b/editor/GuiEditor/scripts/GuiEditorControlListWindow.cs @@ -2,10 +2,16 @@ function GuiEditorControlListWindow::onAdd(%this) { + // "fill", not "width"/"height": this is the window's only child, so it wants + // the whole content rect. Those two only preserve whatever gap the authored + // extent started with, and the authored extent was measured against a 20-pixel + // title bar -- the default is 28 now, so every one of these windows was + // clipping its last eight pixels. Fill measures against the parent's inner + // rect, which already has the title taken out of it, so it cannot drift again. %this.scroller = new GuiScrollCtrl() { - HorizSizing="width"; - VertSizing="height"; + HorizSizing="fill"; + VertSizing="fill"; Position="0 0"; Extent="242 355"; hScrollBar="alwaysOff"; @@ -43,6 +49,14 @@ class = "GuiEditorControlListBox"; } } +// The palette is what a person can drag into a Gui, which is not the same as +// every class deriving from GuiControl. Two kinds are filtered out: the ones +// that are editor or engine plumbing (the console, the edit control, the +// inspector types), and the ones that are real controls but are never placed by +// hand -- GuiDragAndDropCtrl is created at runtime to carry a drag payload, and +// GuiMenuItemCtrl only means anything as a child of a menu bar (it does not +// even register GuiControl's fields, calling SimObject::initPersistFields +// directly, so it has no profile, position or extent to give it). function GuiEditorControlListWindow::populate(%this) { %controls = enumerateConsoleClasses("GuiControl"); @@ -51,10 +65,11 @@ class = "GuiEditorControlListBox"; { %field = getField(%controls, %i); - if(%field !$= "GuiCanvas" && (%field $= "SceneWindow" || getSubStr(%field, 0, 3) $= "Gui") && - getSubStr(%field, 0, 10) !$= "GuiConsole" && getSubStr(%field, 0, 7) !$= "GuiEdit" && + 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") + %field !$= "GuiParticleGraphInspector" && %field !$= "GuiGraphCtrl" && %field !$= "GuiSceneObjectCtrl" && + %field !$= "GuiDragAndDropCtrl" && %field !$= "GuiMenuItemCtrl") { %this.listBox.addItem(%field); } diff --git a/editor/GuiEditor/scripts/GuiEditorControlSpec.cs b/editor/GuiEditor/scripts/GuiEditorControlSpec.cs new file mode 100644 index 000000000..a8ee20c97 --- /dev/null +++ b/editor/GuiEditor/scripts/GuiEditorControlSpec.cs @@ -0,0 +1,847 @@ + +//----------------------------------------------------------------------------- +// 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, parentGroup, and every TypeGuiCursor field -- GuiCursor +// has no category and GuiProfileTheme has no cursor table, so cursors cannot +// join the Variants scheme the profile slots use. +// +// 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, and GuiImageButtonCtrl draws only its asset. + "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 + "GuiImageButtonCtrl" 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("GuiImageButtonCtrl", "Images", "Images", + "HoverImage DownImage InactiveImage"); + + %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"); + %this.addSection("GuiTreeViewCtrl", "Tree", "Tree", "AllowReorder"); + %this.addSection("GuiTreeViewCtrl", "List", "List", + "AllowMultipleSelections FitParentWidth"); + + %this.addSection("GuiMenuBarCtrl", "Scrollbar", "Scroll Bar", + "constantThumbHeight showArrowButtons scrollBarThickness"); + // IsOn is the header's principal value, so the section carries only the two + // fields that decide how the item behaves when it is picked. + %this.addSection("GuiMenuItemCtrl", "Item", "Menu Item", "Toggle Radio"); + + %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 are a second icon row rather than six checkboxes; + // the pane reads this key specially. Kept here so the field list stays in + // one place. + %this.addSection("GuiWindowCtrl", "Window", "Window", + "canMove canClose canMinimize canMaximize resizeWidth resizeHeight"); + %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]; +} + +//----------------------------------------------------------------------------- +// 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["GuiImageButtonCtrl"] = "NormalImage"; + %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"; + %this.headerValues["GuiMenuItemCtrl"] = "IsOn"; + + // 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"; +} + +function GuiEditorControlSpec::easingFields(%this) +{ + return "easeFillColorHL easeFillColorSL easeTimeFillColorHL easeTimeFillColorSL"; +} + +// The toggles the header draws as icons instead of rows. Visible, Active and +// useInput are what the control does when the game runs; hidden and locked are +// what the editor does with it. +function GuiEditorControlSpec::runtimeToggles(%this) +{ + return "Visible Active useInput"; +} + +function GuiEditorControlSpec::editorToggles(%this) +{ + return "hidden locked"; +} + +// 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 +function GuiEditorControlSpec::geometryModeOf(%this, %ctrl) +{ + if(!isObject(%ctrl)) + { + return "full"; + } + + // 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"; +} + +// 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 $= "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 "chainV": return "x"; + case "chainH": 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; + } + 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(); +} + +// The header shows a text box for the roles that have a string of their own; +// inherited and proxy put theirs in a collapsed section instead, because a grid +// that can technically draw text is not a control anyone reaches for text on. +function GuiEditorControlSpec::textBelongsInHeader(%this, %class) +{ + %role = %this.textRoleFor(%class); + return %role $= "render" || %role $= "caption" || %role $= "placeholder"; +} + +// 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" or "float": return "number"; + case "enumval": return "enum"; + case "Point2I" or "Point2F" or "Vector2": return "point"; + case "ColorI" or "ColorF" or "FluidColorI": return "color"; + case "filename": return "file"; + case "assetIdString": return "asset"; + case "GuiProfile": return "profile"; + 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["tooltipWidth"] = "Tooltip Width"; + %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["NormalImage"] = "Normal"; + %this.label["HoverImage"] = "Hover"; + %this.label["DownImage"] = "Down"; + %this.label["InactiveImage"] = "Inactive"; + %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"; +} + +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/GuiEditorDynamicFields.cs b/editor/GuiEditor/scripts/GuiEditorDynamicFields.cs new file mode 100644 index 000000000..17b569c75 --- /dev/null +++ b/editor/GuiEditor/scripts/GuiEditorDynamicFields.cs @@ -0,0 +1,274 @@ + +//----------------------------------------------------------------------------- +// 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 = "GuiProfileEditorFieldRow"; + 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.onProfileRowReset 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::onProfileRowCommit(%this, %row) +{ + if(!isObject(%this.target) || !%row.hasChanged()) + { + return; + } + + %value = %row.getValue(); + %this.target.setFieldValue(%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::onProfileRowReset(%this, %row) +{ + if(!isObject(%this.target)) + { + return; + } + + %this.target.setFieldValue(%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/GuiEditorExplorerWindow.cs b/editor/GuiEditor/scripts/GuiEditorExplorerWindow.cs index 5f6eb6431..6d0b9be95 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"; @@ -31,7 +33,10 @@ }; ThemeManager.setProfile(%this.tree, "treeViewProfile"); %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..d923af208 --- /dev/null +++ b/editor/GuiEditor/scripts/GuiEditorHeaderBlock.cs @@ -0,0 +1,392 @@ + +//----------------------------------------------------------------------------- +// 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 split on purpose. hidden and locked are icon buttons, which +// is what they want to be and what the icon sheet has art for (an open and a +// closed padlock). Visible, Active and Accepts Input stay labelled checkboxes: +// the sheet has an eye, but nothing that reads as "active" or "accepts input" +// without a caption -- and a row of icons that each need a tooltip to be +// understood is worse than three short words. +//----------------------------------------------------------------------------- + +function GuiEditorHeaderBlock::onAdd(%this) +{ + ThemeManager.setProfile(%this, "emptyProfile"); +} + +function GuiEditorHeaderBlock::build(%this) +{ + %this.rowFields = ""; + %this.valueFields = ""; + + %this.buildIdentity(); + %this.buildToggles(); + %this.buildGeometry(); + %this.buildText(); + + // The value grid starts empty; bindClass fills it, because what belongs + // there is the one thing here that changes with the class. + %this.valueGrid = %this.pane.makeCellGrid(0); + %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", ""); + %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; + + // Toggles rather than buttons: these hold a value, so they are checkboxes + // wearing an icon (GuiEditorToggleIcon). frameOn is the icon for the "on" + // reading -- a closed padlock against an open one, and an x in a circle + // against a tick. The x/tick pair is still a placeholder: an eye and a + // struck-through eye is what "hidden" actually wants, and the sheet now + // carries both ($EditorIcon::eye and ::invisible_light). Left alone here + // because hidden is moving out to its own Explorer tree column, and that is + // the change that should pick the icon. + %this.hiddenButton = %this.makeIconToggle(%row, 4, "hidden", + $EditorIcon::round_delete, $EditorIcon::round_checkmark, + "Hidden in the editor", "Shown in the editor"); + %this.lockedButton = %this.makeIconToggle(%row, 32, "locked", + $EditorIcon::padlock_closed, $EditorIcon::padlock_open, + "Locked against editing", "Unlocked"); + + // 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. Six icons fit where + // 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. + %this.visibleButton = %this.makeIconToggle(%row, 68, "Visible", + $EditorIcon::eye, $EditorIcon::invisible_light, + "Draws when the game runs", "Does not draw when the game runs"); + %this.activeButton = %this.makeIconToggle(%row, 96, "Active", + $EditorIcon::on, $EditorIcon::off, + "Responds when the game runs", "Inert when the game runs"); + %this.inputButton = %this.makeIconToggle(%row, 124, "useInput", + $EditorIcon::cursor_arrow, $EditorIcon::cursor_arrow, + "Touch and key events reach this control", + "Touch and key events pass straight through"); + %this.containerButton = %this.makeIconToggle(%row, 152, "isContainer", + $EditorIcon::folder_open, $EditorIcon::folder, + "The editor may drop controls into this one", + "The editor will not drop controls into this one"); +} + +// 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, %frameOn, %frameOff, %tipOn, %tipOff) +{ + %button = new GuiCheckBoxCtrl() + { + class = "GuiEditorToggleIcon"; + Position = %x SPC 2; + Extent = "24 24"; + frameOn = %frameOn; + frameOff = %frameOff; + tipOn = %tipOn; + tipOff = %tipOff; + toggleName = %field; + 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); +} + +function GuiEditorHeaderBlock::makeToggleBox(%this, %row, %x, %w, %field, %label, %tip) +{ + %box = new GuiCheckBoxCtrl() + { + Position = %x SPC 3; + Extent = %w SPC 22; + Text = %label; + boxOffset = "0 2"; + boxExtent = "18 18"; + textOffset = "22 2"; + textExtent = (%w - 22) SPC 18; + Tooltip = %tip; + Command = %this.getID() @ ".onToggleClicked(\"" @ %field @ "\");"; + }; + ThemeManager.setProfile(%box, "checkboxProfile"); + ThemeManager.setProfile(%box, "tipProfile", "TooltipProfile"); + %row.add(%box); + + %box.fieldName = %field; + return %box; +} + +function GuiEditorHeaderBlock::onToggleClicked(%this, %field) +{ + %this.pane.onToggleChanged(%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."; + + %this.anchorPicker.setAxisEnabled( + %spec.isGeometryFieldLive(%mode, "HorizSizing"), + %spec.isGeometryFieldLive(%mode, "VertSizing")); + %this.extentRow.setEnabled(%spec.isGeometryFieldLive(%mode, "Extent"), %reason); + + // Position is the one field whose two axes can disagree: a vertical chain + // stacks its children on Y and copies X straight back from the child, so + // half of this row is live. The row's own setEnabled works on both boxes at + // once, so the axes are set directly -- the same two widgets it would touch. + %axes = %spec.livePositionAxes(%mode); + %liveX = strstr(%axes, "x") >= 0; + %liveY = strstr(%axes, "y") >= 0; + + %this.positionRow.editor.setActive(%liveX); + %this.positionRow.editorY.setActive(%liveY); + %this.positionRow.editor.Tooltip = %liveX ? "" : %reason; + %this.positionRow.editorY.Tooltip = %liveY ? "" : %reason; +} + +//----------------------------------------------------------------------------- +// Text. +//----------------------------------------------------------------------------- + +function GuiEditorHeaderBlock::buildText(%this) +{ + %grid = %this.pane.makeCellGrid(0); + %this.add(%grid); + %this.textGrid = %grid; + + %this.textRow = %this.pane.addFieldRow(%grid, "text", "Text", "text", ""); + + // The two alignments are segmented rows rather than drop-downs: three or + // four values, each with a picture, and which one is set matters more than + // its name. They sit directly under the text box, since that is what they + // are about. + // + // "default" leads each row and wears no icon. It is not an absence -- it is + // the value a control starts on, which getAlignmentType resolves to the + // PROFILE's alignment -- so it needs a button, just not a picture. + %this.alignRow = %this.makeAlignRow(%grid, "align", "Align", + "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.makeAlignRow(%grid, "vAlign", "V-Align", + "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. The "default" +// entry is prepended here so both rows spell it the same way. +function GuiEditorHeaderBlock::makeAlignRow(%this, %grid, %field, %label, %choices) +{ + %row = new GuiControl() + { + class = "GuiEditorChoiceRow"; + Position = "0 0"; + labelText = %label; + 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; +} + +// One of the alignment rows was clicked. +function GuiEditorHeaderBlock::onChoiceRowChanged(%this, %row) +{ + %this.pane.onHeaderChoiceChanged(%row.fieldName, %row.getValue()); +} + +//----------------------------------------------------------------------------- +// 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); + %this.geometryGrid.setVisible(!%bare); + %this.visibleButton.setVisible(!%bare); + %this.activeButton.setVisible(!%bare); + %this.inputButton.setVisible(!%bare); + + // 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 roles that have a string of + // their own worth naming. A grid can technically draw text; it goes in a + // collapsed section instead of the first thing anyone sees. + %inHeader = %spec.textBelongsInHeader(%class); + %this.textGrid.setVisible(%inHeader); + if(%inHeader) + { + %this.textRow.setLabelText(%spec.textLabelFor(%class)); + %this.alignRow.setVisible(%spec.isFieldVisible(%class, "align")); + %this.vAlignRow.setVisible(%spec.isFieldVisible(%class, "vAlign")); + } + + %this.buildValueRows(%ctrl, %class); + %this.resizeToFit(); +} + +// 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 = ""; + + %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); + } + + %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..a8bcd2025 --- /dev/null +++ b/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs @@ -0,0 +1,1199 @@ + +//----------------------------------------------------------------------------- +// 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"); + + %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; + %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.buildSection("Text", "Text", %this.spec.textFields()); + + // 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"); + %this.buildSection("Animation", "Animation", %this.spec.easingFields()); + %this.buildSection("Tooltip", "Tooltip", %this.spec.tooltipFields()); + %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). +function GuiEditorInspectorPane::makeCellGrid(%this, %y) +{ + %grid = new GuiGridCtrl() + { + HorizSizing = "width"; + Position = "0" SPC %y; + Extent = %this.paneWidth 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; +} + +// 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; +} + +function GuiEditorInspectorPane::buildSection(%this, %key, %title, %fields) +{ + %panel = %this.makeSectionPanel(%title); + %this.add(%panel); + + %grid = %this.makeCellGrid(24); + %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)); + } +} + +function GuiEditorInspectorPane::addFieldRow(%this, %container, %field, %label, %kind, %enumItems) +{ + %row = new GuiControl() + { + class = "GuiProfileEditorFieldRow"; + Position = "0 0"; + 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. + %row.resetButton.setVisible(false); + + %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"; + case "fontSizeAdjust": return "number"; + 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.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. +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); + if(%ctrl.getFieldType(%field) !$= "GuiProfile" || %field $= "Profile") + { + continue; + } + if(getWordCount(%this.profileAnchors(%ctrl, %field)) <= 1) + { + continue; + } + %fields = (%fields $= "") ? %field : (%fields SPC %field); + } + return %fields; +} + +// 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; + + %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 header carries the text block for the roles that have a caption worth + // naming; the shared Text section is for the rest, so the two never both + // show it. + %inHeader = %spec.textBelongsInHeader(%class); + %textCount = getWordCount(%spec.textFields()); + for(%i = 0; %i < %textCount; %i++) + { + %field = getWord(%spec.textFields(), %i); + %row = %this.row[%field]; + if(isObject(%row) && %inHeader) + { + %row.setVisible(false); + } + } + + // 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(%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)); + + %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(); +} + +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. + if(!isObject(%row) || %this.isProfileSlot(%field)) + { + continue; + } + %row.setValue(%this.readField(%field)); + } + + // The alignment rows are segmented controls rather than field rows, so they + // are not in rowFields and load here. + %this.header.alignRow.setValue(%this.target.getFieldValue("align")); + %this.header.vAlignRow.setValue(%this.target.getFieldValue("vAlign")); + + %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; + } + + // 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); + + %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) +{ + // setEditFieldValue rather than a plain assignment: it brackets the write + // with inspectPreApply / inspectPostApply, which is what lets a control + // react to being re-profiled or resized from here. + %this.target.setEditFieldValue(%field, %value); +} + +function GuiEditorInspectorPane::refreshToggles(%this) +{ + %header = %this.header; + + %header.hiddenButton.setValue(%this.target.hidden); + %header.lockedButton.setValue(%this.target.locked); + + %header.visibleButton.setValue(%this.target.Visible); + %header.activeButton.setValue(%this.target.Active); + %header.inputButton.setValue(%this.target.useInput); + %header.containerButton.setValue(%this.target.isContainer); +} + +// 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 lets the pane decide what the value becomes, +// so that every write to the control still goes through one place. +function GuiEditorInspectorPane::onToggleChanged(%this, %field) +{ + if(%this.populating || !isObject(%this.target)) + { + return; + } + + // Every widget in the row holds its own state and has already flipped it, so + // the value is read back rather than derived -- the control and the widget + // can never disagree about what was just asked for. + %header = %this.header; + switch$(%field) + { + case "hidden": + %this.writeField("hidden", %header.hiddenButton.getValue()); + case "locked": + %this.writeField("locked", %header.lockedButton.getValue()); + case "Visible": + %this.writeField("Visible", %header.visibleButton.getValue()); + case "Active": + %this.writeField("Active", %header.activeButton.getValue()); + case "useInput": + %this.writeField("useInput", %header.inputButton.getValue()); + case "isContainer": + %this.writeField("isContainer", %header.containerButton.getValue()); + } + + %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 ""; + } + + %library = GuiEditor.themeLibrary; + %theme = GuiEditor.themeByName(GuiEditor.themeName); + + %list = isObject(%theme) ? %theme.getProfiles(%category) : ""; + %list = %this.addUnique(%list, %library.getStandaloneProfiles(%category)); + + %current = GuiEditor.themeApplier.fieldProfile(%ctrl, %field); + 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) +{ + %applier = GuiEditor.themeApplier; + %isRoot = (%ctrl.getParent() == %applier.rootContainer); + %main = %applier.categoryForControl(%ctrl, %isRoot); + return %applier.categoryForField(%field, %main); +} + +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.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)) + { + %this.fillProfileRow(%row, %field); + } + } +} + +// 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::onProfileRowCommit(%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 anyway would put an edit in the undo + // record -- and mark the Gui dirty -- for something that never happened. + if(!%row.hasChanged()) + { + return; + } + + %field = %row.fieldName; + + // 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 + { + %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::isSpriteSourceField(%this, %field) +{ + return %this.spec.listHas("Image Animation Bitmap", %field); +} + +function GuiEditorInspectorPane::rebindDeferred(%this) +{ + if(isObject(%this.target)) + { + %this.bind(%this.target); + } +} + +// The row widget's reset means "back to the theme's stamped value", which has +// no analogue for a control, so the button is hidden and this can never fire. +// It exists because the row's owner contract names it. +function GuiEditorInspectorPane::onProfileRowReset(%this, %row) +{ +} + +// 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 fc26876f5..6f375222e 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,45 +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, "overrideLabelProfile", "OverrideLabelProfile"); - 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) @@ -70,9 +52,47 @@ } } +// 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); + } +} + +// 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) @@ -83,7 +103,7 @@ %count = %this.inspectList.getCount(); if(%count > 0) { - %this.inspector.inspect(%this.inspectList.getObject(%count - 1)); + %this.pane.bind(%this.inspectList.getObject(%count - 1)); } } } @@ -91,13 +111,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/GuiEditorToggleIcon.cs b/editor/GuiEditor/scripts/GuiEditorToggleIcon.cs new file mode 100644 index 000000000..c3b411e29 --- /dev/null +++ b/editor/GuiEditor/scripts/GuiEditorToggleIcon.cs @@ -0,0 +1,142 @@ + +//----------------------------------------------------------------------------- +// 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 colour 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 colour. 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 GuiEditorToggleIcon::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"; + + %this.icon = new GuiSpriteCtrl() + { + HorizSizing = "center"; + VertSizing = "center"; + Extent = "16 16"; + MinExtent = "16 16"; + Position = "0 0"; + Image = "EditorCore:EditorIcons16"; + ImageSize = "16 16"; + constrainProportions = "1"; + fullSize = "0"; + Frame = %this.frameOff; + UseInput = false; + }; + ThemeManager.setProfile(%this.icon, "spriteProfile"); + %this.add(%this.icon); + + %this.Command = %this.getID() @ ".onToggled();"; + %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 GuiEditorToggleIcon::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 GuiEditorToggleIcon::setValue(%this, %on) +{ + %this.setStateOn(%on); + %this.refresh(); +} + +function GuiEditorToggleIcon::getValue(%this) +{ + return %this.getStateOn(); +} + +// The single place the icon's look is decided: which frame, which tooltip, and +// which of the profile's font colours tints it. +function GuiEditorToggleIcon::refresh(%this) +{ + %on = %this.getStateOn(); + %profile = ThemeManager.activeTheme.iconButtonProfile; + + // 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 GuiEditorChoiceRow 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); + } + + if(!%this.isActive()) + { + %this.icon.setImageColor(%profile.fontColorNA); + } + else + { + %this.icon.setImageColor(%on ? %profile.fontColorHL : %profile.fontColor); + } + + %tip = %on ? %this.tipOn : %this.tipOff; + if(%tip !$= "") + { + %this.Tooltip = %tip; + } +} + +// setActive does not repaint on its own, and the disabled tint is ours to draw. +function GuiEditorToggleIcon::onActive(%this) +{ + %this.refresh(); +} + +function GuiEditorToggleIcon::onInactive(%this) +{ + %this.refresh(); +} diff --git a/editor/GuiEditor/scripts/GuiEditorToolsWindow.cs b/editor/GuiEditor/scripts/GuiEditorToolsWindow.cs index ee93fafb7..cd1cc27ab 100644 --- a/editor/GuiEditor/scripts/GuiEditorToolsWindow.cs +++ b/editor/GuiEditor/scripts/GuiEditorToolsWindow.cs @@ -14,7 +14,7 @@ %this.add(%this.buttonBar); %this.buttonBar.addButton("onProfileEditor", $EditorIcon::doc_edit, "Open the Gui Profile Editor", ""); - %this.buttonBar.addButton("onSetTheme", $EditorIcon::grid_2x2, "Set this Gui's theme", ""); + %this.buttonBar.addButton("onSetTheme", $EditorIcon::brush, "Set this Gui's theme", ""); } function GuiEditorToolsWindow::onRemove(%this) diff --git a/editor/GuiEditor/scripts/GuiProfileEditorLibrary.cs b/editor/GuiEditor/scripts/GuiProfileEditorLibrary.cs index 4ebf53f70..f56d79f62 100644 --- a/editor/GuiEditor/scripts/GuiProfileEditorLibrary.cs +++ b/editor/GuiEditor/scripts/GuiProfileEditorLibrary.cs @@ -221,6 +221,30 @@ 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) diff --git a/tests/shots/inspectorPane.cs b/tests/shots/inspectorPane.cs new file mode 100644 index 000000000..9e5249e98 --- /dev/null +++ b/tests/shots/inspectorPane.cs @@ -0,0 +1,126 @@ +// Visual harness for the Gui Editor's properties pane. Screenshots the header +// archetypes side by side, which is the thing the design turns on: one shell, +// with the geometry, text and value blocks swapped per control. +// +// The four cases are the ones that look different from each other: +// button the ordinary shape -- full geometry, a text block, easing +// window the most secondary fields of anything, plus a title +// tabpage no geometry at all; the book owns every bit of it +// chainkid one axis owned, the other still the control's +// +// Run: tests/run.ps1 -Shots inspectorPane ; look in shots/. + +setLogMode(1); +$Scripts::ignoreDSOs = true; +setScriptExecEcho(false); +trace(false); + +testExec("editor/main.cs"); +schedule(2500, 0, "sOpenProject"); + +// A real project, loaded the way the project selector does it, because the pane +// has to be drawn wearing a project's theme to be worth looking at. +function sOpenProject() +{ + ProjectManager.setProjectFolder("PlanetX"); + EditorCore.projectSelector.onProjectSelected(pathConcat(getMainDotCsDir(), "PlanetX")); + schedule(2500, 0, "sOpenEditor"); +} + +// Loading AppCore starts the project's game over the canvas; the editor comes +// back the way Ctrl+~ does it. Pages register in load order: EditorConsole, +// ProjectManager, AssetAdmin, GuiEditor. +function sOpenEditor() +{ + EditorCore.toggleEditor(); + EditorCore.tabBook.selectPage(3); + schedule(1500, 0, "sStep1"); +} + +function sStep1() +{ + // A theme, so the pane is drawn wearing what a real project would. + $sTheme = GuiEditor.themeLibrary.createTheme("PaneShotTheme"); + GuiEditor.themeName = $sTheme.getName(); + + // One of each interesting shape, parked in the Gui being edited. + $sButton = sPlace("GuiButtonCtrl"); + $sWindow = sPlace("GuiWindowCtrl"); + + %book = sPlace("GuiTabBookCtrl"); + $sPage = new GuiTabPageCtrl(); + %book.add($sPage); + GuiEditor.themeApplier.applyToBranch($sPage, $sTheme, true); + + %chain = sPlace("GuiChainCtrl"); + %chain.IsVertical = true; + $sChainKid = new GuiButtonCtrl(); + %chain.add($sChainKid); + GuiEditor.themeApplier.applyToBranch($sChainKid, $sTheme, true); + + // A dynamic field and a second WindowContent, so the two sections that only + // appear when they have something to say are in the window's shot. + $sWindow.myTag = "example"; + $sTheme.createProfile("WindowContent"); + + createPath(testRoot("shots/")); + + // case TAB object global + $sCases = + "pane-button" TAB "$sButton" NL + "pane-window" TAB "$sWindow" NL + "pane-tabpage" TAB "$sPage" NL + "pane-chainkid" TAB "$sChainKid"; + $sIndex = 0; + schedule(1000, 0, "sShoot"); +} + +function sPlace(%class) +{ + %ctrl = eval("return new " @ %class @ "();"); + GuiEditor.rootGui.add(%ctrl); + GuiEditor.themeApplier.applyToBranch(%ctrl, $sTheme, true); + return %ctrl; +} + +function sShoot() +{ + %rec = getRecord($sCases, $sIndex); + %name = getField(%rec, 0); + %ctrl = eval("return " @ getField(%rec, 1) @ ";"); + + %pane = GuiEditor.inspectorWindow.pane; + %pane.bind(%ctrl); + + // Every section open, so the shot shows what each control actually offers + // rather than a column of collapsed headers. + %panels = %pane.panelList SPC %pane.classPanels; + for(%i = 0; %i < getWordCount(%panels); %i++) + { + %panel = %pane.panel[getWord(%panels, %i)]; + if(isObject(%panel) && %panel.isVisible()) + { + %panel.setExpanded(true); + } + } + %pane.dynamicPanel.setExpanded(true); + + // Let the chain, the grids and the panels settle before grabbing. + schedule(500, 0, "sGrab", %name); +} + +function sGrab(%name) +{ + screenShot(testRoot("shots/" @ %name @ ".png"), "PNG"); + echo("SHOT: wrote " @ %name @ ".png"); + + $sIndex++; + if($sIndex < getRecordCount($sCases)) + { + schedule(300, 0, "sShoot"); + return; + } + + echo("SHOT DONE"); + schedule(300, 0, "quit"); +} diff --git a/tests/smoke/assetPicker.cs b/tests/smoke/assetPicker.cs index 5cbe5e13d..aef0c8a30 100644 --- a/tests/smoke/assetPicker.cs +++ b/tests/smoke/assetPicker.cs @@ -340,53 +340,44 @@ function fStep6() } //----------------------------------------------------------------------------- -// The other caller: the native inspector's browse button. This is the path the -// engine change opened up -- GuiInspectorTypeAsset builds a "..." button and -// bakes a call to EditorCore.openAssetPicker into its Command. +// The other caller: the Find button on an asset field in the Gui Editor's +// properties pane. +// +// This block used to drive the native GuiInspector, whose GuiInspectorTypeAsset +// baked an EditorCore.openAssetPicker call straight into a "..." button's +// Command. The Gui Editor no longer builds an inspector -- GuiEditorInspectorPane +// replaced it -- so the path under test is now GuiProfileEditorFieldRow's +// "asset" kind, which routes the click through onFindAssetClicked instead of a +// baked-in command string. Same promise, one indirection later. +// +// GuiInspectorTypeAsset itself is still live: editor/AssetAdmin/AssetInspector.cs +// builds a GuiInspector. Nothing covers that one, which is a gap this change +// created rather than closed. //----------------------------------------------------------------------------- -function fFindBrowseButton(%ctrl) -{ - if(strstr(%ctrl.Command, "openAssetPicker") != -1) - { - return %ctrl; - } - for(%i = 0; %i < %ctrl.getCount(); %i++) - { - %found = fFindBrowseButton(%ctrl.getObject(%i)); - if(isObject(%found)) - { - return %found; - } - } - return 0; -} - function fStep7() { - // A Sprite's Image field is a TypeImageAssetPtr, which is what gets the - // browse button. Driven through the Gui Editor's own inspector rather than a - // fresh one: a detached inspector never wakes, and an unwoken control does - // not build its field editors, so there would be no button to find. - $fSprite = new Sprite(); - $fInspector = GuiEditor.inspectorWindow.inspector; - fCheck("the editor's inspector is available", isObject($fInspector)); - $fInspector.inspect($fSprite); - - %browse = fFindBrowseButton($fInspector); - fCheck("inspector built a browse button for the asset field", isObject(%browse)); - fCheck("browse button calls the editor's picker", - strstr(%browse.Command, "EditorCore.openAssetPicker(") != -1); - fCheck("browse button passes the asset type", - strstr(%browse.Command, "ImageAsset") != -1); - fCheck("browse button passes apply as the callback method", - strstr(%browse.Command, "\"apply\"") != -1); + // A GuiSpriteCtrl's Image field is a TypeAssetId, which is what gets the + // Find button. It has to be a Gui control now rather than a Sprite: the + // pane binds controls, and a scene object was only ever usable here because + // the old inspector took any SimObject. + $fSprite = new GuiSpriteCtrl(); + GuiEditor.rootGui.add($fSprite); + + $fPane = GuiEditor.inspectorWindow.pane; + fCheck("the editor's properties pane is available", isObject($fPane)); + $fPane.bind($fSprite); + + %row = $fPane.row["Image"]; + fCheck("pane built a row for the asset field", isObject(%row)); + fCheck("the asset field got an asset row", isObject(%row) && %row.kind $= "asset"); + fCheck("asset row has a Find button", isObject(%row) && isObject(%row.findButton)); // Run exactly what a click would run. - eval(%browse.Command); + eval(%row.findButton.Command); %picker = fPicker(); - fCheck("the browse button opened the picker", isObject(%picker)); + fCheck("the Find button opened the picker", isObject(%picker)); fCheck("the picker opened on the right asset type", %picker.assetType $= "ImageAsset"); %item = %picker.grid.getObject(0); @@ -399,11 +390,11 @@ function fStep7() function fStep8() { - fCheck("choosing wrote the asset onto the inspected object", + fCheck("choosing wrote the asset onto the bound control", $fSprite.Image $= $fInspectorChoice); - fCheck("the inspector's picker closed", !isObject(fPicker())); + fCheck("the pane's picker closed", !isObject(fPicker())); - $fInspector.clear(); + $fPane.unbind(); $fSprite.delete(); // Land on a border node before quitting. Quitting with a profile node diff --git a/tests/smoke/inspectorPane.cs b/tests/smoke/inspectorPane.cs new file mode 100644 index 000000000..8d4679e0d --- /dev/null +++ b/tests/smoke/inspectorPane.cs @@ -0,0 +1,570 @@ +// Properties-pane smoke test. Drives GuiEditorInspectorPane -- the custom pane +// that replaced the native GuiInspector in the Gui Editor -- through a control +// of each family, checking that what it shows matches what the class can +// actually use, that a commit reaches the control, and that reparenting into a +// layout container makes the geometry it no longer owns go inert. +// Run: tests/run.ps1 inspectorPane ; grep IPSMOKE in console.log. + +setLogMode(1); +$Scripts::ignoreDSOs = true; +setScriptExecEcho(false); +trace(false); + +function pCheck(%label, %cond) +{ + if(%cond) echo("IPSMOKE PASS: " @ %label); + else echo("IPSMOKE FAIL: " @ %label); +} + +// Drop a control into the Gui being edited and select it, which is the path a +// real selection takes -- the brain themes it on arrival and posts Inspect. +function pAdd(%class) +{ + %ctrl = eval("return new " @ %class @ "();"); + GuiEditor.rootGui.add(%ctrl); + + %theme = GuiEditor.themeByName(GuiEditor.themeName); + if(isObject(%theme)) + { + GuiEditor.themeApplier.applyToBranch(%ctrl, %theme, false); + } + return %ctrl; +} + +function pBind(%ctrl) +{ + GuiEditor.inspectorWindow.pane.bind(%ctrl); + return GuiEditor.inspectorWindow.pane; +} + +// Is a row on show? A row that was never built is not visible either, and +// isVisible() on nothing is false -- so ask both, or a missing row would read +// as a deliberate hide (which is exactly how a stale check once passed for +// years; see tests/README.md). +function pRowShown(%pane, %field) +{ + %row = %pane.row[%field]; + return isObject(%row) && %row.isVisible(); +} + +function pRowBuilt(%pane, %field) +{ + return isObject(%pane.row[%field]); +} + +// Does the control really carry this dynamic field? Comparing the value against +// "" proves nothing: an absent field and an empty one read back identically, +// because an empty one is exactly what the engine deletes. +function pHasDynamicField(%ctrl, %name) +{ + for(%i = 0; %i < %ctrl.getDynamicFieldCount(); %i++) + { + if(getWord(%ctrl.getDynamicField(%i), 0) $= %name) + { + return true; + } + } + return false; +} + +testExec("editor/main.cs"); +schedule(2000, 0, "pStep1"); + +//----------------------------------------------------------------------------- +// The pane exists and replaced the inspector. +//----------------------------------------------------------------------------- + +function pStep1() +{ + ProjectManager.setProjectFolder("inspectorPaneSmokeProject"); + GuiEditor.open(); + + %w = GuiEditor.inspectorWindow; + pCheck("inspector window exists", isObject(%w)); + pCheck("pane built", isObject(%w.pane)); + pCheck("native inspector gone", !isObject(%w.inspector)); + pCheck("pane owns a spec", isObject(%w.pane.spec)); + pCheck("header built", isObject(%w.pane.header)); + + schedule(200, 0, "pStep2"); +} + +//----------------------------------------------------------------------------- +// A button: text in the header, easing shown, no class sections beyond that. +//----------------------------------------------------------------------------- + +function pStep2() +{ + $pButton = pAdd("GuiButtonCtrl"); + %pane = pBind($pButton); + + pCheck("bound to the button", %pane.target == $pButton); + pCheck("pane visible once bound", %pane.isVisible()); + pCheck("button text is in the header", %pane.header.textGrid.isVisible()); + + // --- Alignment is a segmented row per axis, "default" first and blank. --- + // "default" is not an absence: getAlignmentType resolves it to the profile's + // own alignment, and the engine's tables only started exposing it with this + // change (the count said 3 while the array held 4). + %align = %pane.header.alignRow; + pCheck("align row has four choices", %align.choiceCount == 4); + pCheck("align row leads with default", %align.choiceValue[0] $= "default"); + pCheck("the default choice has no icon", %align.choiceIcon[0] $= ""); + pCheck("the default button draws no icon", !%align.choiceButton[0].icon.isVisible()); + pCheck("the other three do", %align.choiceButton[1].icon.isVisible()); + pCheck("v-align row has four choices", %pane.header.vAlignRow.choiceCount == 4); + + $pButton.align = "default"; + %pane.refresh(); + pCheck("default reads back by name now the table exposes it", + $pButton.align $= "default"); + pCheck("row shows default chosen", %align.getValue() $= "default"); + pCheck("only the default button is down", + %align.choiceButton[0].getValue() && !%align.choiceButton[2].getValue()); + + // Picking one writes it and moves the pressed button. + %align.choiceButton[2].performClick(); + pCheck("choosing centre reached the control", $pButton.align $= "center"); + pCheck("the chosen button is down", %align.choiceButton[2].getValue()); + pCheck("and the previous one is up", !%align.choiceButton[0].getValue()); + + // A radio cannot be un-picked, only replaced. + %align.choiceButton[2].performClick(); + pCheck("clicking the chosen one again keeps it", $pButton.align $= "center"); + pCheck("and leaves it looking chosen", %align.choiceButton[2].getValue()); + + %align.choiceButton[0].performClick(); + pCheck("going back to default writes default", $pButton.align $= "default"); + pCheck("button shows easing", pRowShown(%pane, "easeFillColorHL")); + pCheck("button shows tooltip", pRowShown(%pane, "tooltip")); + pCheck("button can be a container", %pane.header.containerButton.isVisible()); + pCheck("shared text row hidden while header owns it", + !pRowShown(%pane, "text")); + + // The header loaded the control's actual values. + pCheck("name row loaded", %pane.header.nameRow.getValue() $= $pButton.getName()); + pCheck("extent row loaded", %pane.header.extentRow.getValue() $= $pButton.getExtent()); + + // --- A commit reaches the control. --- + // applyValue, not setValue: setValue also records the value as the row's + // baseline, which is what tells a later commit that nothing was edited. A + // user typing into the box changes the widget without touching the + // baseline, and applyValue is the half that does that. + %pane.header.textRow.applyValue("Smoke"); + %pane.header.textRow.commit(); + pCheck("text commit reached the control", $pButton.getText() $= "Smoke"); + + %pane.header.extentRow.applyValue("123 45"); + %pane.header.extentRow.commit(); + pCheck("extent commit reached the control", $pButton.getExtent() $= "123 45"); + + // A row that lost focus without being edited must not write. + $pButton.setText("Untouched"); + %pane.refresh(); + %pane.header.textRow.commit(); + pCheck("unchanged row does not write", $pButton.getText() $= "Untouched"); + + schedule(200, 0, "pStep3"); +} + +//----------------------------------------------------------------------------- +// A chain: no text at all, and its own two fields promoted to the header. +//----------------------------------------------------------------------------- + +function pStep3() +{ + $pChain = pAdd("GuiChainCtrl"); + %pane = pBind($pChain); + + pCheck("chain hides the header text block", !%pane.header.textGrid.isVisible()); + pCheck("chain hides the shared text row", !pRowShown(%pane, "text")); + pCheck("chain hides align", !pRowShown(%pane, "align")); + pCheck("chain hides easing", !pRowShown(%pane, "easeFillColorHL")); + pCheck("chain promotes IsVertical", pRowBuilt(%pane, "IsVertical")); + pCheck("chain promotes ChildSpacing", pRowBuilt(%pane, "ChildSpacing")); + pCheck("chain can be a container", %pane.header.containerButton.isVisible()); + + // A grid draws text through the base onRender, so it keeps the fields -- + // but in the collapsed section rather than the header. + $pGrid = pAdd("GuiGridCtrl"); + %pane = pBind($pGrid); + pCheck("grid hides the header text block", !%pane.header.textGrid.isVisible()); + pCheck("grid keeps the shared text row", pRowShown(%pane, "text")); + pCheck("grid text section shown", %pane.panel["Text"].isVisible()); + pCheck("grid promotes CellModeX", pRowBuilt(%pane, "CellModeX")); + pCheck("grid has its Grid section", pRowBuilt(%pane, "MaxColCount")); + + schedule(200, 0, "pStep4"); +} + +//----------------------------------------------------------------------------- +// Classes whose sections have to be rebuilt as the selection moves between +// them -- the one part of the pane that is not filtered but replaced. +//----------------------------------------------------------------------------- + +function pStep4() +{ + $pWindow = pAdd("GuiWindowCtrl"); + %pane = pBind($pWindow); + pCheck("window has its Window section", pRowBuilt(%pane, "canClose")); + pCheck("window has its Grips section", pRowBuilt(%pane, "resizeRightWidth")); + pCheck("window keeps its title text", %pane.header.textGrid.isVisible()); + pCheck("window promotes titleHeight", pRowBuilt(%pane, "titleHeight")); + + // Moving to a slider must take the window's fields away again. + $pSlider = pAdd("GuiSliderCtrl"); + %pane = pBind($pSlider); + pCheck("window fields gone after reselect", !pRowBuilt(%pane, "canClose")); + pCheck("slider has its ticks", pRowBuilt(%pane, "ticks")); + pCheck("slider promotes range", pRowBuilt(%pane, "range")); + pCheck("slider hides the header text block", !%pane.header.textGrid.isVisible()); + pCheck("slider keeps fontSizeAdjust", pRowShown(%pane, "fontSizeAdjust")); + pCheck("slider cannot be a container", !%pane.header.containerButton.isVisible()); + + // And back again, to prove the rebuild is not one-way. + %pane = pBind($pWindow); + pCheck("window fields returned", pRowBuilt(%pane, "canClose")); + pCheck("slider fields gone", !pRowBuilt(%pane, "ticks")); + + schedule(200, 0, "pStep5"); +} + +//----------------------------------------------------------------------------- +// Geometry, which belongs to the parent. This is the check that would have +// caught the fields the old inspector let you edit into a silent revert. +//----------------------------------------------------------------------------- + +function pStep5() +{ + %pane = pBind($pButton); + %anchor = %pane.header.anchorPicker; + pCheck("free control edits its position", + $pButton.getParent() == GuiEditor.rootGui && + %pane.header.positionRow.editor.isActive()); + pCheck("free control edits its extent", %pane.header.extentRow.editor.isActive()); + pCheck("free control edits horizontal sizing", %anchor.leftPin.isActive()); + + // --- The anchor picker and the sizing names it speaks. --- + // The deprecated set still loads: "right" is the old name for anchorLeft, + // and it has to keep working because every Gui already on disk uses it. + $pButton.HorizSizing = "right"; + $pButton.VertSizing = "height"; + pCheck("a deprecated name still sets the field", + $pButton.HorizSizing $= "anchorLeft"); + pCheck("and reads back as the anchor name, which is what TAML will write", + $pButton.HorizSizing !$= "right"); + + %pane.refresh(); + pCheck("anchorLeft reads back as a left-edge pin", + %anchor.pinLeft && !%anchor.pinRight); + pCheck("\"height\" reads back as both vertical pins", + %anchor.pinTop && %anchor.pinBottom); + pCheck("readout names the resolved pair", + %anchor.readout.getText() $= "anchorLeft / height"); + + // The other three deprecated spellings. + $pButton.HorizSizing = "left"; + pCheck("\"left\" still maps to anchorRight", $pButton.HorizSizing $= "anchorRight"); + $pButton.VertSizing = "top"; + pCheck("\"top\" still maps to anchorBottom", $pButton.VertSizing $= "anchorBottom"); + $pButton.VertSizing = "bottom"; + pCheck("\"bottom\" still maps to anchorTop", $pButton.VertSizing $= "anchorTop"); + $pButton.HorizSizing = "relative"; + pCheck("\"relative\" still maps to scale", $pButton.HorizSizing $= "scale"); + + // And the picker reads a control that was set the old way. + $pButton.HorizSizing = "right"; + $pButton.VertSizing = "height"; + %pane.refresh(); + + // The pins are toggle icons, so performClick drives the real path: the + // checkbox flips itself and reports what it became. + // Pinning the right edge as well must produce "width", not "left". + %anchor.rightPin.performClick(); + pCheck("both horizontal pins resolve to width", $pButton.HorizSizing $= "width"); + %anchor.leftPin.performClick(); + pCheck("right pin alone resolves to anchorRight", $pButton.HorizSizing $= "anchorRight"); + %anchor.rightPin.performClick(); + pCheck("no horizontal pin resolves to center", $pButton.HorizSizing $= "center"); + + // Fill supersedes the pins and clearing it hands the axis back to them. + %anchor.leftPin.performClick(); + %anchor.onChipClicked("h", "fill"); + pCheck("fill chip supersedes the pins", $pButton.HorizSizing $= "fill"); + pCheck("fill chip shows as on", %anchor.hFill.getStateOn()); + %anchor.onChipClicked("h", "fill"); + pCheck("clearing fill returns to the pins", $pButton.HorizSizing $= "anchorLeft"); + + // Scale and fill are exclusive on an axis. + %anchor.onChipClicked("h", "scale"); + pCheck("scale chip applies", $pButton.HorizSizing $= "scale"); + %anchor.onChipClicked("h", "fill"); + pCheck("fill replaces scale", $pButton.HorizSizing $= "fill"); + pCheck("scale turned off", !%anchor.hRel.getStateOn()); + + // Touching a pin drops the special. + %anchor.leftPin.performClick(); + pCheck("a pin click clears the special", $pButton.HorizSizing !$= "fill"); + + // The vertical axis is written even when only the horizontal one moved. + // Checked here, while everything above has touched H alone. + pCheck("vertical sizing survived horizontal edits", $pButton.VertSizing $= "height"); + + // --- Center and fill take effect at once, and are reversible. --- + // Both describe a position the control should always be in rather than a + // reaction to a size change, so waiting for the next parent resize would + // make picking one look like it did nothing. + $pButton.HorizSizing = "anchorLeft"; + $pButton.VertSizing = "anchorTop"; + $pButton.setPosition(37, 41); + $pButton.setExtent(120, 26); + %pane.refresh(); + + %parentW = getWord($pButton.getParent().getExtent(), 0); + %anchor.onChipClicked("h", "fill"); + pCheck("fill moved the control to the left edge immediately", + getWord($pButton.getPosition(), 0) == 0); + pCheck("fill widened the control immediately", + getWord($pButton.getExtent(), 0) > 120); + pCheck("fill left the other axis alone", + getWord($pButton.getPosition(), 1) == 41 && + getWord($pButton.getExtent(), 1) == 26); + + pCheck("fill stashed what it overwrote", + %pane.stashPos["h"] $= "37 41" && %pane.stashExtent["h"] $= "120 26"); + + // Clicking away from fill gives back exactly what it overwrote. + %anchor.onChipClicked("h", "scale"); + pCheck("leaving fill cleared the stash", %pane.stashed["h"] $= ""); + pCheck("leaving fill restored the x position", + getWord($pButton.getPosition(), 0) == 37); + pCheck("leaving fill restored the width", + getWord($pButton.getExtent(), 0) == 120); + + // Center owns the position but not the extent. Clearing scale hands the + // axis back to its pins, which is anchorLeft, so one click empties them. + %anchor.onChipClicked("h", "scale"); + %anchor.leftPin.performClick(); + pCheck("no pins resolves to center", $pButton.HorizSizing $= "center"); + pCheck("center recentred the control immediately", + getWord($pButton.getPosition(), 0) != 37); + pCheck("center left the width alone", getWord($pButton.getExtent(), 0) == 120); + + %anchor.leftPin.performClick(); + pCheck("leaving center restored the x position", + getWord($pButton.getPosition(), 0) == 37); + + // The stash belongs to the control it came from: selecting something else + // throws it away rather than carrying a stale position across. + %anchor.onChipClicked("h", "fill"); + pBind($pWindow); + %pane = pBind($pButton); + pCheck("the stash does not survive a selection change", + %pane.stashed["h"] $= ""); + $pButton.HorizSizing = "anchorLeft"; + $pButton.setPosition(37, 41); + $pButton.setExtent(120, 26); + %pane.refresh(); + + // With Fill in effect the pins are not what is happening, so they read off + // -- and a disabled axis must refuse the click entirely. + %anchor.setAxisEnabled(false, true); + %before = $pButton.HorizSizing; + %anchor.leftPin.performClick(); + pCheck("a disabled axis ignores its pins", $pButton.HorizSizing $= %before); + %anchor.setAxisEnabled(true, true); + + $pButton.HorizSizing = "right"; + $pButton.VertSizing = "bottom"; + %pane.refresh(); + + // Into a vertical chain: it takes the Y position and the vertical sizing, + // and leaves the X position and the extent alone. + $pChain.IsVertical = true; + $pChain.add($pButton); + %pane = pBind($pButton); + pCheck("chain child keeps X position", %pane.header.positionRow.editor.isActive()); + pCheck("chain child loses Y position", !%pane.header.positionRow.editorY.isActive()); + pCheck("chain child keeps horizontal anchoring", + %pane.header.anchorPicker.leftPin.isActive()); + pCheck("chain child loses vertical anchoring", + !%pane.header.anchorPicker.topPin.isActive()); + pCheck("chain child keeps its extent", %pane.header.extentRow.editor.isActive()); + + // Into a grid: the cell owns everything. + $pGrid.add($pButton); + %pane = pBind($pButton); + pCheck("grid child loses X position", !%pane.header.positionRow.editor.isActive()); + pCheck("grid child loses Y position", !%pane.header.positionRow.editorY.isActive()); + pCheck("grid child loses its extent", !%pane.header.extentRow.editor.isActive()); + pCheck("grid child loses both sizings", + !%pane.header.anchorPicker.leftPin.isActive() && + !%pane.header.anchorPicker.topPin.isActive()); + + // A scroller is the container that looks like it owns its children and does + // not -- it only scrolls them. + $pScroll = pAdd("GuiScrollCtrl"); + $pScroll.add($pButton); + %pane = pBind($pButton); + pCheck("scroll child keeps its position", %pane.header.positionRow.editor.isActive()); + pCheck("scroll child keeps its extent", %pane.header.extentRow.editor.isActive()); + + schedule(200, 0, "pStep6"); +} + +//----------------------------------------------------------------------------- +// Toggles, the profile picker, and clearing the selection. +//----------------------------------------------------------------------------- + +function pStep6() +{ + %pane = pBind($pButton); + %header = %pane.header; + + // The toggle's box has to cover the whole control for it to read as a + // button rather than a checkbox. It got this wrong once: setBoxOffset and + // setBoxExtent document one argument and read two, so a single "0 0" left + // the box at (0,32) -- drawn entirely below the control. + pCheck("toggle box covers the whole control", + %header.lockedButton.getBoxOffset() $= "0 0" && + %header.lockedButton.getBoxExtent() $= %header.lockedButton.getExtent()); + + // All six state flags are icon toggles now that the sheet has art for them. + pCheck("visible toggle reflects the control", + %header.visibleButton.getValue() == $pButton.Visible); + + %header.visibleButton.performClick(); + pCheck("visible toggle reached the control", !$pButton.Visible); + %header.visibleButton.performClick(); + pCheck("visible toggle restored", $pButton.Visible); + + %header.inputButton.performClick(); + pCheck("accepts-input toggle reached the control", !$pButton.useInput); + %header.inputButton.performClick(); + + %header.containerButton.performClick(); + pCheck("accepts-children toggle reached the control", $pButton.isContainer); + %header.containerButton.performClick(); + + // A control that cannot draw children has the field forced false, so the + // button goes rather than sitting there wired to nothing. + %listPane = pBind(pAdd("GuiListBoxCtrl")); + pCheck("accepts-children hidden where it is dead", + !%listPane.header.containerButton.isVisible()); + %pane = pBind($pButton); + %header = %pane.header; + pCheck("accepts-children shown where it is live", + %header.containerButton.isVisible()); + + // hidden and locked are toggle icons -- checkboxes wearing an icon -- so + // they hold their own state. performClick drives the real path: the + // checkbox flips itself and its Command tells the pane what it became. + %header.lockedButton.performClick(); + pCheck("locked toggle reached the control", $pButton.locked); + pCheck("locked icon shows the on frame", + %header.lockedButton.icon.getImageFrame() == %header.lockedButton.frameOn); + %header.lockedButton.performClick(); + pCheck("locked toggle flips back", !$pButton.locked); + + // A disabled toggle must not act. The engine hands touch events to inactive + // controls -- findHitControl checks mVisible and mUseInput, never mActive -- + // so this is the checkbox's own guard doing the work. + %header.lockedButton.setActive(false); + %header.lockedButton.performClick(); + pCheck("a disabled toggle does not change the control", !$pButton.locked); + pCheck("a disabled toggle keeps its own state off", !%header.lockedButton.getValue()); + %header.lockedButton.setActive(true); + + // The profile picker offers the theme's members for the control's category, + // not every profile in the sim. + %items = %header.profileRow.editor.getItemCount(); + pCheck("profile picker has candidates (" @ %items @ ")", %items > 0); + %current = GuiEditor.themeApplier.fieldProfile($pButton, "Profile"); + pCheck("profile picker shows what the control wears", + isObject(%current) && %header.profileRow.getValue() $= %current.getName()); + + // A menu item has no GuiControl fields at all, so the header sheds + // everything but its name and its caption. + $pMenuItem = new GuiMenuItemCtrl(); + %pane = pBind($pMenuItem); + pCheck("menu item hides the profile row", !%pane.header.profileRow.isVisible()); + pCheck("menu item hides geometry", !%pane.header.geometryGrid.isVisible()); + pCheck("menu item keeps its caption", %pane.header.textGrid.isVisible()); + + schedule(200, 0, "pStep7"); +} + +//----------------------------------------------------------------------------- +// Dynamic fields: built fresh rather than ported, and filtered so a frame set's +// serialized layout tree cannot be hand-edited into an unloadable Gui. +//----------------------------------------------------------------------------- + +function pStep7() +{ + %pane = pBind($pButton); + %dyn = %pane.dynamicFields; + + pCheck("dynamic section built", isObject(%dyn)); + pCheck("dynamic section shown for a bound control", %pane.dynamicPanel.isVisible()); + pCheck("a plain control starts with no dynamic fields", !%dyn.hasFields()); + + // Add through the widget, exactly as a click would. A dynamic field cannot + // hold an empty value -- SimFieldDictionary::setFieldValue frees the entry + // when the value is empty -- so Add produces a row, and giving that row a + // value is what puts the field on the control. + %dyn.nameBox.setText("smokeTag"); + %dyn.onAddClicked(); + pCheck("add built a row for the new name", isObject(%dyn.row["smokeTag"])); + pCheck("name box cleared after adding", %dyn.nameBox.getText() $= ""); + pCheck("naming alone does not create the field", + !pHasDynamicField($pButton, "smokeTag")); + + // Edit its value: this is what creates it. + %dyn.row["smokeTag"].applyValue("hello"); + %dyn.row["smokeTag"].commit(); + pCheck("dynamic value commit reached the control", $pButton.smokeTag $= "hello"); + pCheck("the field now really exists", pHasDynamicField($pButton, "smokeTag")); + + // A name that is already a registered field must not be accepted -- writing + // it would set the real field and quietly do something else entirely. + %dyn.nameBox.setText("Extent"); + %dyn.onAddClicked(); + pCheck("a built-in field name is refused", !isObject(%dyn.row["Extent"])); + %dyn.nameBox.setText(""); + + // A frame set hides the fields it serializes its own layout into. + $pFrameSet = pAdd("GuiFrameSetCtrl"); + $pFrameSet.frameID0 = "1"; + $pFrameSet.myOwnField = "keep"; + %pane = pBind($pFrameSet); + %dyn = %pane.dynamicFields; + pCheck("frame set hides its serialized layout field", !isObject(%dyn.row["frameID0"])); + pCheck("frame set still shows a user field", isObject(%dyn.row["myOwnField"])); + + schedule(200, 0, "pStep8"); +} + +function pStep8() +{ + %pane = pBind($pButton); + %dyn = %pane.dynamicFields; + pCheck("dynamic field survived reselect", isObject(%dyn.row["smokeTag"])); + + // Remove, which is the row's reset button repurposed. + %dyn.onProfileRowReset(%dyn.row["smokeTag"]); + schedule(100, 0, "pStep9"); +} + +function pStep9() +{ + %pane = GuiEditor.inspectorWindow.pane; + pCheck("remove took the field off the control", !pHasDynamicField($pButton, "smokeTag")); + pCheck("remove took the row with it", !isObject(%pane.dynamicFields.row["smokeTag"])); + + // Nothing selected: the pane stops drawing rather than showing stale values. + %pane.unbind(); + pCheck("pane hides when nothing is selected", !%pane.isVisible()); + + echo("IPSMOKE DONE"); + schedule(300, 0, "quit"); +} diff --git a/tests/smoke/inspectorSpec.cs b/tests/smoke/inspectorSpec.cs new file mode 100644 index 000000000..0b6178cda --- /dev/null +++ b/tests/smoke/inspectorSpec.cs @@ -0,0 +1,322 @@ +// Control-spec smoke test. Exercises GuiEditorControlSpec on its own -- no +// editor, no canvas, no UI -- because the spec is pure data and every rule it +// encodes is a claim about what the engine reads. If one of these fails, either +// the engine changed or the table was wrong. +// +// The checks are deliberately weighted toward the surprising entries: the ones +// where the field name suggests the opposite of what the render path does. +// Run: tests/run.ps1 inspectorSpec ; grep ISSMOKE in console.log. + +setLogMode(1); +$Scripts::ignoreDSOs = true; +setScriptExecEcho(false); +trace(false); + +function sCheck(%label, %cond) +{ + if(%cond) echo("ISSMOKE PASS: " @ %label); + else echo("ISSMOKE FAIL: " @ %label); +} + +// Make a control, and deliberately do NOT give it a profile. +// +// This used to have to set one. Several constructors never did -- GuiChainCtrl, +// GuiTabPageCtrl, GuiSliderCtrl, GuiTextEditCtrl, GuiInputCtrl, GuiSpriteCtrl +// and SceneWindow among them -- so a control built from script carried a null +// mProfile, and GuiChainCtrl::onChildAdded runs calculateExtent, which asks the +// profile for its borders. Adding a child to a chain was a hard crash. +// +// GuiControl::onAdd now falls back to GuiDefaultProfile for anything that +// reaches registration without one, so every line below is also a test of that. +function sMake(%class) +{ + return eval("return new " @ %class @ "();"); +} + +// The spec itself needs nothing but the class list, but the controls step 4 +// builds to ask about geometry are real ones, and profiling a real control +// pulls in fonts and textures -- which need a live canvas. So this boots the +// editor like every other suite rather than running the spec in a vacuum. +testExec("editor/main.cs"); +schedule(2000, 0, "sStep1"); + +//----------------------------------------------------------------------------- +// The table itself. +//----------------------------------------------------------------------------- + +function sStep1() +{ + testExec("editor/GuiEditor/scripts/GuiEditorControlSpec.cs"); + $sSpec = new ScriptObject() { class = "GuiEditorControlSpec"; }; + sCheck("spec object created", isObject($sSpec)); + + // --- Drift guard: a control class added to the engine must not fall + // through to the unknown-class fallback unnoticed. --- + %missing = $sSpec.findMissingClasses(enumerateConsoleClasses("GuiControl")); + sCheck("spec covers every placeable class (missing: " @ %missing @ ")", %missing $= ""); + + sCheck("known class recognised", $sSpec.isKnownClass("GuiButtonCtrl")); + sCheck("unknown class not recognised", !$sSpec.isKnownClass("GuiNotARealCtrl")); + + schedule(50, 0, "sStep2"); +} + +//----------------------------------------------------------------------------- +// Text. Every one of these is a case where the field list and the render path +// disagree. +//----------------------------------------------------------------------------- + +function sStep2() +{ + %s = $sSpec; + + // A chain draws only a "+" in edit mode, so the nine text fields it + // inherits are all dead. + sCheck("chain hides text", !%s.isFieldVisible("GuiChainCtrl", "text")); + sCheck("chain hides align", !%s.isFieldVisible("GuiChainCtrl", "align")); + sCheck("chain hides fontColor", !%s.isFieldVisible("GuiChainCtrl", "fontColor")); + + // A slider never calls renderText, but it does size its dglDrawText with + // getFont(mFontSizeAdjust). + sCheck("slider hides text", !%s.isFieldVisible("GuiSliderCtrl", "text")); + sCheck("slider hides align", !%s.isFieldVisible("GuiSliderCtrl", "align")); + sCheck("slider keeps fontSizeAdjust", %s.isFieldVisible("GuiSliderCtrl", "fontSizeAdjust")); + + // The multi-line text control: wrap is real, vAlign and textExtend are not. + sCheck("text edit keeps textWrap", %s.isFieldVisible("GuiTextEditCtrl", "textWrap")); + sCheck("text edit keeps align", %s.isFieldVisible("GuiTextEditCtrl", "align")); + sCheck("text edit hides vAlign", !%s.isFieldVisible("GuiTextEditCtrl", "vAlign")); + sCheck("text edit hides textExtend", !%s.isFieldVisible("GuiTextEditCtrl", "textExtend")); + + // A list renders item text with its own profile: layout lives, the control's + // own string does not. + sCheck("list box hides text", !%s.isFieldVisible("GuiListBoxCtrl", "text")); + sCheck("list box hides textID", !%s.isFieldVisible("GuiListBoxCtrl", "textID")); + sCheck("list box keeps align", %s.isFieldVisible("GuiListBoxCtrl", "align")); + + // The book draws the page's caption, so the string belongs to the page and + // the layout belongs to the book. + sCheck("tab page keeps text", %s.isFieldVisible("GuiTabPageCtrl", "text")); + sCheck("tab page hides align", !%s.isFieldVisible("GuiTabPageCtrl", "align")); + sCheck("tab book hides text", !%s.isFieldVisible("GuiTabBookCtrl", "text")); + sCheck("tab book keeps align", %s.isFieldVisible("GuiTabBookCtrl", "align")); + + // A panel hands its text to the header button it owns. + sCheck("panel keeps text", %s.isFieldVisible("GuiPanelCtrl", "text")); + sCheck("panel hides textWrap", !%s.isFieldVisible("GuiPanelCtrl", "textWrap")); + + // Where the text box belongs, and what it is called there. + sCheck("button text in header", %s.textBelongsInHeader("GuiButtonCtrl")); + sCheck("grid text not in header", !%s.textBelongsInHeader("GuiGridCtrl")); + sCheck("grid text still shown", %s.isFieldVisible("GuiGridCtrl", "text")); + sCheck("drop down text labelled Placeholder", + %s.textLabelFor("GuiDropDownCtrl") $= "Placeholder"); + sCheck("tab page text labelled Tab Caption", + %s.textLabelFor("GuiTabPageCtrl") $= "Tab Caption"); + + schedule(50, 0, "sStep3"); +} + +//----------------------------------------------------------------------------- +// Easing. Narrower than the class tree suggests: inheriting GuiEasingSupport is +// not the same as calling getFillColor. +//----------------------------------------------------------------------------- + +function sStep3() +{ + %s = $sSpec; + + sCheck("button keeps easing", %s.isFieldVisible("GuiButtonCtrl", "easeFillColorHL")); + sCheck("drop down keeps easing", %s.isFieldVisible("GuiDropDownCtrl", "easeTimeFillColorSL")); + sCheck("frame set keeps easing", %s.isFieldVisible("GuiFrameSetCtrl", "easeFillColorSL")); + + // GuiCheckBoxCtrl::onRender draws its box through renderInnerControl and + // never renders a universal rect of its own, so it inherits a dead set. + sCheck("check box hides easing", !%s.isFieldVisible("GuiCheckBoxCtrl", "easeFillColorHL")); + sCheck("radio hides easing", !%s.isFieldVisible("GuiRadioCtrl", "easeFillColorHL")); + + // An image button draws only its asset -- there is no fill to ease. + sCheck("image button hides easing", !%s.isFieldVisible("GuiImageButtonCtrl", "easeFillColorHL")); + sCheck("image button hides text", !%s.isFieldVisible("GuiImageButtonCtrl", "text")); + + // A menu item calls SimObject::initPersistFields, not GuiControl's. + sCheck("menu item is bare", %s.hasFlag("GuiMenuItemCtrl", "bare")); + sCheck("menu item hides Profile", !%s.isFieldVisible("GuiMenuItemCtrl", "Profile")); + sCheck("menu item hides tooltip", !%s.isFieldVisible("GuiMenuItemCtrl", "tooltip")); + sCheck("menu item keeps text", %s.isFieldVisible("GuiMenuItemCtrl", "text")); + + // Never shown for anyone. + sCheck("canSave never shown", !%s.isFieldVisible("GuiControl", "canSave")); + sCheck("parentGroup never shown", !%s.isFieldVisible("GuiControl", "parentGroup")); + + schedule(50, 0, "sStep4"); +} + +//----------------------------------------------------------------------------- +// Geometry, which is the parent's answer rather than the class's, and +// isContainer, which is the engine's. +//----------------------------------------------------------------------------- + +function sStep4() +{ + %s = $sSpec; + + // --- Every control gets a profile, whether its constructor set one or not. + // The chain is the one that used to crash outright on the next line. --- + %bare = sMake("GuiChainCtrl"); + sCheck("a chain gets a fallback profile", isObject(%bare.Profile)); + sCheck("a tab page gets a fallback profile", isObject(sMake("GuiTabPageCtrl").Profile)); + sCheck("a slider gets a fallback profile", isObject(sMake("GuiSliderCtrl").Profile)); + sCheck("a text edit gets a fallback profile", isObject(sMake("GuiTextEditCtrl").Profile)); + sCheck("an input control gets a fallback profile", isObject(sMake("GuiInputCtrl").Profile)); + sCheck("a sprite gets a fallback profile", isObject(sMake("GuiSpriteCtrl").Profile)); + sCheck("a scene window gets a fallback profile", isObject(sMake("SceneWindow").Profile)); + + // A control whose constructor names its own profile keeps that one rather + // than being overwritten by the fallback. + sCheck("a button keeps its constructor's profile choice", + isObject(sMake("GuiButtonCtrl").Profile)); + + $sRoot = sMake("GuiControl"); + $sRoot.setExtent(400, 400); + + %vChain = sMake("GuiChainCtrl"); + %vChain.IsVertical = true; + $sRoot.add(%vChain); + %vKid = sMake("GuiButtonCtrl"); + %vChain.add(%vKid); + + %hChain = sMake("GuiChainCtrl"); + %hChain.IsVertical = false; + $sRoot.add(%hChain); + %hKid = sMake("GuiButtonCtrl"); + %hChain.add(%hKid); + + %grid = sMake("GuiGridCtrl"); + $sRoot.add(%grid); + %gridKid = sMake("GuiButtonCtrl"); + %grid.add(%gridKid); + + // The container people expect to own its children and does not. + %scroll = sMake("GuiScrollCtrl"); + $sRoot.add(%scroll); + %scrollKid = sMake("GuiButtonCtrl"); + %scroll.add(%scrollKid); + + %book = sMake("GuiTabBookCtrl"); + $sRoot.add(%book); + %page = sMake("GuiTabPageCtrl"); + %book.add(%page); + + sCheck("plain child owns its geometry", %s.geometryModeOf(%vChain) $= "full"); + sCheck("vertical chain child is chainV", %s.geometryModeOf(%vKid) $= "chainV"); + sCheck("horizontal chain child is chainH", %s.geometryModeOf(%hKid) $= "chainH"); + sCheck("grid child owns nothing", %s.geometryModeOf(%gridKid) $= "none"); + sCheck("tab page owns nothing", %s.geometryModeOf(%page) $= "none"); + sCheck("scroll child still owns its geometry", %s.geometryModeOf(%scrollKid) $= "full"); + + // A vertical chain takes the Y position and the vertical sizing; the cross + // axis is left alone, and the extent is the child's own on both axes. + sCheck("chainV keeps HorizSizing", %s.isGeometryFieldLive("chainV", "HorizSizing")); + sCheck("chainV drops VertSizing", !%s.isGeometryFieldLive("chainV", "VertSizing")); + sCheck("chainV keeps Extent", %s.isGeometryFieldLive("chainV", "Extent")); + sCheck("chainV position is x only", %s.livePositionAxes("chainV") $= "x"); + sCheck("chainH position is y only", %s.livePositionAxes("chainH") $= "y"); + sCheck("none has no live position axis", %s.livePositionAxes("none") $= ""); + sCheck("full keeps both position axes", %s.livePositionAxes("full") $= "xy"); + sCheck("none drops Extent", !%s.isGeometryFieldLive("none", "Extent")); + + // isContainer is only meaningful where the control draws children, which is + // the engine's answer via the new rendersChildren() accessor. + %list = sMake("GuiListBoxCtrl"); + %slider = sMake("GuiSliderCtrl"); + %sprite = sMake("GuiSpriteCtrl"); + sCheck("plain control can be a container", %s.isContainerFieldVisible($sRoot)); + sCheck("chain can be a container", %s.isContainerFieldVisible(%vChain)); + sCheck("list box cannot be a container", !%s.isContainerFieldVisible(%list)); + sCheck("slider cannot be a container", !%s.isContainerFieldVisible(%slider)); + sCheck("sprite can be a container", %s.isContainerFieldVisible(%sprite)); + + schedule(50, 0, "sStep5"); +} + +//----------------------------------------------------------------------------- +// Dynamic fields, type mapping, and the unknown-class fallback. +//----------------------------------------------------------------------------- + +function sStep5() +{ + %s = $sSpec; + + // A frame set serializes its whole frame tree into dynamic fields; editing + // those by hand can leave a Gui that will not load. + sCheck("frame set hides frameID0", %s.hidesDynamicField("GuiFrameSetCtrl", "frameID0")); + sCheck("frame set hides frameExtentX2", %s.hidesDynamicField("GuiFrameSetCtrl", "frameExtentX2")); + sCheck("frame set keeps a user field", !%s.hidesDynamicField("GuiFrameSetCtrl", "myThing")); + sCheck("button hides no dynamic fields", !%s.hidesDynamicField("GuiButtonCtrl", "frameID0")); + + // getFieldType answers with a console type's class name, not its TypeXxx + // constant, so the mapping is keyed on the former. + sCheck("bool maps to bool", %s.kindForType("bool") $= "bool"); + sCheck("int maps to number", %s.kindForType("int") $= "number"); + sCheck("char maps to number", %s.kindForType("char") $= "number"); + sCheck("float maps to number", %s.kindForType("float") $= "number"); + sCheck("enumval maps to enum", %s.kindForType("enumval") $= "enum"); + sCheck("Point2I maps to point", %s.kindForType("Point2I") $= "point"); + sCheck("ColorI maps to color", %s.kindForType("ColorI") $= "color"); + sCheck("FluidColorI maps to color", %s.kindForType("FluidColorI") $= "color"); + sCheck("filename maps to file", %s.kindForType("filename") $= "file"); + sCheck("assetIdString maps to asset", %s.kindForType("assetIdString") $= "asset"); + sCheck("GuiProfile maps to profile", %s.kindForType("GuiProfile") $= "profile"); + sCheck("GuiCursor maps to hidden", %s.kindForType("GuiCursor") $= "hidden"); + sCheck("string maps to text", %s.kindForType("string") $= "text"); + + // The type names above have to be what the engine actually answers. + %btn = sMake("GuiButtonCtrl"); + sCheck("engine spells Point2I as Point2I", %btn.getFieldType("Extent") $= "Point2I"); + sCheck("engine spells TypeBool as bool", %btn.getFieldType("Visible") $= "bool"); + sCheck("engine spells TypeEnum as enumval", %btn.getFieldType("HorizSizing") $= "enumval"); + sCheck("engine spells TypeGuiProfile as GuiProfile", %btn.getFieldType("Profile") $= "GuiProfile"); + sCheck("engine spells TypeS32 as int", %btn.getFieldType("tooltipWidth") $= "int"); + sCheck("engine spells TypeColorI as ColorI", %btn.getFieldType("fontColor") $= "ColorI"); + + %sprite = sMake("GuiSpriteCtrl"); + sCheck("engine spells TypeAssetId as assetIdString", + %sprite.getFieldType("Image") $= "assetIdString"); + sCheck("engine spells TypeFluidColorI as FluidColorI", + %sprite.getFieldType("imageColor") $= "FluidColorI"); + + // A sprite names its picture three mutually exclusive ways. + sCheck("sprite defaults to Image source", %s.spriteSourceModeOf(%sprite) $= "Image"); + sCheck("Image source offers Frame", + %s.listHas(%s.spriteSourceFields("Image"), "Frame")); + sCheck("Bitmap source drops Frame", + !%s.listHas(%s.spriteSourceFields("Bitmap"), "Frame")); + + // An unknown class shows everything rather than less. + sCheck("unknown class shows text", %s.isFieldVisible("GuiNotARealCtrl", "text")); + sCheck("unknown class shows tooltip", %s.isFieldVisible("GuiNotARealCtrl", "tooltip")); + sCheck("unknown class is not bare", !%s.hasFlag("GuiNotARealCtrl", "bare")); + sCheck("unknown class still hides canSave", !%s.isFieldVisible("GuiNotARealCtrl", "canSave")); + + // Labels fall back to the field name when nothing better is registered. + sCheck("registered label used", %s.labelFor("HorizSizing") $= "Horizontal Sizing"); + sCheck("unregistered label falls back", %s.labelFor("someOddField") $= "someOddField"); + + // Sections are declared per class rather than inherited, so a subclass that + // wants its parent's section has to say so. + sCheck("tree view has both its sections", + %s.listHas(%s.sectionKeys("GuiTreeViewCtrl"), "List") && + %s.listHas(%s.sectionKeys("GuiTreeViewCtrl"), "Tree")); + sCheck("window has its two sections", + %s.listHas(%s.sectionKeys("GuiWindowCtrl"), "Window") && + %s.listHas(%s.sectionKeys("GuiWindowCtrl"), "Grips")); + sCheck("plain control has no sections", %s.sectionKeys("GuiControl") $= ""); + sCheck("window section titled", + %s.sectionTitle("GuiWindowCtrl", "Grips") $= "Resize Grips"); + sCheck("window grips fields", + %s.listHas(%s.sectionFields("GuiWindowCtrl", "Grips"), "resizeRightWidth")); + + echo("ISSMOKE DONE"); + schedule(300, 0, "quit"); +} diff --git a/tests/smoke/inspectorVariants.cs b/tests/smoke/inspectorVariants.cs new file mode 100644 index 000000000..b9d4f50fd --- /dev/null +++ b/tests/smoke/inspectorVariants.cs @@ -0,0 +1,331 @@ +// Variants smoke test. The properties pane hides a control's secondary profile +// slots -- contentProfile, thumbProfile and the rest -- until there is actually +// something to choose between, and the set that decides whether a row appears +// is deliberately narrower than the set the row then offers. +// +// The rule, from the design: +// +// anchors = theme members of the slot's category +// + standalones stamped for that category +// + whatever the slot currently holds +// row appears <=> count(anchors) > 1 +// options = anchors + uncategorised ("Any") standalones +// +// So an "Any" standalone can be picked in a row that exists, but can never be +// the reason one appears -- otherwise a single uncategorised profile would put +// a row on every slot of every control. +// Run: tests/run.ps1 inspectorVariants ; grep IVSMOKE in console.log. + +setLogMode(1); +$Scripts::ignoreDSOs = true; +setScriptExecEcho(false); +trace(false); + +function vCheck(%label, %cond) +{ + if(%cond) echo("IVSMOKE PASS: " @ %label); + else echo("IVSMOKE FAIL: " @ %label); +} + +function vPane() +{ + return GuiEditor.inspectorWindow.pane; +} + +// Rebind so the pane re-evaluates the slots against the theme as it is now. +function vRebind(%ctrl) +{ + %pane = vPane(); + %pane.bind(%ctrl); + return %pane; +} + +function vHasRow(%pane, %field) +{ + return isObject(%pane.row[%field]); +} + +function vOffers(%pane, %field, %name) +{ + %row = %pane.row[%field]; + return isObject(%row) && %row.editor.findItemText(%name, false) >= 0; +} + +testExec("editor/main.cs"); +schedule(2000, 0, "vStep1"); + +//----------------------------------------------------------------------------- +// One member per category: no Variants rows at all. +//----------------------------------------------------------------------------- + +function vStep1() +{ + ProjectManager.setProjectFolder("inspectorVariantsSmokeProject"); + GuiEditor.open(); + + // A window is the control with the most secondary slots -- content, close, + // minimise and maximise -- so it is the one that would be noisiest if the + // threshold were wrong. + $vWindow = new GuiWindowCtrl(); + GuiEditor.rootGui.add($vWindow); + + $vTheme = GuiEditor.themeLibrary.createTheme("IVSmoke"); + vCheck("theme created", isObject($vTheme)); + + GuiEditor.themeApplier.applyToBranch($vWindow, $vTheme, true); + GuiEditor.themeName = $vTheme.getName(); + + %pane = vRebind($vWindow); + vCheck("window bound", %pane.target == $vWindow); + vCheck("no Variants section with one member per category", + !isObject(%pane.panel["Variants"])); + vCheck("no content slot row", !vHasRow(%pane, "contentProfile")); + vCheck("no close button slot row", !vHasRow(%pane, "closeButtonProfile")); + + // The header's own Profile picker exists regardless -- it is not a Variants + // row and has no threshold. + vCheck("header profile picker still there", %pane.header.profileRow.isVisible()); + + schedule(200, 0, "vStep2"); +} + +//----------------------------------------------------------------------------- +// A second WindowContent in the theme: that slot, and only that slot, appears. +//----------------------------------------------------------------------------- + +function vStep2() +{ + $vExtra = $vTheme.createProfile("WindowContent"); + vCheck("extra WindowContent created", isObject($vExtra)); + vCheck("theme reports two WindowContent members", + getWordCount($vTheme.getProfiles("WindowContent")) == 2); + + %pane = vRebind($vWindow); + vCheck("Variants section appeared", isObject(%pane.panel["Variants"])); + vCheck("content slot got a row", vHasRow(%pane, "contentProfile")); + vCheck("Variants section is visible", %pane.panel["Variants"].isVisible()); + + // Only the slot with a choice. The other three categories still have one + // member each. + vCheck("close button slot still hidden", !vHasRow(%pane, "closeButtonProfile")); + vCheck("min button slot still hidden", !vHasRow(%pane, "minButtonProfile")); + vCheck("tooltip slot still hidden", !vHasRow(%pane, "tooltipProfile")); + + // The row offers both members and starts on the one the control wears. + vCheck("row offers the default member", + vOffers(%pane, "contentProfile", $vTheme.getProfile("WindowContent").getName())); + vCheck("row offers the extra member", + vOffers(%pane, "contentProfile", $vExtra.getName())); + + %current = GuiEditor.themeApplier.fieldProfile($vWindow, "contentProfile"); + vCheck("row shows what the control wears", + isObject(%current) && %pane.row["contentProfile"].getValue() $= %current.getName()); + + schedule(200, 0, "vStep3"); +} + +//----------------------------------------------------------------------------- +// Choosing the extra, and the write going in by id rather than by name. +//----------------------------------------------------------------------------- + +function vStep3() +{ + %pane = vRebind($vWindow); + %row = %pane.row["contentProfile"]; + + // applyValue rather than setValue: setValue also records the baseline that + // tells a later commit nothing was edited. + %row.applyValue($vExtra.getName()); + %row.commit(); + + %now = GuiEditor.themeApplier.fieldProfile($vWindow, "contentProfile"); + vCheck("choosing a variant reached the control", %now == $vExtra.getId()); + + // A theme member chosen deliberately survives a re-apply: applyToControl + // skips any slot already wearing a profile from this theme. + GuiEditor.themeApplier.applyToBranch($vWindow, $vTheme, false); + %after = GuiEditor.themeApplier.fieldProfile($vWindow, "contentProfile"); + vCheck("the choice survives Set Theme", %after == $vExtra.getId()); + + schedule(200, 0, "vStep4"); +} + +//----------------------------------------------------------------------------- +// Standalones. A categorised one is an anchor; an "Any" one is only an option. +//----------------------------------------------------------------------------- + +function vStep4() +{ + %library = GuiEditor.themeLibrary; + + // Uncategorised -- what createStandalone makes, and what the Profile + // Editor shows as "Any". + $vAny = %library.createStandalone("IVAnyProfile"); + vCheck("standalone created", isObject($vAny)); + vCheck("standalone starts uncategorised", $vAny.category $= ""); + + %pane = vRebind($vWindow); + + // THE RULE: it must not have made any new row appear. + vCheck("Any standalone adds no close button row", !vHasRow(%pane, "closeButtonProfile")); + vCheck("Any standalone adds no tooltip row", !vHasRow(%pane, "tooltipProfile")); + vCheck("Any standalone adds no min button row", !vHasRow(%pane, "minButtonProfile")); + + // But it is offered where a row already exists, and in the header picker, + // because those slots are on show regardless. + vCheck("Any standalone offered in an existing Variants row", + vOffers(%pane, "contentProfile", "IVAnyProfile")); + vCheck("Any standalone offered as the control's own profile", + %pane.header.profileRow.editor.findItemText("IVAnyProfile", false) >= 0); + + // Stamp it for a category and the matching slot must appear. + $vAny.category = "WindowCloseButton"; + %pane = vRebind($vWindow); + vCheck("categorised standalone makes its slot appear", + vHasRow(%pane, "closeButtonProfile")); + vCheck("the new row offers the standalone", + vOffers(%pane, "closeButtonProfile", "IVAnyProfile")); + vCheck("other slots still hidden", !vHasRow(%pane, "minButtonProfile")); + + schedule(200, 0, "vStep5"); +} + +//----------------------------------------------------------------------------- +// What is never offered, and what the "currently assigned" term is for. +//----------------------------------------------------------------------------- + +function vStep5() +{ + %pane = vRebind($vWindow); + + // A script profile is neither a theme member nor a standalone the editor + // manages. The old inspector listed every named profile in the sim; that is + // the dropdown this pane exists to replace. + vCheck("script profiles are not offered", + !vOffers(%pane, "contentProfile", "GuiDefaultProfile")); + + // A second theme's members are not offered either, even at the right + // category -- the Gui wears one theme. + $vOther = GuiEditor.themeLibrary.createTheme("IVOther"); + %pane = vRebind($vWindow); + vCheck("another theme's member is not offered", + !vOffers(%pane, "contentProfile", $vOther.getProfile("WindowContent").getName())); + + // Unless the control is actually wearing it. The currently-assigned term + // exists so a slot holding something the theme does not offer is visible + // and changeable rather than silently stuck -- hand-edit a saved Gui to + // point one slot at another theme and that slot, and only that slot, gets a + // picker offering both. + $vWindow.setEditFieldValue("minButtonProfile", $vOther.getProfile("WindowButton").getId()); + %pane = vRebind($vWindow); + vCheck("an off-theme assignment makes its row appear", + vHasRow(%pane, "minButtonProfile")); + vCheck("and the row offers what is actually worn", + vOffers(%pane, "minButtonProfile", $vOther.getProfile("WindowButton").getName())); + + // Only that slot. maxButtonProfile shares minButtonProfile's category and + // is still on the current theme, so it stays quiet -- the current value is + // counted per slot, not per category. + vCheck("a sibling slot on the current theme stays hidden", + !vHasRow(%pane, "maxButtonProfile")); + + $vWindow.setEditFieldValue("Profile", $vOther.getProfile("Window").getId()); + %pane = vRebind($vWindow); + vCheck("the header always shows the control's own profile", + %pane.header.profileRow.getValue() $= $vOther.getProfile("Window").getName()); + + // A control with no secondary slots at all never gets the section. + $vButton = new GuiButtonCtrl(); + GuiEditor.rootGui.add($vButton); + GuiEditor.themeApplier.applyToBranch($vButton, $vTheme, true); + %pane = vRebind($vButton); + vCheck("a button has no Variants section", !isObject(%pane.panel["Variants"])); + + schedule(200, 0, "vStep6"); +} + +//----------------------------------------------------------------------------- +// The real drop order, which is where this all went wrong. +// +// GuiEditorBrain::onControlDropped adds the control -- which announces it, and +// so inspects it -- and only THEN applies the theme. A GuiWindowCtrl's +// constructor names five profiles (GuiWindowProfile, GuiWindowContentProfile +// and the rest), so the pane used to read all five as the control's current +// choice: four slots crossed the threshold, every drop-down offered a +// Gui*Profile, and the boxes showed one as selected. None of it was true a +// moment later. +//----------------------------------------------------------------------------- + +function vStep6() +{ + GuiEditor.themeName = $vTheme.getName(); + + // Exactly what the brain does, in its order. + $vDropped = new GuiWindowCtrl(); + vCheck("a fresh window wears its constructor's profile", + $vDropped.Profile $= "GuiWindowProfile"); + + GuiEditor.brain.addNewCtrl($vDropped); + GuiEditor.themeApplier.applyToBranch($vDropped, $vTheme, false); + GuiEditor.brain.postEvent("Rethemed", $vDropped); + + %pane = GuiEditor.inspectorWindow.pane; + vCheck("the drop left the pane on the dropped control", %pane.target == $vDropped); + vCheck("the control ended up on the theme", + $vDropped.Profile $= $vTheme.getProfile("Window").getName()); + + // The symptom. Not "no Variants section at all" -- by now this theme has a + // second WindowContent and there is a standalone stamped WindowCloseButton, + // so those two slots have a genuine choice and should show. The bug was the + // slots with NO choice showing, because the constructor's profile counted + // as one. WindowButton has a single member and no standalone, so its two + // slots are the clean test. + vCheck("a slot with no real choice stays hidden after a drop", + !vHasRow(%pane, "minButtonProfile") && !vHasRow(%pane, "maxButtonProfile")); + + // And no constructor default may appear anywhere, including in the rows + // that are legitimately on show. + %slots = %pane.panelFields["Variants"]; + %leaked = ""; + for(%i = 0; %i < getWordCount(%slots); %i++) + { + %field = getWord(%slots, %i); + if(%pane.row[%field].editor.findItemText("Gui" @ %field, false) >= 0) + { + %leaked = %leaked SPC %field; + } + } + vCheck("no constructor default leaked into a Variants row (" @ %leaked @ ")", + %leaked $= ""); + vCheck("content row offers the theme's member", + !vHasRow(%pane, "contentProfile") || + vOffers(%pane, "contentProfile", $vTheme.getProfile("WindowContent").getName())); + + // And the header must show what the control actually wears, not the + // constructor default it was announced with. + vCheck("the header shows the themed profile", + %pane.header.profileRow.getValue() $= $vTheme.getProfile("Window").getName()); + vCheck("the constructor default is not offered", + %pane.header.profileRow.editor.findItemText("GuiWindowProfile", false) < 0); + + schedule(200, 0, "vStep7"); +} + +// Set Theme sweeps the document; the pane has to hear about that too, and the +// list it rebuilds must not keep a ghost of the theme it just left. +function vStep7() +{ + %pane = GuiEditor.inspectorWindow.pane; + %before = $vDropped.Profile; + + GuiEditor.setTheme($vOther, false); + + vCheck("Set Theme moved the control", $vDropped.Profile !$= %before); + vCheck("the header followed Set Theme without a reselect", + %pane.header.profileRow.getValue() $= $vOther.getProfile("Window").getName()); + vCheck("the old theme's profile is not left in the list", + %pane.header.profileRow.editor.findItemText(%before, false) < 0); + + echo("IVSMOKE DONE"); + schedule(300, 0, "quit"); +} diff --git a/tests/smoke/standalone.cs b/tests/smoke/standalone.cs index e4869f27d..237a25aa6 100644 --- a/tests/smoke/standalone.cs +++ b/tests/smoke/standalone.cs @@ -63,6 +63,18 @@ function dropdownOffers(%ctrl, %text) return false; } +// Bind the Gui Editor's properties pane to a control and ask what its Profile +// picker offers. This used to read the native GuiInspector's dropdown, which +// listed every named profile in the sim; the pane offers the candidates for the +// control's category instead, so the question is the same but the place to ask +// it moved. +function paneOffers(%ctrl, %text) +{ + %pane = GuiEditor.inspectorWindow.pane; + %pane.bind(%ctrl); + return dropdownOffers(%pane.header.profileRow.editor, %text); +} + function previewSampleClass(%dialog, %index) { %stage = %dialog.preview.stage; @@ -277,16 +289,27 @@ function reproStep5() Extent = "100 30"; Text = "Probe"; }; - GuiEditor.inspectorWindow.inspector.inspect(%button); - - smokeCheck("inspector offers a known engine profile", - dropdownOffers(GuiEditor.inspectorWindow.inspector, "GuiDefaultProfile")); - smokeCheck("inspector offers the standalone profile", - dropdownOffers(GuiEditor.inspectorWindow.inspector, "RubySwitch")); - smokeCheck("inspector offers the migrated legacy profile", - dropdownOffers(GuiEditor.inspectorWindow.inspector, "LegacyProfile")); - - GuiEditor.inspectorWindow.inspector.clear(); + // RubySwitch is stamped for the Button category, which is what a + // GuiButtonCtrl's own Profile slot asks for, so it is a candidate. + smokeCheck("pane offers the standalone profile", + paneOffers(%button, "RubySwitch")); + + // LegacyProfile carries no category at all -- what the Profile Editor shows + // as "Any". Those are offered as a control's main profile (the slot exists + // regardless, so listing one costs nothing) but never make a secondary + // Variants slot appear, which is the rule that keeps one uncategorised + // profile from sprouting a row on every slot of every control. + smokeCheck("pane offers the uncategorised legacy profile", + paneOffers(%button, "LegacyProfile")); + + // Deliberately NOT offered any more. GuiDefaultProfile is a script profile, + // neither a theme member nor a standalone the editor manages; the old + // inspector listed every named profile in the sim, which is the flat + // several-hundred-entry dropdown the pane exists to replace. + smokeCheck("pane does not offer a bare script profile", + !paneOffers(%button, "GuiDefaultProfile")); + + GuiEditor.inspectorWindow.pane.unbind(); %button.delete(); // Reopen for the delete pass, and give the profile a custom border first: @@ -351,12 +374,11 @@ function reproStep7() Extent = "100 30"; Text = "Probe"; }; - GuiEditor.inspectorWindow.inspector.inspect(%button); - smokeCheck("inspector no longer offers the deleted profile", - !dropdownOffers(GuiEditor.inspectorWindow.inspector, "RubySwitch")); - smokeCheck("inspector still offers the surviving profile", - dropdownOffers(GuiEditor.inspectorWindow.inspector, "LegacyProfile")); - GuiEditor.inspectorWindow.inspector.clear(); + smokeCheck("pane no longer offers the deleted profile", + !paneOffers(%button, "RubySwitch")); + smokeCheck("pane still offers the surviving profile", + paneOffers(%button, "LegacyProfile")); + GuiEditor.inspectorWindow.pane.unbind(); %button.delete(); %names = "RubyButton" TAB "RubySwitch" TAB "LegacyProfile"; From 7b3511865a263be9bdcbb0500acb01b0ba749e53 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Wed, 29 Jul 2026 09:15:31 -0400 Subject: [PATCH 08/55] Gui Editor: let a GuiControl be told what it is, and give its text a home Building a high score dialog hit two defects at once. A GuiControl dropped into a panel had its category guessed once, at drop time, from what it held -- no text yet, so Empty -- and typing the caption afterwards re-ran nothing, so the Profile drop-down had one entry in it and no way to reach a Label. Reaching for the font size instead failed too: applyFilter hid all nine text fields whenever the header carried the text block, and the header carried three of them, so fontSizeAdjust, fontColor, overrideFontColor, textWrap and textExtend had no home at all on any render, caption or placeholder class. A Category row now sits between Name and Profile, on the one class in the palette whose category cannot be read off its type, offering the four things a bare GuiControl is: Empty, Panel, Label, Overlay. Picking one writes that category's profile at once, so the picker and the profile can never disagree. It needs no new state -- a profile carries the category it was stamped for, so what the control wears is the record of what it was told to be, and it survives a reselect, a save and a Set Theme. GuiEditorTextBlock replaces the nine scattered rows with one component: a caption row carrying the wrap and extend flags, a three-line wrapped text box, one H:/V: alignment row, and the font size and colour pair. Two instances exist -- the header's and the Text section's -- and the pane points the three shared row names at whichever is in use, which is what stops them fighting over one entry in the registry. Three things found on the way, each of which made the field it touched not work: - The field row rounded every float. getValue ran mFloor, so a font size of 1.5 committed as 1 and a slider's value and range lost everything after the point. Latent for as long as those rows were invisible. New decimal and pointf kinds: decimal input mode, no flooring, arrow keys stepping a tenth. - A profile's category reads back in whatever case StringTable interned the word in first -- "empty", not "Empty" -- so matching it needs $=, not the strstr-based listHas. That read as "this profile is not one of the four" and quietly fell back to the guess. - textExtend is live with wrap off, whatever the pane assumed: guiControl.cc grows the width when wrap is off and the height when it is on. Both icons stay clickable and the tooltip says which axis. The text box is now a real multi-line editor, which took four engine fixes: - GFont::isValidChar answered true for a line break, because every caller reads that as "draw this and count its width" and WinFont::isValidChar returns true for everything but NUL, its range check commented out. A newline in any control's text drew the font's missing-glyph box and took up space. Answered in the cross-platform layer so every back-end agrees. - GuiControl::getLineList built its paragraphs with getline, which cannot tell an empty string from no string -- so empty text produced no lines at all, and a line block is what draws the caret. An empty multi-line box had no cursor in it. It also dropped the empty paragraph a trailing newline makes, and called back() on it, which is undefined behaviour. - handleEnterKey inserts a line break in a wrapped box instead of ending the edit. Nothing in the repo sets textWrap on a GuiTextEditCtrl, so no existing content changes behaviour. - onUndo now reports the buffer change like every other path that makes one. The up and down arrows work in a text box again. GuiTextEditCtrl hands a script onUpArrow the key before its own caret movement and isMethod answers for the class, not the instance, so the spinner class that every row input wore swallowed both arrows in every box that was not a number. It goes on numeric rows only now. The control also fills in as you type: GuiTextEditCtrl already ran its Command on every buffer change, so the box writes the control directly per keystroke and the pane writes it once, properly, when the box loses focus -- one edit rather than one per character. tests/smoke/inspectorText.cs covers the pane half and tests/smoke/textEdit.cs the engine half, driving real clicks and real Return and Up keys. All 21 suites pass. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qi2pZ9UzF5rsLCNVErpzwt --- editor/GuiEditor/GuiEditor.cs | 1 + .../GuiEditor/scripts/GuiEditorChoiceRow.cs | 5 +- .../GuiEditor/scripts/GuiEditorControlSpec.cs | 101 +++- .../GuiEditor/scripts/GuiEditorHeaderBlock.cs | 94 ++-- .../scripts/GuiEditorInspectorPane.cs | 402 ++++++++++++++-- .../GuiEditor/scripts/GuiEditorTextBlock.cs | 444 ++++++++++++++++++ .../scripts/GuiProfileEditorFieldRow.cs | 86 +++- engine/source/graphics/gFont.h | 13 + engine/source/gui/guiControl.cc | 33 +- engine/source/gui/guiTextEditCtrl.cc | 45 ++ engine/source/gui/guiTextEditCtrl.h | 1 + tests/shots/inspectorPane.cs | 4 + tests/shots/inspectorText.cs | 117 +++++ tests/smoke/inspectorPane.cs | 17 +- tests/smoke/inspectorSpec.cs | 20 +- tests/smoke/inspectorText.cs | 391 +++++++++++++++ tests/smoke/textEdit.cs | 184 ++++++++ tests/smoke/textEdit.input.ps1 | 29 ++ 18 files changed, 1861 insertions(+), 126 deletions(-) create mode 100644 editor/GuiEditor/scripts/GuiEditorTextBlock.cs create mode 100644 tests/shots/inspectorText.cs create mode 100644 tests/smoke/inspectorText.cs create mode 100644 tests/smoke/textEdit.cs create mode 100644 tests/smoke/textEdit.input.ps1 diff --git a/editor/GuiEditor/GuiEditor.cs b/editor/GuiEditor/GuiEditor.cs index 2095c0aaf..2a9a1cf48 100644 --- a/editor/GuiEditor/GuiEditor.cs +++ b/editor/GuiEditor/GuiEditor.cs @@ -55,6 +55,7 @@ exec("./scripts/GuiEditorToggleIcon.cs"); exec("./scripts/GuiEditorChoiceRow.cs"); exec("./scripts/GuiEditorAnchorPicker.cs"); + exec("./scripts/GuiEditorTextBlock.cs"); exec("./scripts/GuiEditorHeaderBlock.cs"); exec("./scripts/GuiEditorDynamicFields.cs"); exec("./scripts/GuiEditorInspectorPane.cs"); diff --git a/editor/GuiEditor/scripts/GuiEditorChoiceRow.cs b/editor/GuiEditor/scripts/GuiEditorChoiceRow.cs index dd04d4d6f..31ae18ce3 100644 --- a/editor/GuiEditor/scripts/GuiEditorChoiceRow.cs +++ b/editor/GuiEditor/scripts/GuiEditorChoiceRow.cs @@ -42,7 +42,10 @@ function GuiEditorChoiceRow::build(%this) { - %labelW = 68; + // 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; diff --git a/editor/GuiEditor/scripts/GuiEditorControlSpec.cs b/editor/GuiEditor/scripts/GuiEditorControlSpec.cs index a8ee20c97..7f16f66f6 100644 --- a/editor/GuiEditor/scripts/GuiEditorControlSpec.cs +++ b/editor/GuiEditor/scripts/GuiEditorControlSpec.cs @@ -586,13 +586,93 @@ %ctrl.rendersChildren(); } -// The header shows a text box for the roles that have a string of their own; -// inherited and proxy put theirs in a collapsed section instead, because a grid -// that can technically draw text is not a control anyone reaches for text on. -function GuiEditorControlSpec::textBelongsInHeader(%this, %class) +// 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); - return %role $= "render" || %role $= "caption" || %role $= "placeholder"; + 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 colour +// swatch is both fields, because picking a colour 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 @@ -648,9 +728,16 @@ switch$(%type) { case "bool": return "bool"; - case "int" or "char" or "float": return "number"; + 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" or "Point2F" or "Vector2": return "point"; + 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"; diff --git a/editor/GuiEditor/scripts/GuiEditorHeaderBlock.cs b/editor/GuiEditor/scripts/GuiEditorHeaderBlock.cs index d923af208..27e5a754f 100644 --- a/editor/GuiEditor/scripts/GuiEditorHeaderBlock.cs +++ b/editor/GuiEditor/scripts/GuiEditorHeaderBlock.cs @@ -62,6 +62,14 @@ %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", ""); } @@ -249,63 +257,21 @@ class = "GuiEditorAnchorPicker"; // 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 colour +// 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) { - %grid = %this.pane.makeCellGrid(0); - %this.add(%grid); - %this.textGrid = %grid; - - %this.textRow = %this.pane.addFieldRow(%grid, "text", "Text", "text", ""); - - // The two alignments are segmented rows rather than drop-downs: three or - // four values, each with a picture, and which one is set matters more than - // its name. They sit directly under the text box, since that is what they - // are about. - // - // "default" leads each row and wears no icon. It is not an absence -- it is - // the value a control starts on, which getAlignmentType resolves to the - // PROFILE's alignment -- so it needs a button, just not a picture. - %this.alignRow = %this.makeAlignRow(%grid, "align", "Align", - "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.makeAlignRow(%grid, "vAlign", "V-Align", - "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. The "default" -// entry is prepended here so both rows spell it the same way. -function GuiEditorHeaderBlock::makeAlignRow(%this, %grid, %field, %label, %choices) -{ - %row = new GuiControl() - { - class = "GuiEditorChoiceRow"; - Position = "0 0"; - labelText = %label; - 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)); - } + %this.textBlock = %this.pane.makeTextBlock(%this, 0); - %row.build(); - return %row; -} - -// One of the alignment rows was clicked. -function GuiEditorHeaderBlock::onChoiceRowChanged(%this, %row) -{ - %this.pane.onHeaderChoiceChanged(%row.fieldName, %row.getValue()); + %this.textGrid = %this.textBlock; + %this.textRow = %this.textBlock.textRow; + %this.alignRow = %this.textBlock.alignRow; + %this.vAlignRow = %this.textBlock.vAlignRow; } //----------------------------------------------------------------------------- @@ -322,6 +288,10 @@ class = "GuiEditorChoiceRow"; // 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); %this.visibleButton.setVisible(!%bare); %this.activeButton.setVisible(!%bare); @@ -332,17 +302,11 @@ class = "GuiEditorChoiceRow"; // rather than sitting there wired to nothing. %this.containerButton.setVisible(%spec.isContainerFieldVisible(%ctrl)); - // The text block is only in the header for the roles that have a string of - // their own worth naming. A grid can technically draw text; it goes in a - // collapsed section instead of the first thing anyone sees. - %inHeader = %spec.textBelongsInHeader(%class); - %this.textGrid.setVisible(%inHeader); - if(%inHeader) - { - %this.textRow.setLabelText(%spec.textLabelFor(%class)); - %this.alignRow.setVisible(%spec.isFieldVisible(%class, "align")); - %this.vAlignRow.setVisible(%spec.isFieldVisible(%class, "vAlign")); - } + // 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"); %this.buildValueRows(%ctrl, %class); %this.resizeToFit(); diff --git a/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs b/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs index a8bcd2025..7d347fba1 100644 --- a/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs +++ b/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs @@ -117,7 +117,7 @@ class = "GuiEditorHeaderBlock"; %this.add(%this.variantsChain); // The shared sections, in the order the work usually goes. - %this.buildSection("Text", "Text", %this.spec.textFields()); + %this.buildTextSection(); // isContainer and useInput are both in the header's icon row now that there // is art for them, so neither has a row here. @@ -209,6 +209,51 @@ class = "GuiEditorDynamicFields"; 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); + } +} + +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) { %panel = %this.makeSectionPanel(%title); @@ -230,12 +275,21 @@ class = "GuiEditorDynamicFields"; } } -function GuiEditorInspectorPane::addFieldRow(%this, %container, %field, %label, %kind, %enumItems) +// 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 = "GuiProfileEditorFieldRow"; + + // 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; @@ -247,8 +301,15 @@ class = "GuiProfileEditorFieldRow"; // 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. + // never appears. The one exception is the font colour 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); @@ -281,7 +342,10 @@ class = "GuiProfileEditorFieldRow"; case "MinExtent": return "point"; case "isContainer" or "textWrap" or "textExtend" or "overrideFontColor" or "useInput": return "bool"; case "tooltipWidth" or "hovertime": return "number"; - case "fontSizeAdjust": 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"; @@ -660,6 +724,13 @@ class = "GuiProfileEditorFieldRow"; %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++) { @@ -677,19 +748,13 @@ class = "GuiProfileEditorFieldRow"; // isContainer moved to the header's icon row, which decides its own // visibility from the same rule -- see GuiEditorHeaderBlock::bindClass. - // The header carries the text block for the roles that have a caption worth - // naming; the shared Text section is for the rest, so the two never both - // show it. - %inHeader = %spec.textBelongsInHeader(%class); - %textCount = getWordCount(%spec.textFields()); - for(%i = 0; %i < %textCount; %i++) + // 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)) { - %field = getWord(%spec.textFields(), %i); - %row = %this.row[%field]; - if(isObject(%row) && %inHeader) - { - %row.setVisible(false); - } + %block.bindClass(%this.target, %class); } // A section with nothing left to show gets out of the way entirely. @@ -716,6 +781,33 @@ class = "GuiProfileEditorFieldRow"; %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); @@ -759,17 +851,24 @@ class = "GuiProfileEditorFieldRow"; // 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. - if(!isObject(%row) || %this.isProfileSlot(%field)) + // + // fontColor is skipped for a different reason: what its swatch shows is + // the profile's colour 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)); } - // The alignment rows are segmented controls rather than field rows, so they - // are not in rowFields and load here. - %this.header.alignRow.setValue(%this.target.getFieldValue("align")); - %this.header.vAlignRow.setValue(%this.target.getFieldValue("vAlign")); + // What the text block holds beyond its three field rows: two segmented rows, + // two toggles, and the font colour swatch. + %block = %this.activeTextBlock(); + if(isObject(%block)) + { + %block.load(%this.target); + } %this.refreshToggles(); %this.header.anchorPicker.readEnums( @@ -1004,13 +1103,21 @@ class = "GuiProfileEditorFieldRow"; 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)); - %current = GuiEditor.themeApplier.fieldProfile(%ctrl, %field); if(isObject(%current)) { %list = %this.addUnique(%list, %current); @@ -1028,11 +1135,82 @@ class = "GuiProfileEditorFieldRow"; } 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); - %main = %applier.categoryForControl(%ctrl, %isRoot); - return %applier.categoryForField(%field, %main); + %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. + %this.writeField("Profile", %profile.getId()); + } + + // Everything downstream of the profile moves with it -- which profiles the + // slot now offers, and the font colour the swatch falls back to. + %this.refresh(); + %this.afterCommit(); } function GuiEditorInspectorPane::addUnique(%this, %list, %additions) @@ -1059,6 +1237,11 @@ class = "GuiProfileEditorFieldRow"; return; } + if(%this.header.categoryRow.isVisible()) + { + %this.fillCategoryRow(); + } + if(%this.header.profileRow.isVisible()) { %this.fillProfileRow(%this.header.profileRow, "Profile"); @@ -1077,6 +1260,29 @@ class = "GuiProfileEditorFieldRow"; } } +// 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. @@ -1122,6 +1328,26 @@ class = "GuiProfileEditorFieldRow"; return; } + %field = %row.fieldName; + + // The font colour swatch is two fields, and the second of them is why the + // changed test cannot come first: picking the colour 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. @@ -1130,7 +1356,15 @@ class = "GuiProfileEditorFieldRow"; return; } - %field = %row.fieldName; + // 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 @@ -1181,11 +1415,123 @@ class = "GuiProfileEditorFieldRow"; } } -// The row widget's reset means "back to the theme's stamped value", which has -// no analogue for a control, so the button is hidden and this can never fire. -// It exists because the row's owner contract names it. +//----------------------------------------------------------------------------- +// 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 colour, 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 colour turns the override on, and the row's reset button +// turns it off again. With it off the swatch shows the profile's own colour, +// so the row always says what the control will actually draw in. +//----------------------------------------------------------------------------- + +function GuiEditorInspectorPane::commitFontColor(%this, %row) +{ + // Nothing to do only when the colour is unchanged AND the override was + // already on. Picking the profile's exact colour while it was off is the + // user asking to pin that colour down, which is a change to the control + // even though the swatch looks the same. + if(!%row.hasChanged() && %this.target.overrideFontColor) + { + return; + } + + %this.writeField("fontColor", %row.getValue()); + %this.writeField("overrideFontColor", true); + + %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 colour row keeps it, meaning "stop overriding the +// profile's". function GuiEditorInspectorPane::onProfileRowReset(%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 diff --git a/editor/GuiEditor/scripts/GuiEditorTextBlock.cs b/editor/GuiEditor/scripts/GuiEditorTextBlock.cs new file mode 100644 index 000000000..c8b0a1c68 --- /dev/null +++ b/editor/GuiEditor/scripts/GuiEditorTextBlock.cs @@ -0,0 +1,444 @@ + +//----------------------------------------------------------------------------- +// 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 colour, 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", + $EditorIcon::align_just, + "Wraps onto as many lines as it needs", + "Draws on one line whatever its width"); + + %this.extendButton = %this.makeIcon(%row, %w - (%size + 2), %size, "textExtend", + $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, %frame, %tipOn, %tipOff) +{ + %button = new GuiCheckBoxCtrl() + { + class = "GuiEditorToggleIcon"; + HorizSizing = "left"; + Position = %x SPC 1; + Extent = %size SPC %size; + frameOn = %frame; + frameOff = %frame; + tipOn = %tipOn; + tipOff = %tipOff; + toggleName = %field; + 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 the wrapped text" + : "Grows wider to fit the text"; + + %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 = "GuiEditorChoiceRow"; + Position = "0 0"; + labelText = %label; + labelWidth = 20; + 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 colour. +//----------------------------------------------------------------------------- + +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 colour + // 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 colour, 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. +function GuiEditorTextBlock::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); +} + +//----------------------------------------------------------------------------- +// 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 colour while +// it is overriding, and the profile's while it is not. Anything else would have +// the row claim a colour 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/GuiProfileEditorFieldRow.cs b/editor/GuiEditor/scripts/GuiProfileEditorFieldRow.cs index 0129a8f5d..7b9c000b5 100644 --- a/editor/GuiEditor/scripts/GuiProfileEditorFieldRow.cs +++ b/editor/GuiEditor/scripts/GuiProfileEditorFieldRow.cs @@ -42,8 +42,17 @@ %pad = 4; %resetW = 24; %labelH = 16; - %editorY = %labelH + 4; - %editorH = 24; + + // 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 three lines of a wrapped text box rather than one line of + // a plain one. Everything else about it is a text row. + %editorH = (%this.kind $= "multiline") ? 62 : 24; %h = %editorY + %editorH + 4; // The editor stops short of the reset button so the two never overlap once @@ -61,6 +70,7 @@ Text = %this.labelText; align = "left"; vAlign = "middle"; + Visible = %captioned; }; ThemeManager.setProfile(%this.label, "labelProfile"); %this.add(%this.label); @@ -116,9 +126,9 @@ class = "GuiProfileEditorRowDropDown"; %this.fillItems(%this.enumItems); } } - else if(%kind $= "point") + else if(%kind $= "point" || %kind $= "pointf") { - // A Point2I field ("x y") gets one box per axis; either one commits both. + // 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"); @@ -136,9 +146,23 @@ class = "GuiProfileEditorRowDropDown"; // 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", "width"); + %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 @@ -178,23 +202,46 @@ class = "EditorIconButton"; %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 GuiProfileEditorFieldRow::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 GuiProfileEditorFieldRow::makeInput(%this, %x, %y, %w, %h, %numeric, %sizing) { + %decimal = %this.isDecimalKind(); + %box = new GuiTextEditCtrl() { - class = "GuiProfileEditorRowInput"; + // 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 ? "GuiProfileEditorRowInput" : ""; 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) { - %box.inputMode = "Number"; + // 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();"; @@ -278,7 +325,7 @@ class = "GuiProfileEditorColorPopup"; { %this.selectItem(%value); } - else if(%kind $= "point") + else if(%kind $= "point" || %kind $= "pointf") { %this.editor.setText(getWord(%value, 0)); %this.editorY.setText(getWord(%value, 1)); @@ -304,17 +351,26 @@ class = "GuiProfileEditorColorPopup"; { return %this.editor.getText(); } - if(%kind $= "point") + if(%kind $= "point" || %kind $= "pointf") { - return mFloor(%this.editor.getText()) SPC mFloor(%this.editorY.getText()); + return %this.numberIn(%this.editor) SPC %this.numberIn(%this.editorY); } - if(%kind $= "number") + if(%kind $= "number" || %kind $= "decimal") { - return mFloor(%this.editor.getText()); + 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 GuiProfileEditorFieldRow::numberIn(%this, %box) +{ + return %this.isDecimalKind() ? %box.getText() : mFloor(%box.getText()); +} + //----------------------------------------------------------------------------- // Drop-down contents. //----------------------------------------------------------------------------- @@ -499,7 +555,11 @@ class = "GuiProfileEditorColorPopup"; { return; } - %this.setText(%this.getText() + %delta); + + // 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(); } diff --git a/engine/source/graphics/gFont.h b/engine/source/graphics/gFont.h index 873e2981e..0cfc450e4 100755 --- a/engine/source/graphics/gFont.h +++ b/engine/source/graphics/gFont.h @@ -220,6 +220,19 @@ inline U32 GFont::getCharHeight(const UTF16 in_charIndex) inline bool GFont::isValidChar(const UTF16 in_charIndex) const { + // A line break is not a glyph. Every caller reads "valid" as "draw this and + // count its width", and a platform font may well claim it is drawable -- + // WinFont::isValidChar answers true for everything but NUL, its range check + // commented out -- so a newline inside a string rendered as the font's + // missing-glyph box AND took up space, which in a text edit moved the caret + // as well. Answered here rather than in each back-end so every platform + // agrees. + // + // Before the remap table, not after: a font cached to a .uft with the box + // already in it would otherwise go on saying yes. + if(in_charIndex == '\n' || in_charIndex == '\r') + return false; + if(mRemapTable[in_charIndex] != -1) return true; diff --git a/engine/source/gui/guiControl.cc b/engine/source/gui/guiControl.cc index 66f187593..834b22b1c 100755 --- a/engine/source/gui/guiControl.cc +++ b/engine/source/gui/guiControl.cc @@ -2427,12 +2427,32 @@ vector GuiControl::getLineList(const char* text, GuiControlProfile* prof } else { + // Split on newlines by hand rather than with getline, which cannot tell + // an empty string from no string: it fails immediately on "", so an + // empty control produced NO lines at all. A line block is what draws a + // GuiTextEditCtrl's caret, which is why an empty multi-line box had no + // cursor in it. getline also drops the empty paragraph a trailing + // newline creates, so pressing return at the end of the text left the + // caret with no line to sit on. + // + // The newline stays on the end of its paragraph, for the same reason the + // wrapping below re-appends the space it consumed: GuiTextEditCtrl finds + // the character offset of a line by summing the lengths of the lines + // above it (renderLineList), so a character dropped here moves the + // caret. Nothing draws or measures it -- both dglDrawText and + // GFont::getStrWidth skip a character the font has no glyph for. vector paragraphList = vector(); - istringstream f(text); - string s; - while (getline(f, s)) { - paragraphList.push_back(s); + string paragraphBuffer; + for (const char* c = text; *c != '\0'; c++) + { + paragraphBuffer += *c; + if (*c == '\n') + { + paragraphList.push_back(paragraphBuffer); + paragraphBuffer.clear(); + } } + paragraphList.push_back(paragraphBuffer); for (string& paragraph : paragraphList) { @@ -2470,7 +2490,10 @@ vector GuiControl::getLineList(const char* text, GuiControlProfile* prof line = word; } } - if (paragraph.back() == ' ') + // back() on an empty string is undefined behaviour, and an empty + // paragraph is ordinary now: it is what a blank line between two + // others is made of. + if (!paragraph.empty() && paragraph.back() == ' ') { line += " "; } diff --git a/engine/source/gui/guiTextEditCtrl.cc b/engine/source/gui/guiTextEditCtrl.cc index e553d4aba..c3c3a864f 100755 --- a/engine/source/gui/guiTextEditCtrl.cc +++ b/engine/source/gui/guiTextEditCtrl.cc @@ -867,6 +867,10 @@ void GuiTextEditCtrl::onUndo() mUndoText = tempText; mUndoSelector = tempSelector; + + // Every other path that changes the buffer reports it, so a Command + // watching the box live would have gone quiet on ctrl+Z alone. + execConsoleCallback(); } bool GuiTextEditCtrl::onKeyDown(const GuiEvent &event) @@ -1614,6 +1618,17 @@ bool GuiTextEditCtrl::handleEscapeKey() bool GuiTextEditCtrl::handleEnterKey() { + // Wrapping is what makes this control multi-line -- it is the flag that + // decides whether the text is one line or a paragraph -- so in a wrapped + // box return is a line break rather than "I am done with this field". + // It takes precedence over returnCommand and returnCausesTab, which are + // how a single-line box ends an edit; a box with a paragraph in it ends + // its edit by losing focus. + if (mTextWrap) + { + return insertNewLine(); + } + if (isMethod("onReturn")) Con::executef(this, 1, "onReturn"); @@ -1639,6 +1654,36 @@ bool GuiTextEditCtrl::handleEnterKey() return true; } +// A newline goes in the buffer directly rather than through +// handleCharacterInput: the font has no glyph for it, so isValidChar would +// refuse it, and no InputMode has an opinion about it worth honouring -- a +// Number-only box is single-line and never reaches this. +bool GuiTextEditCtrl::insertNewLine() +{ + saveUndoState(); + + if (mSelector.hasSelection()) + { + mSelector.eraseSelection(mTextBuffer); + } + + if (mTextBuffer.length() >= mMaxStrLen) + { + keyDenied(); + return true; + } + + // Always an insert, never an overwrite: there is no character in a line + // break for insert-off mode to replace. + mTextBuffer.insert(mSelector.getCursorPos(), "\n"); + mSelector.setTextLength(mTextBuffer.length()); + mSelector.stepCursorForward(); + setText(mTextBuffer); + + execConsoleCallback(); + return true; +} + bool GuiTextEditCtrl::handleArrowKey(GuiDirection direction) { if (direction == GuiDirection::Left) diff --git a/engine/source/gui/guiTextEditCtrl.h b/engine/source/gui/guiTextEditCtrl.h index e9e5c60c8..7d62f8be5 100755 --- a/engine/source/gui/guiTextEditCtrl.h +++ b/engine/source/gui/guiTextEditCtrl.h @@ -166,6 +166,7 @@ class GuiTextEditCtrl : public GuiControl virtual bool handleCharacterInput(const GuiEvent& event); virtual bool handleEscapeKey(); virtual bool handleEnterKey(); + virtual bool insertNewLine(); virtual bool handleArrowKey(GuiDirection direction); virtual bool handleShiftArrowKey(GuiDirection direction); virtual bool handleBackSpace(); diff --git a/tests/shots/inspectorPane.cs b/tests/shots/inspectorPane.cs index 9e5249e98..a243351f4 100644 --- a/tests/shots/inspectorPane.cs +++ b/tests/shots/inspectorPane.cs @@ -94,6 +94,10 @@ function sShoot() // Every section open, so the shot shows what each control actually offers // rather than a column of collapsed headers. + // textPanel is not in panelList: it holds one component rather than a list + // of rows, so the pane decides its visibility outright. + %pane.textPanel.setExpanded(true); + %panels = %pane.panelList SPC %pane.classPanels; for(%i = 0; %i < getWordCount(%panels); %i++) { diff --git a/tests/shots/inspectorText.cs b/tests/shots/inspectorText.cs new file mode 100644 index 000000000..259ebe6f4 --- /dev/null +++ b/tests/shots/inspectorText.cs @@ -0,0 +1,117 @@ +// Visual harness for the Category picker and the text block: the high-score +// heading from the bug report, before and after. +// +// text-empty a GuiControl inside a panel, which is what you get when you +// drop one and type a caption into it afterwards -- the guess ran +// when it was dropped, saw no text, and made it an Empty. +// text-label the same control after its Category is set to Label and its +// font size raised, which is the whole of the fix from the +// outside. +// +// Run: tests/run.ps1 -Shots inspectorText ; look in shots/. + +setLogMode(1); +$Scripts::ignoreDSOs = true; +setScriptExecEcho(false); +trace(false); + +testExec("editor/main.cs"); +schedule(2500, 0, "sOpenProject"); + +function sOpenProject() +{ + ProjectManager.setProjectFolder("PlanetX"); + EditorCore.projectSelector.onProjectSelected(pathConcat(getMainDotCsDir(), "PlanetX")); + schedule(2500, 0, "sOpenEditor"); +} + +function sOpenEditor() +{ + EditorCore.toggleEditor(); + EditorCore.tabBook.selectPage(3); + schedule(1500, 0, "sStep1"); +} + +function sStep1() +{ + $sTheme = GuiEditor.themeLibrary.createTheme("TextShotTheme"); + GuiEditor.themeName = $sTheme.getName(); + + // The dialog: a backdrop with a heading in it. The heading is themed while + // it is still empty, which is the whole of the problem. + $sPanel = new GuiControl() + { + Position = "40 40"; + Extent = "260 180"; + }; + GuiEditor.rootGui.add($sPanel); + + $sHeading = new GuiControl() + { + Position = "20 16"; + Extent = "220 32"; + }; + $sPanel.add($sHeading); + GuiEditor.themeApplier.applyToBranch($sPanel, $sTheme, true); + + $sHeading.text = "High Scores"; + + createPath(testRoot("shots/")); + schedule(500, 0, "sShootEmpty"); +} + +function sShootEmpty() +{ + sBind(); + schedule(500, 0, "sGrab", "text-empty", "sFix"); +} + +// What the fix is, in two edits: say what the control is, then set the size the +// caption wants. +function sFix() +{ + %pane = GuiEditor.inspectorWindow.pane; + + %row = %pane.header.categoryRow; + %row.applyValue("Label"); + %row.commit(); + + %row = %pane.row["fontSizeAdjust"]; + %row.applyValue("1.6"); + %row.commit(); + + sBind(); + schedule(500, 0, "sGrab", "text-label", ""); +} + +function sBind() +{ + %pane = GuiEditor.inspectorWindow.pane; + %pane.bind($sHeading); + + %pane.textPanel.setExpanded(true); + %panels = %pane.panelList SPC %pane.classPanels; + for(%i = 0; %i < getWordCount(%panels); %i++) + { + %panel = %pane.panel[getWord(%panels, %i)]; + if(isObject(%panel) && %panel.isVisible()) + { + %panel.setExpanded(true); + } + } +} + +function sGrab(%name, %next) +{ + screenShot(testRoot("shots/" @ %name @ ".png"), "PNG"); + echo("SHOT: wrote " @ %name @ ".png"); + + if(%next !$= "") + { + schedule(300, 0, %next); + return; + } + + echo("SHOT DONE"); + schedule(300, 0, "quit"); +} diff --git a/tests/smoke/inspectorPane.cs b/tests/smoke/inspectorPane.cs index 8d4679e0d..b0807b4c4 100644 --- a/tests/smoke/inspectorPane.cs +++ b/tests/smoke/inspectorPane.cs @@ -138,8 +138,11 @@ function pStep2() pCheck("button shows easing", pRowShown(%pane, "easeFillColorHL")); pCheck("button shows tooltip", pRowShown(%pane, "tooltip")); pCheck("button can be a container", %pane.header.containerButton.isVisible()); - pCheck("shared text row hidden while header owns it", - !pRowShown(%pane, "text")); + // One text block, in one place. The row the pane filters and loads IS the + // header block's -- there is no second copy of it to disagree with. + pCheck("the text row on show is the header block's", + pRowShown(%pane, "text") && %pane.row["text"] == %pane.header.textBlock.row["text"]); + pCheck("the text section is not also showing", !%pane.textPanel.isVisible()); // The header loaded the control's actual values. pCheck("name row loaded", %pane.header.nameRow.getValue() $= $pButton.getName()); @@ -178,7 +181,11 @@ function pStep3() pCheck("chain hides the header text block", !%pane.header.textGrid.isVisible()); pCheck("chain hides the shared text row", !pRowShown(%pane, "text")); - pCheck("chain hides align", !pRowShown(%pane, "align")); + // A chain draws no text at all, so the block is in neither of its homes -- + // checked at both ends, because "no row" and "a hidden row" read the same + // through pRowShown and only one of them is the answer here. + pCheck("chain hides the text section", !%pane.textPanel.isVisible()); + pCheck("chain has no text row at all", !pRowBuilt(%pane, "text")); pCheck("chain hides easing", !pRowShown(%pane, "easeFillColorHL")); pCheck("chain promotes IsVertical", pRowBuilt(%pane, "IsVertical")); pCheck("chain promotes ChildSpacing", pRowBuilt(%pane, "ChildSpacing")); @@ -190,7 +197,9 @@ function pStep3() %pane = pBind($pGrid); pCheck("grid hides the header text block", !%pane.header.textGrid.isVisible()); pCheck("grid keeps the shared text row", pRowShown(%pane, "text")); - pCheck("grid text section shown", %pane.panel["Text"].isVisible()); + pCheck("grid text section shown", %pane.textPanel.isVisible()); + pCheck("and it is the section block's row", + %pane.row["text"] == %pane.sectionText.row["text"]); pCheck("grid promotes CellModeX", pRowBuilt(%pane, "CellModeX")); pCheck("grid has its Grid section", pRowBuilt(%pane, "MaxColCount")); diff --git a/tests/smoke/inspectorSpec.cs b/tests/smoke/inspectorSpec.cs index 0b6178cda..fb6b6c4fb 100644 --- a/tests/smoke/inspectorSpec.cs +++ b/tests/smoke/inspectorSpec.cs @@ -106,8 +106,17 @@ function sStep2() sCheck("panel hides textWrap", !%s.isFieldVisible("GuiPanelCtrl", "textWrap")); // Where the text box belongs, and what it is called there. - sCheck("button text in header", %s.textBelongsInHeader("GuiButtonCtrl")); - sCheck("grid text not in header", !%s.textBelongsInHeader("GuiGridCtrl")); + // Where the text block goes, which is one answer per class rather than the + // two overlapping ones it used to be. Proxy is the interesting case: a list + // has no string of its own but draws its items with this control's font, so + // the block is worth having open. + sCheck("button text in header", %s.textBlockHome("GuiButtonCtrl") $= "header"); + sCheck("panel caption in header", %s.textBlockHome("GuiPanelCtrl") $= "header"); + sCheck("tree item font in header", %s.textBlockHome("GuiTreeViewCtrl") $= "header"); + sCheck("grid text in the section", %s.textBlockHome("GuiGridCtrl") $= "section"); + sCheck("slider keeps only its font size", %s.textBlockHome("GuiSliderCtrl") $= "section"); + sCheck("a chain gets no text block", %s.textBlockHome("GuiChainCtrl") $= "none"); + sCheck("nor does a sprite", %s.textBlockHome("GuiSpriteCtrl") $= "none"); sCheck("grid text still shown", %s.isFieldVisible("GuiGridCtrl", "text")); sCheck("drop down text labelled Placeholder", %s.textLabelFor("GuiDropDownCtrl") $= "Placeholder"); @@ -260,9 +269,14 @@ function sStep5() sCheck("bool maps to bool", %s.kindForType("bool") $= "bool"); sCheck("int maps to number", %s.kindForType("int") $= "number"); sCheck("char maps to number", %s.kindForType("char") $= "number"); - sCheck("float maps to number", %s.kindForType("float") $= "number"); + // The two real-numbered kinds are separate from the whole-numbered ones + // because the row rounds on the way out, which turned a font size multiplier + // of 1.5 into a 1 and a slider value of 0.5 into a 0. + sCheck("float maps to decimal", %s.kindForType("float") $= "decimal"); sCheck("enumval maps to enum", %s.kindForType("enumval") $= "enum"); sCheck("Point2I maps to point", %s.kindForType("Point2I") $= "point"); + sCheck("Point2F maps to pointf", %s.kindForType("Point2F") $= "pointf"); + sCheck("Vector2 maps to pointf", %s.kindForType("Vector2") $= "pointf"); sCheck("ColorI maps to color", %s.kindForType("ColorI") $= "color"); sCheck("FluidColorI maps to color", %s.kindForType("FluidColorI") $= "color"); sCheck("filename maps to file", %s.kindForType("filename") $= "file"); diff --git a/tests/smoke/inspectorText.cs b/tests/smoke/inspectorText.cs new file mode 100644 index 000000000..5b9804f5e --- /dev/null +++ b/tests/smoke/inspectorText.cs @@ -0,0 +1,391 @@ +// Category picker and text block smoke test. +// +// Both halves of one bug report: a GuiControl dropped into a panel, given the +// text "High Scores", could not be made to look like a heading. Its category +// was guessed once when it was dropped -- no text yet, so Empty -- and nothing +// re-ran the guess or let it be corrected, so the Profile drop-down had one +// entry in it. Reaching for the font size instead did not work either, because +// the pane hid all nine text fields whenever the header carried the text box +// and the header carried three of them. +// +// So: the Category row exists where the class is ambiguous and nowhere else, +// picking one moves the control onto that category's profile, and every field +// GuiControl::renderText reads has a home. +// Run: tests/run.ps1 inspectorText ; grep ITSMOKE in console.log. + +setLogMode(1); +$Scripts::ignoreDSOs = true; +setScriptExecEcho(false); +trace(false); + +function tCheck(%label, %cond) +{ + if(%cond) echo("ITSMOKE PASS: " @ %label); + else echo("ITSMOKE FAIL: " @ %label); +} + +function tPane() +{ + return GuiEditor.inspectorWindow.pane; +} + +function tBind(%ctrl) +{ + %pane = tPane(); + %pane.bind(%ctrl); + return %pane; +} + +function tRowShown(%pane, %field) +{ + %row = %pane.row[%field]; + return isObject(%row) && %row.isVisible(); +} + +function tItemCount(%row) +{ + return %row.editor.getItemCount(); +} + +function tOffers(%row, %name) +{ + return %row.editor.findItemText(%name, false) >= 0; +} + +// A profile field reads back as a NAME, and in editor mode that name was never +// registered with the Sim -- so comparing what the field holds against a +// profile id is comparing a string to a number and always answering no. The +// applier already knows how to resolve one; ask it. +function tProfileOf(%ctrl) +{ + return GuiEditor.themeApplier.fieldProfile(%ctrl, "Profile"); +} + +testExec("editor/main.cs"); +schedule(2000, 0, "tStep1"); + +//----------------------------------------------------------------------------- +// The report, reproduced: a panel with a GuiControl inside it that wants to be +// a heading. +//----------------------------------------------------------------------------- + +function tStep1() +{ + ProjectManager.setProjectFolder("inspectorTextSmokeProject"); + GuiEditor.open(); + + $tTheme = GuiEditor.themeLibrary.createTheme("ITSmoke"); + tCheck("theme created", isObject($tTheme)); + GuiEditor.themeName = $tTheme.getName(); + + // The panel is a child of the simulated canvas, so it is the Gui's root and + // takes Panel. The heading is a child of it with no text yet, which is + // exactly the state the guess reads as Empty. + $tPanel = new GuiControl(); + GuiEditor.rootGui.add($tPanel); + $tHeading = new GuiControl(); + $tPanel.add($tHeading); + GuiEditor.themeApplier.applyToBranch($tPanel, $tTheme, true); + + echo("ITSMOKE: panel wears " @ tProfileOf($tPanel).getName() @ + ", heading wears " @ tProfileOf($tHeading).getName() @ + " (category '" @ tProfileOf($tHeading).category @ "')"); + + tCheck("root GuiControl took Panel", + tProfileOf($tPanel) == $tTheme.getProfile("Panel")); + tCheck("the child took Empty", + tProfileOf($tHeading) == $tTheme.getProfile("Empty")); + + // Typing the caption is what the user did next. It changes nothing about the + // profile -- the guess ran when the control was dropped. + $tHeading.text = "High Scores"; + + %pane = tBind($tHeading); + %row = %pane.header.categoryRow; + tCheck("the category row is on show", %row.isVisible()); + tCheck("and reads the category the control is on", %row.getValue() $= "Empty"); + tCheck("it offers all four", tOffers(%row, "Empty") && tOffers(%row, "Panel") && + tOffers(%row, "Label") && tOffers(%row, "Overlay")); + + // The bug: one entry, and no way to reach a Label profile. + tCheck("the profile row offers only the Empty member", + tItemCount(%pane.header.profileRow) == 1); + + schedule(200, 0, "tStep2"); +} + +//----------------------------------------------------------------------------- +// Correcting the guess. +//----------------------------------------------------------------------------- + +function tStep2() +{ + %pane = tPane(); + %row = %pane.header.categoryRow; + + %row.applyValue("Label"); + %row.commit(); + + tCheck("picking Label moved the control onto the Label profile", + tProfileOf($tHeading) == $tTheme.getProfile("Label")); + tCheck("the category row now reads Label", %row.getValue() $= "Label"); + tCheck("and the profile row offers the theme's Label member", + tOffers(%pane.header.profileRow, $tTheme.getProfile("Label").getName())); + + // The choice is recorded by the profile the control wears, so it survives a + // reselect with no state of its own. + %pane = tBind($tPanel); + %pane = tBind($tHeading); + tCheck("the category survives a reselect", + %pane.header.categoryRow.getValue() $= "Label"); + + // And back, to prove it is not one-way. + %row = %pane.header.categoryRow; + %row.applyValue("Overlay"); + %row.commit(); + tCheck("and moves again to Overlay", + tProfileOf($tHeading) == $tTheme.getProfile("Overlay")); + + %row.applyValue("Label"); + %row.commit(); + + schedule(200, 0, "tStep3"); +} + +//----------------------------------------------------------------------------- +// Only the ambiguous class gets one. Everything else is pinned by its class, +// where a picker would be a way to make a check box look like a scrollbar. +//----------------------------------------------------------------------------- + +function tStep3() +{ + $tButton = new GuiButtonCtrl(); + GuiEditor.rootGui.add($tButton); + GuiEditor.themeApplier.applyToBranch($tButton, $tTheme, true); + + %pane = tBind($tButton); + tCheck("a button has no category row", !%pane.header.categoryRow.isVisible()); + tCheck("and still offers its own profile", + tOffers(%pane.header.profileRow, $tTheme.getProfile("Button").getName())); + + schedule(200, 0, "tStep4"); +} + +//----------------------------------------------------------------------------- +// The five fields that had nowhere to go. This is the half of the report that +// was never about categories at all. +//----------------------------------------------------------------------------- + +function tStep4() +{ + %pane = tBind($tHeading); + + tCheck("font size is reachable", tRowShown(%pane, "fontSizeAdjust")); + tCheck("font colour is reachable", tRowShown(%pane, "fontColor")); + tCheck("the text box is reachable", tRowShown(%pane, "text")); + + %block = %pane.activeTextBlock(); + tCheck("the block is the header's", %block == %pane.header.textBlock); + tCheck("wrap is reachable", %block.wrapButton.isVisible()); + tCheck("extend is reachable", %block.extendButton.isVisible()); + tCheck("both alignments are reachable", + %block.alignRow.isVisible() && %block.vAlignRow.isVisible()); + + // textID went back to Localization, which is the only place that builds it + // now. It used to be built twice and hidden by the text filter with the rest. + tCheck("text id is reachable", tRowShown(%pane, "textID")); + + // The size the user could not reach. A multiplier, so the value that matters + // is a fractional one: the row used to round on the way out, which would + // have made reaching the field no better than not reaching it. + %row = %pane.row["fontSizeAdjust"]; + tCheck("the font size row takes decimals", %row.kind $= "decimal"); + + %row.applyValue("1.5"); + %row.commit(); + tCheck("setting the font size reaches the control", $tHeading.fontSizeAdjust == 1.5); + tCheck("and the row reads it back whole", %row.getValue() == 1.5); + + // The arrow keys step a multiplier by a tenth rather than by one. + %row.editor.onUpArrow(); + tCheck("an arrow key steps it by a tenth", $tHeading.fontSizeAdjust == 1.6); + + // Only a box that wants the arrows to step a value may claim them. + // GuiTextEditCtrl hands a script onUpArrow the key before its own caret + // movement, and isMethod answers for the class -- so the spinner class on a + // text box swallowed both arrows and left the caret unable to change line. + tCheck("the spinner class is on the number box", + %row.editor.class $= "GuiProfileEditorRowInput"); + tCheck("and not on the text box", %pane.row["text"].editor.class $= ""); + tCheck("so the text box has no onUpArrow to claim the key", + !%pane.row["text"].editor.isMethod("onUpArrow")); + + %row.applyValue("2"); + %row.commit(); + + schedule(200, 0, "tStepTyping"); +} + +//----------------------------------------------------------------------------- +// Typing. The control fills in as you type, but the edit still reaches it as +// one change: the text box writes the control directly per keystroke and the +// pane writes it properly once, when the box loses focus. +//----------------------------------------------------------------------------- + +function tStepTyping() +{ + %pane = tBind($tHeading); + %block = %pane.activeTextBlock(); + %row = %pane.row["text"]; + + $tTextBefore = $tHeading.text; + tCheck("nothing is being typed yet", !%block.typing); + + // What a keystroke does: the box's buffer changes and the engine runs its + // Command. Set the buffer and call the same handler the Command names. + %row.editor.setText("High Scores!"); + %block.onTextTyped(); + + tCheck("the control filled in as we typed", $tHeading.text $= "High Scores!"); + tCheck("the edit is open", %block.typing); + tCheck("and it remembers what the control said before", + %block.textBeforeEdit $= $tTextBefore); + tCheck("and which control it belongs to", %block.typingTarget == $tHeading); + + // Another keystroke must not move the stash: the whole edit is one change. + %row.editor.setText("High Scores!!"); + %block.onTextTyped(); + tCheck("a second keystroke keeps the original text stashed", + %block.textBeforeEdit $= $tTextBefore); + + // Blur. The row commits, the pane puts back what the control held when the + // edit began, and writes the new value once. + %row.commit(); + tCheck("the commit closed the edit", !%block.typing); + tCheck("and the control kept the typed text", $tHeading.text $= "High Scores!!"); + + // A commit with nothing typed must not write anything. + %row.commit(); + tCheck("committing again is a no-op", $tHeading.text $= "High Scores!!"); + + $tHeading.text = "High Scores"; + %pane.refresh(); + + schedule(200, 0, "tStep5"); +} + +//----------------------------------------------------------------------------- +// Wrap and extend. Extend does something in both wrap states -- guiControl.cc +// grows the width when wrap is off and the height when it is on -- so it must +// not be disabled with wrap off. +//----------------------------------------------------------------------------- + +function tStep5() +{ + %pane = tPane(); + %block = %pane.activeTextBlock(); + + tCheck("extend is live while wrap is off", + !$tHeading.textWrap && %block.extendButton.isActive()); + + %block.extendButton.performClick(); + tCheck("extend reached the control", $tHeading.textExtend); + tCheck("its tooltip says which way it grows", + %block.extendButton.Tooltip $= "Grows wider to fit the text"); + + %block.wrapButton.performClick(); + tCheck("wrap reached the control", $tHeading.textWrap); + tCheck("and the tooltip changed with it", + %block.extendButton.Tooltip $= "Grows taller to fit the wrapped text"); + + // Off again, and loaded back the same way on a rebind. + %block.wrapButton.performClick(); + %block.extendButton.performClick(); + tCheck("both cleared", !$tHeading.textWrap && !$tHeading.textExtend); + + %pane = tBind($tHeading); + %block = %pane.activeTextBlock(); + tCheck("the toggles reload from the control", + !%block.wrapButton.getValue() && !%block.extendButton.getValue()); + + schedule(200, 0, "tStep6"); +} + +//----------------------------------------------------------------------------- +// Font colour, which is two fields wearing one swatch. +//----------------------------------------------------------------------------- + +function tStep6() +{ + %pane = tPane(); + %row = %pane.row["fontColor"]; + + tCheck("nothing is overridden to begin with", !$tHeading.overrideFontColor); + tCheck("so the swatch shows the profile's colour", + %row.getValue() $= tProfileOf($tHeading).fontColor); + tCheck("and there is nothing to revert", !%row.resetButton.isVisible()); + + %row.editor.setColorI("10 20 30 255"); + %row.commit(); + tCheck("picking a colour wrote it", $tHeading.fontColor $= "10 20 30 255"); + tCheck("and turned the override on", $tHeading.overrideFontColor); + tCheck("the revert appeared", %row.resetButton.isVisible()); + + // The revert is the only way back to the profile's colour. + %pane.onProfileRowReset(%row); + tCheck("revert turned the override off", !$tHeading.overrideFontColor); + tCheck("the swatch fell back to the profile's colour", + %row.getValue() $= tProfileOf($tHeading).fontColor); + tCheck("and the revert went away", !%row.resetButton.isVisible()); + + schedule(200, 0, "tStep7"); +} + +//----------------------------------------------------------------------------- +// Where the block lives, class by class. There are two of it and only ever one +// on show. +//----------------------------------------------------------------------------- + +function tStep7() +{ + $tDrop = new GuiDropDownCtrl(); + GuiEditor.rootGui.add($tDrop); + $tTree = new GuiTreeViewCtrl(); + GuiEditor.rootGui.add($tTree); + $tPage = new GuiPanelCtrl(); + GuiEditor.rootGui.add($tPage); + $tGrid = new GuiGridCtrl(); + GuiEditor.rootGui.add($tGrid); + $tSprite = new GuiSpriteCtrl(); + GuiEditor.rootGui.add($tSprite); + + %pane = tBind($tDrop); + tCheck("a drop-down's placeholder is in the header", + %pane.header.textBlock.isVisible() && !%pane.textPanel.isVisible()); + + %pane = tBind($tPage); + tCheck("a panel's header text is in the header", + %pane.header.textBlock.isVisible() && !%pane.textPanel.isVisible()); + tCheck("but its dead layout fields are not offered", + !tRowShown(%pane, "fontSizeAdjust") && + !%pane.header.textBlock.alignRow.isVisible()); + + // A list draws its items with this control's font, so the layout half is + // live even though the text field itself is never drawn. + %pane = tBind($tTree); + tCheck("a tree's item font is in the header", + %pane.header.textBlock.isVisible() && !%pane.textPanel.isVisible()); + tCheck("with no string of its own to edit", !tRowShown(%pane, "text")); + tCheck("but the font size it draws them at", tRowShown(%pane, "fontSizeAdjust")); + + %pane = tBind($tGrid); + tCheck("a grid's text is in the section", + !%pane.header.textBlock.isVisible() && %pane.textPanel.isVisible()); + + %pane = tBind($tSprite); + tCheck("a sprite gets no text block at all", + !%pane.header.textBlock.isVisible() && !%pane.textPanel.isVisible()); + + echo("ITSMOKE DONE"); + schedule(300, 0, "quit"); +} diff --git a/tests/smoke/textEdit.cs b/tests/smoke/textEdit.cs new file mode 100644 index 000000000..cfbdd8a8a --- /dev/null +++ b/tests/smoke/textEdit.cs @@ -0,0 +1,184 @@ +//----------------------------------------------------------------------------- +// The multi-line text box, at the engine level. Driven by textEdit.input.ps1, +// which posts a real click and real Return/Up keys, so the whole input path +// runs rather than the methods behind it. +// +// Two behaviours, both of them GuiControl::getLineList's doing: +// +// A. How many lines a piece of text is. getLineList built its paragraph list +// with getline, which cannot tell an empty string from no string -- it +// fails immediately on "" -- so empty text produced NO lines at all, and a +// line block is what draws a GuiTextEditCtrl's caret. An empty multi-line +// box therefore had no cursor in it. getline also dropped the empty +// paragraph that a trailing newline makes, so pressing return at the end +// of the text left the caret with no line to sit on. +// +// Measured through textExtend, which sizes a control from +// textHeight * lineList.size() -- the only place the line count is visible +// from script. +// +// B. Return puts a line break in a wrapped box rather than ending the edit, +// and the up arrow then moves the caret to the line above. +//----------------------------------------------------------------------------- + +setLogMode(2); +setScriptExecEcho(false); +trace(false); +$Scripts::ignoreDSOs = true; +setCompanyAndProduct("Torque Game Engines", "Torque2D"); +ModuleDatabase.EchoInfo = false; +AssetDatabase.EchoInfo = false; + +testExec("editor/main.cs"); + +function teCheck(%label, %condition) +{ + if(%condition) echo("TESMOKE PASS: " @ %label); + else echo("TESMOKE FAIL: " @ %label); +} + +// A wrapped, extending control holding %text. Its height after a frame is +// one line per line of text, which is what makes the line count observable. +function teProbe(%stage, %x, %text) +{ + %probe = new GuiControl() + { + Position = %x SPC 400; + Extent = "180 20"; + textWrap = true; + textExtend = true; + Text = %text; + }; + ThemeManager.setProfile(%probe, "labelProfile"); + %stage.add(%probe); + return %probe; +} + +$teText = "abc def"; + +schedule(2500, 0, "teStep1"); + +function teStep1() +{ + // The Gui Editor owns the profiles these wear. + GuiEditor.open(); + + %stage = new GuiControl() + { + Position = "0 0"; + Extent = "1024 768"; + }; + ThemeManager.setProfile(%stage, "overlayProfile"); + + // vAlign top so the first line sits at the top of the content rect. Left to + // the profile it is centred, and a click above the line reads as position 0 + // (calculateIbeamPosition returns 0 for a point above the first block) -- + // which looks exactly like a click that missed. + $teBox = new GuiTextEditCtrl() + { + Position = "200 200"; + Extent = "500 90"; + textWrap = true; + vAlign = "top"; + Text = $teText; + }; + ThemeManager.setProfile($teBox, "textEditProfile"); + %stage.add($teBox); + + // One line each, two lines, and two lines where the second is empty. + $teEmpty = teProbe(%stage, 100, ""); + $teOneLine = teProbe(%stage, 300, "x"); + $teTwoLines = teProbe(%stage, 500, "x" NL "y"); + $teTrailing = teProbe(%stage, 700, "x" NL ""); + + Canvas.pushDialog(%stage); + + // Pushing a dialog hands the first responder to the box, and taking focus + // selects the whole text -- which leaves the caret at the end, exactly where + // a successful click would leave it. Give it back so that a caret at the end + // later can only be the click's doing. + $teBox.makeFirstResponder(false); + $teBox.setCursorPos(0); + + // The driver clicks into the box during this window. The windows are wide + // on purpose: the driver sleeps against the wall clock while these run + // against a boot that takes longer when the whole suite is running, and a + // key that arrives before its step has been mistaken for a broken engine + // once already. + schedule(4500, 0, "teStep2"); +} + +//----------------------------------------------------------------------------- +// A. The line count. +//----------------------------------------------------------------------------- + +function teStep2() +{ + %empty = getWord($teEmpty.getExtent(), 1); + %one = getWord($teOneLine.getExtent(), 1); + %two = getWord($teTwoLines.getExtent(), 1); + %trailing = getWord($teTrailing.getExtent(), 1); + + echo("TESMOKE: heights empty=" @ %empty @ " one=" @ %one @ + " two=" @ %two @ " trailing=" @ %trailing); + + // The one that was broken: empty text is one (blank) line, not none. + teCheck("empty text is one line, like a one-word line", %empty == %one); + teCheck("two paragraphs are taller than one", %two > %one); + teCheck("a trailing newline keeps its empty line", %trailing == %two); + + // The driver clicks past the end of the text, so the engine's own hit test + // parks the caret at the end -- which is where Return has to be for it to + // make a new empty line rather than split the text in two. Placed by the + // click rather than by setCursorPos here, so that nothing depends on this + // step running before the key arrives. + echo("TESMOKE: caret at " @ $teBox.getCursorPos() @ " of " @ strlen($teText)); + teCheck("the box took the click and the caret is at the end", + $teBox.getCursorPos() == strlen($teText)); + + // The driver presses Return during this window. + schedule(4000, 0, "teStep3"); +} + +//----------------------------------------------------------------------------- +// B. Return, and then the up arrow. +//----------------------------------------------------------------------------- + +function teStep3() +{ + %text = $teBox.getText(); + echo("TESMOKE: after return, length " @ strlen(%text) @ + " cursor " @ $teBox.getCursorPos()); + + teCheck("return added a character", strlen(%text) == (strlen($teText) + 1)); + teCheck("and the character is a line break", %text $= ($teText NL "")); + teCheck("the caret moved onto the new line", + $teBox.getCursorPos() == strlen($teText) + 1); + + // Whether the box kept the focus is answered in teStep4: a control that + // ended its edit on return would not be there to receive the up arrow. + $teCursorAfterReturn = $teBox.getCursorPos(); + + // The driver presses Up during this window. + schedule(4000, 0, "teStep4"); +} + +function teStep4() +{ + %pos = $teBox.getCursorPos(); + echo("TESMOKE: after up arrow, cursor " @ %pos @ + " (was " @ $teCursorAfterReturn @ ")"); + + // Up moves the caret to the line above. Without this the key was swallowed + // by a script onUpArrow that had nothing to do with a text box. + teCheck("the up arrow moved the caret off the new line", + %pos < $teCursorAfterReturn); + + // The same fact, read the other way: the key could only arrive because + // return left the box holding the focus instead of ending the edit. + teCheck("so return did not end the edit", %pos != $teCursorAfterReturn); + + screenShot(testRoot("shots/textEdit.png"), "PNG"); + echo("TESMOKE DONE"); + schedule(400, 0, "quit"); +} diff --git a/tests/smoke/textEdit.input.ps1 b/tests/smoke/textEdit.input.ps1 new file mode 100644 index 000000000..338e2552d --- /dev/null +++ b/tests/smoke/textEdit.input.ps1 @@ -0,0 +1,29 @@ +# Input for textEdit.cs. Posts a real click to focus the multi-line box, then a +# real Return and a real Up, so the engine's own key path decides what they do. +# +# The gaps are wide because these sleeps run against the wall clock while the +# suite's steps run against a boot that is slower when every test is running. +# With half-second margins the Return once landed before the step that reads the +# caret, while focus still had the whole text selected -- so it replaced the text +# instead of appending to it, and read as a broken engine rather than a racing +# test. Each key now lands two seconds clear of the steps either side of it. +param([IntPtr]$Hwnd) + +. "$PSScriptRoot\..\lib\input.ps1" + +# The stage is built at t=2.5s and read at t=7s. The box is at 200,200 with +# extent 500x90; the click is well past the end of "abc def", so the engine's +# hit test parks the caret at the end of the text. +Start-Sleep -Seconds 5 +Send-EngineClick -Hwnd $Hwnd -X 600 -Y 212 +Write-Host " clicked past the end of the text at (600,212)" + +# Read at t=7s, next read at t=11s. +Start-Sleep -Seconds 4 +Send-EngineKey -Hwnd $Hwnd -Key 'RETURN' +Write-Host " pressed Return" + +# Read at t=11s, next read at t=15s. +Start-Sleep -Seconds 4 +Send-EngineKey -Hwnd $Hwnd -Key 'UP' +Write-Host " pressed Up" From 3e020cf771545e6df92121755f0109adb32475f7 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Wed, 29 Jul 2026 10:47:58 -0400 Subject: [PATCH 09/55] Gui Editor: compact the properties pane, and tell each switch what it does Seven things, all of them about fitting more of an answer into the pane and fewer questions into the user's head. A toggle's tooltip is two lines now: what the switch is and which way it is set, then what that means. Visible - On Draws when the game runs. Its children draw with it... The descriptions are longer than they were on purpose. This is an editor; answering the question here is cheaper than making someone go and look it up. The heading is opt-in, from a toggleLabel on the icon -- a segmented row's buttons are choices rather than switches, and "Centre text - On" would be a worse caption than "Centre text". That needed the tooltip renderer taught about line breaks. It does its own word wrapping, splitting on spaces alone, so a newline was part of a word -- and once GFont stopped drawing a glyph for one, the two lines would have run together. The wrapping is unchanged; it just runs once per paragraph now. The rest of the compaction: - The tooltip itself is a three-line box at the width of the pane, the same one the text block uses, with its width and its hover delay sharing the line beneath it. It is a paragraph, not a caption. - A window's six switches are an icon row beside Title Height rather than a section of six captioned checkboxes. The section is gone. - Each easing sits beside the time it takes: hover on one line, press on the next. Grids take a minimum cell width now, so a section asks for half a pane and still reflows to one column when the frame is dragged narrow. Three fixes for things that were showing what they should not: - A drop-down's placeholder offered Wrap Text and Extend To Fit Text. A placeholder is drawn only while nothing is chosen, so a control that wraps or resizes to fit one changes shape when it is used. - A GuiInputCtrl drew its text block's caption and both icons on top of the Text section's own title, which read as "Textt". resizeToFit called resize(0, 0, ...), and resize sets the position as well as the extent, so every bind dragged the block up out of the panel it was sitting in. The header's copy never showed it because a chain repositions its children. - A menu bar offered a Position, an Extent and an Anchor picker, none of which it honours: GuiMenuBarCtrl::resize throws away the position it is given and onRender resizes the bar to its parent's width every frame. It does keep mBounds.extent.y, so Position and Anchor go and Extent stays, with its width greyed and its height live -- the bar's thickness is the one thing there is to set on it, and nothing else can set it. Two pieces of dead code went with them. onToggleChanged now takes the value the widget flipped itself to, which retires a six-case switch and is what made the window row cheap; and makeToggleBox with its onToggleClicked had had no callers since the icon row replaced captioned checkboxes -- after the signature change it would have written an empty value had anything reached it. Also: colour -> color throughout, and the tooltip and class rows had been captioning themselves with their field names next to a properly cased "Super Class". tests/smoke/toggleTip.cs covers the two-line format and screenshots a real hovered tip, since the rendering is the half that script cannot see. All 22 suites pass. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qi2pZ9UzF5rsLCNVErpzwt --- editor/EditorCore/EditorIconButton.cs | 2 +- .../GuiEditor/scripts/GuiEditorControlSpec.cs | 78 +++++++- .../GuiEditor/scripts/GuiEditorHeaderBlock.cs | 165 ++++++++++++----- .../scripts/GuiEditorInspectorPane.cs | 115 +++++++----- .../GuiEditor/scripts/GuiEditorTextBlock.cs | 42 +++-- .../GuiEditor/scripts/GuiEditorToggleIcon.cs | 29 ++- engine/source/gui/guiControl.cc | 166 +++++++++++------- tests/run.ps1 | 4 +- tests/shots/inspectorPane.cs | 25 ++- tests/smoke/inspectorPane.cs | 13 +- tests/smoke/inspectorSpec.cs | 9 +- tests/smoke/inspectorText.cs | 23 ++- tests/smoke/toggleTip.cs | 105 +++++++++++ tests/smoke/toggleTip.input.ps1 | 17 ++ 14 files changed, 590 insertions(+), 203 deletions(-) create mode 100644 tests/smoke/toggleTip.cs create mode 100644 tests/smoke/toggleTip.input.ps1 diff --git a/editor/EditorCore/EditorIconButton.cs b/editor/EditorCore/EditorIconButton.cs index 06d594407..0cebcac94 100644 --- a/editor/EditorCore/EditorIconButton.cs +++ b/editor/EditorCore/EditorIconButton.cs @@ -33,7 +33,7 @@ // 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 colour and the disabled look was gone until something else +// 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) { diff --git a/editor/GuiEditor/scripts/GuiEditorControlSpec.cs b/editor/GuiEditor/scripts/GuiEditorControlSpec.cs index 7f16f66f6..9d7464330 100644 --- a/editor/GuiEditor/scripts/GuiEditorControlSpec.cs +++ b/editor/GuiEditor/scripts/GuiEditorControlSpec.cs @@ -212,11 +212,9 @@ %this.addSection("GuiTabBookCtrl", "Tabs", "Tabs", "MinTabWidth"); - // The six window toggles are a second icon row rather than six checkboxes; - // the pane reads this key specially. Kept here so the field list stays in - // one place. - %this.addSection("GuiWindowCtrl", "Window", "Window", - "canMove canClose canMinimize canMaximize resizeWidth resizeHeight"); + // 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"); @@ -369,9 +367,11 @@ 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 easeFillColorSL easeTimeFillColorHL easeTimeFillColorSL"; + return "easeFillColorHL easeTimeFillColorHL easeFillColorSL easeTimeFillColorSL"; } // The toggles the header draws as icons instead of rows. Visible, Active and @@ -387,6 +387,14 @@ 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"; +} + // Never shown anywhere, for any class. function GuiEditorControlSpec::deadFields(%this) { @@ -444,6 +452,7 @@ // 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)) @@ -451,6 +460,16 @@ 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(); @@ -483,6 +502,19 @@ 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) { @@ -490,6 +522,11 @@ { return false; } + if(%mode $= "bar") + { + // Only the extent, and only half of it -- see liveExtentAxes. + return %field $= "Extent"; + } if(%mode $= "chainV") { return %field !$= "VertSizing"; @@ -507,12 +544,26 @@ 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. @@ -549,6 +600,15 @@ { 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; @@ -621,8 +681,8 @@ // 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 colour -// swatch is both fields, because picking a colour IS turning the override on. +// 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"; @@ -757,7 +817,9 @@ %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"; diff --git a/editor/GuiEditor/scripts/GuiEditorHeaderBlock.cs b/editor/GuiEditor/scripts/GuiEditorHeaderBlock.cs index 27e5a754f..71cd8ea5d 100644 --- a/editor/GuiEditor/scripts/GuiEditorHeaderBlock.cs +++ b/editor/GuiEditor/scripts/GuiEditorHeaderBlock.cs @@ -46,8 +46,10 @@ %this.buildText(); // The value grid starts empty; bindClass fills it, because what belongs - // there is the one thing here that changes with the class. - %this.valueGrid = %this.pane.makeCellGrid(0); + // 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); } @@ -99,12 +101,14 @@ // carries both ($EditorIcon::eye and ::invisible_light). Left alone here // because hidden is moving out to its own Explorer tree column, and that is // the change that should pick the icon. - %this.hiddenButton = %this.makeIconToggle(%row, 4, "hidden", + %this.hiddenButton = %this.makeIconToggle(%row, 4, "hidden", "Hidden", $EditorIcon::round_delete, $EditorIcon::round_checkmark, - "Hidden in the editor", "Shown in the editor"); - %this.lockedButton = %this.makeIconToggle(%row, 32, "locked", + "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.", + "Drawn on the canvas as it will be in the game."); + %this.lockedButton = %this.makeIconToggle(%row, 32, "locked", "Locked", $EditorIcon::padlock_closed, $EditorIcon::padlock_open, - "Locked against editing", "Unlocked"); + "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.", + "Can be picked and dragged on the canvas."); // 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 @@ -114,26 +118,28 @@ // 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. - %this.visibleButton = %this.makeIconToggle(%row, 68, "Visible", + %this.visibleButton = %this.makeIconToggle(%row, 68, "Visible", "Visible", $EditorIcon::eye, $EditorIcon::invisible_light, - "Draws when the game runs", "Does not draw when the game runs"); - %this.activeButton = %this.makeIconToggle(%row, 96, "Active", + "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, 96, "Active", "Active", $EditorIcon::on, $EditorIcon::off, - "Responds when the game runs", "Inert when the game runs"); - %this.inputButton = %this.makeIconToggle(%row, 124, "useInput", + "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, 124, "useInput", "Accepts Input", $EditorIcon::cursor_arrow, $EditorIcon::cursor_arrow, - "Touch and key events reach this control", - "Touch and key events pass straight through"); - %this.containerButton = %this.makeIconToggle(%row, 152, "isContainer", + "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, 152, "isContainer", "Accepts Children", $EditorIcon::folder_open, $EditorIcon::folder, - "The editor may drop controls into this one", - "The editor will not drop controls into this one"); + "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, %frameOn, %frameOff, %tipOn, %tipOff) +function GuiEditorHeaderBlock::makeIconToggle(%this, %row, %x, %field, %label, %frameOn, %frameOff, %tipOn, %tipOff) { %button = new GuiCheckBoxCtrl() { @@ -145,6 +151,7 @@ class = "GuiEditorToggleIcon"; tipOn = %tipOn; tipOff = %tipOff; toggleName = %field; + toggleLabel = %label; owner = %this; }; ThemeManager.setProfile(%button, "iconButtonProfile"); @@ -157,34 +164,77 @@ class = "GuiEditorToggleIcon"; // the new value rather than being asked to work it out. function GuiEditorHeaderBlock::onToggleIconChanged(%this, %toggle) { - %this.pane.onToggleChanged(%toggle.toggleName); + %this.pane.onToggleChanged(%toggle.toggleName, %toggle.getValue()); } -function GuiEditorHeaderBlock::makeToggleBox(%this, %row, %x, %w, %field, %label, %tip) +//----------------------------------------------------------------------------- +// 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) { - %box = new GuiCheckBoxCtrl() + %size = 24; + %gap = 2; + %fields = %this.spec.windowToggles(); + + %row = new GuiControl() { - Position = %x SPC 3; - Extent = %w SPC 22; - Text = %label; - boxOffset = "0 2"; - boxExtent = "18 18"; - textOffset = "22 2"; - textExtent = (%w - 22) SPC 18; - Tooltip = %tip; - Command = %this.getID() @ ".onToggleClicked(\"" @ %field @ "\");"; + Position = "0 0"; + Extent = (getWordCount(%fields) * (%size + %gap)) SPC (%size + 4); }; - ThemeManager.setProfile(%box, "checkboxProfile"); - ThemeManager.setProfile(%box, "tipProfile", "TooltipProfile"); - %row.add(%box); + 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)); + } - %box.fieldName = %field; - return %box; + %this.windowToggleRow = %row; } -function GuiEditorHeaderBlock::onToggleClicked(%this, %field) +// Load the six from the control, when the bound class has them. +function GuiEditorHeaderBlock::refreshWindowToggles(%this, %ctrl) { - %this.pane.onToggleChanged(%field); + 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)); + } } //----------------------------------------------------------------------------- @@ -234,23 +284,35 @@ class = "GuiEditorAnchorPicker"; %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")); - %this.extentRow.setEnabled(%spec.isGeometryFieldLive(%mode, "Extent"), %reason); - // Position is the one field whose two axes can disagree: a vertical chain - // stacks its children on Y and copies X straight back from the child, so - // half of this row is live. The row's own setEnabled works on both boxes at - // once, so the axes are set directly -- the same two widgets it would touch. - %axes = %spec.livePositionAxes(%mode); + // 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; - %this.positionRow.editor.setActive(%liveX); - %this.positionRow.editorY.setActive(%liveY); - %this.positionRow.editor.Tooltip = %liveX ? "" : %reason; - %this.positionRow.editorY.Tooltip = %liveY ? "" : %reason; + %row.editor.setActive(%liveX); + %row.editorY.setActive(%liveY); + %row.editor.Tooltip = %liveX ? "" : %reason; + %row.editorY.Tooltip = %liveY ? "" : %reason; } //----------------------------------------------------------------------------- @@ -258,7 +320,7 @@ class = "GuiEditorAnchorPicker"; //----------------------------------------------------------------------------- // 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 colour +// 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. // @@ -320,6 +382,7 @@ class = "GuiEditorAnchorPicker"; %this.pane.clearRows(%this.valueFields); %this.valueGrid.deleteObjects(); %this.valueFields = ""; + %this.windowToggleRow = ""; %fields = %this.spec.headerValueFields(%class); @@ -340,6 +403,14 @@ class = "GuiEditorAnchorPicker"; %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); } diff --git a/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs b/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs index 7d347fba1..f22ec03c0 100644 --- a/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs +++ b/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs @@ -67,6 +67,12 @@ class = "GuiEditorControlSpec"; // 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 = ""; @@ -123,8 +129,11 @@ class = "GuiEditorHeaderBlock"; // is art for them, so neither has a row here. %this.buildSection("Layout", "Layout", "MinExtent"); %this.buildSection("Command", "Command", "Command AltCommand Variable Accelerator"); - %this.buildSection("Animation", "Animation", %this.spec.easingFields()); - %this.buildSection("Tooltip", "Tooltip", %this.spec.tooltipFields()); + + // 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"); @@ -167,7 +176,12 @@ class = "GuiEditorDynamicFields"; // 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). -function GuiEditorInspectorPane::makeCellGrid(%this, %y) +// %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() { @@ -176,7 +190,7 @@ class = "GuiEditorDynamicFields"; Extent = %this.paneWidth SPC 4; CellModeX = "variable"; CellModeY = "variable"; - CellSizeX = %this.rowWidth; + CellSizeX = (%cellW $= "") ? %this.rowWidth : %cellW; CellSizeY = 48; CellSpacingX = 4; CellSpacingY = 4; @@ -235,6 +249,39 @@ class = "GuiEditorDynamicFields"; } } +// 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", ""); +} + function GuiEditorInspectorPane::makeTextBlock(%this, %parent, %y) { %block = new GuiChainCtrl() @@ -254,12 +301,12 @@ class = "GuiEditorTextBlock"; return %block; } -function GuiEditorInspectorPane::buildSection(%this, %key, %title, %fields) +function GuiEditorInspectorPane::buildSection(%this, %key, %title, %fields, %cellW) { %panel = %this.makeSectionPanel(%title); %this.add(%panel); - %grid = %this.makeCellGrid(24); + %grid = %this.makeCellGrid(24, %cellW); %panel.add(%grid); %this.panel[%key] = %panel; @@ -301,7 +348,7 @@ class = "GuiProfileEditorFieldRow"; // 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 colour row, where the button + // 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; @@ -853,7 +900,7 @@ class = "GuiProfileEditorFieldRow"; // never registered. // // fontColor is skipped for a different reason: what its swatch shows is - // the profile's colour while the control is not overriding it, which 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") { @@ -863,7 +910,7 @@ class = "GuiProfileEditorFieldRow"; } // What the text block holds beyond its three field rows: two segmented rows, - // two toggles, and the font colour swatch. + // two toggles, and the font color swatch. %block = %this.activeTextBlock(); if(isObject(%block)) { @@ -1031,6 +1078,8 @@ class = "GuiProfileEditorFieldRow"; %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: @@ -1046,35 +1095,17 @@ class = "GuiProfileEditorFieldRow"; %this.afterCommit(); } -// A toggle reports the click and lets the pane decide what the value becomes, -// so that every write to the control still goes through one place. -function GuiEditorInspectorPane::onToggleChanged(%this, %field) +// 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; } - // Every widget in the row holds its own state and has already flipped it, so - // the value is read back rather than derived -- the control and the widget - // can never disagree about what was just asked for. - %header = %this.header; - switch$(%field) - { - case "hidden": - %this.writeField("hidden", %header.hiddenButton.getValue()); - case "locked": - %this.writeField("locked", %header.lockedButton.getValue()); - case "Visible": - %this.writeField("Visible", %header.visibleButton.getValue()); - case "Active": - %this.writeField("Active", %header.activeButton.getValue()); - case "useInput": - %this.writeField("useInput", %header.inputButton.getValue()); - case "isContainer": - %this.writeField("isContainer", %header.containerButton.getValue()); - } - + %this.writeField(%field, %value); %this.refreshToggles(); %this.afterCommit(); } @@ -1208,7 +1239,7 @@ class = "GuiProfileEditorFieldRow"; } // Everything downstream of the profile moves with it -- which profiles the - // slot now offers, and the font colour the swatch falls back to. + // slot now offers, and the font color the swatch falls back to. %this.refresh(); %this.afterCommit(); } @@ -1330,8 +1361,8 @@ class = "GuiProfileEditorFieldRow"; %field = %row.fieldName; - // The font colour swatch is two fields, and the second of them is why the - // changed test cannot come first: picking the colour the profile already + // 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") { @@ -1467,19 +1498,19 @@ class = "GuiProfileEditorFieldRow"; } //----------------------------------------------------------------------------- -// Font colour, which is two fields wearing one widget. overrideFontColor is +// 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 colour turns the override on, and the row's reset button -// turns it off again. With it off the swatch shows the profile's own colour, +// 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 colour is unchanged AND the override was - // already on. Picking the profile's exact colour while it was off is the - // user asking to pin that colour down, which is a change to the control + // 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) { @@ -1496,7 +1527,7 @@ class = "GuiProfileEditorFieldRow"; // 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 colour row keeps it, meaning "stop overriding the +// the button. The font color row keeps it, meaning "stop overriding the // profile's". function GuiEditorInspectorPane::onProfileRowReset(%this, %row) { diff --git a/editor/GuiEditor/scripts/GuiEditorTextBlock.cs b/editor/GuiEditor/scripts/GuiEditorTextBlock.cs index c8b0a1c68..fe1237bfa 100644 --- a/editor/GuiEditor/scripts/GuiEditorTextBlock.cs +++ b/editor/GuiEditor/scripts/GuiEditorTextBlock.cs @@ -20,7 +20,7 @@ // 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 colour, the pair anyone reaching for "this text is +// 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 @@ -90,17 +90,17 @@ %row.add(%this.label); %this.wrapButton = %this.makeIcon(%row, %w - ((%size + 2) * 2), %size, "textWrap", - $EditorIcon::align_just, - "Wraps onto as many lines as it needs", - "Draws on one line whatever its width"); + "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", - $EditorIcon::expand, "", ""); + "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, %frame, %tipOn, %tipOff) +function GuiEditorTextBlock::makeIcon(%this, %row, %x, %size, %field, %label, %frame, %tipOn, %tipOff) { %button = new GuiCheckBoxCtrl() { @@ -113,6 +113,7 @@ class = "GuiEditorToggleIcon"; tipOn = %tipOn; tipOff = %tipOff; toggleName = %field; + toggleLabel = %label; owner = %this; }; ThemeManager.setProfile(%button, "iconButtonProfile"); @@ -127,8 +128,9 @@ class = "GuiEditorToggleIcon"; // when not). function GuiEditorTextBlock::applyExtendTip(%this, %wrapping) { - %tip = %wrapping ? "Grows taller to fit the wrapped text" - : "Grows wider to fit the text"; + %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; @@ -249,7 +251,7 @@ class = "GuiEditorChoiceRow"; } //----------------------------------------------------------------------------- -// Size and colour. +// Size and color. //----------------------------------------------------------------------------- function GuiEditorTextBlock::buildFont(%this) @@ -266,10 +268,10 @@ class = "GuiEditorChoiceRow"; "Font Size", %this.pane.sharedKindFor("fontSizeAdjust"), ""); %this.fontSizeRow = %this.row["fontSizeAdjust"]; - // One widget for two fields. overrideFontColor has no row: picking a colour + // 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 colour, so the row always says what the control will draw in. + // 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"]; @@ -352,12 +354,22 @@ class = "GuiEditorChoiceRow"; // 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(0, 0, %w + 1, %h); - %this.resize(0, 0, %w, %h); + + %this.resize(%x, %y, %w + 1, %h); + %this.resize(%x, %y, %w, %h); } //----------------------------------------------------------------------------- @@ -389,9 +401,9 @@ class = "GuiEditorChoiceRow"; %this.populating = false; } -// The swatch shows what the control will actually draw in: its own colour while +// 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 colour the control does not use. +// the row claim a color the control does not use. function GuiEditorTextBlock::loadFontColor(%this, %ctrl) { %override = %ctrl.overrideFontColor; diff --git a/editor/GuiEditor/scripts/GuiEditorToggleIcon.cs b/editor/GuiEditor/scripts/GuiEditorToggleIcon.cs index c3b411e29..8cdd41a26 100644 --- a/editor/GuiEditor/scripts/GuiEditorToggleIcon.cs +++ b/editor/GuiEditor/scripts/GuiEditorToggleIcon.cs @@ -19,7 +19,7 @@ // // 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 colour as NormalState, so the four "On" +// 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). @@ -27,7 +27,7 @@ // 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 colour. Here the state is +// 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 @@ -95,7 +95,7 @@ } // The single place the icon's look is decided: which frame, which tooltip, and -// which of the profile's font colours tints it. +// which of the profile's font colors tints it. function GuiEditorToggleIcon::refresh(%this) { %on = %this.getStateOn(); @@ -123,11 +123,30 @@ %this.icon.setImageColor(%on ? %profile.fontColorHL : %profile.fontColor); } + %this.Tooltip = %this.buildTip(%on); +} + +// 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 GuiEditorToggleIcon::buildTip(%this, %on) +{ %tip = %on ? %this.tipOn : %this.tipOff; - if(%tip !$= "") + + if(%this.toggleLabel $= "") { - %this.Tooltip = %tip; + 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. diff --git a/engine/source/gui/guiControl.cc b/engine/source/gui/guiControl.cc index 834b22b1c..1ebf02a48 100755 --- a/engine/source/gui/guiControl.cc +++ b/engine/source/gui/guiControl.cc @@ -910,6 +910,90 @@ GuiControlProfile* GuiControl::resolveDefaultTooltipProfile() return dynamic_cast(Sim::findObject("GuiDefaultProfile")); } +// Wrap one paragraph of tooltip text to maxWidth, appending to lines and +// returning the new line count. This is the word wrapping renderTooltip has +// always done, lifted out so it can run once per paragraph -- a tooltip splits +// on line breaks first now, which is what lets one carry a heading and an +// explanation of what it means on separate lines. +// +// Not GuiControl::getLineList: that wraps only when the CONTROL wraps, and a +// tooltip has to wrap whatever the control it belongs to does. +static S32 wrapTooltipParagraph(const char* paragraph, GFont* font, const S32 maxWidth, + const S32 spaceWidth, FrameTemp& lines, S32 lineCount, + const S32 maxLines, S32& widestLine) +{ + const S32 wordCount = StringUnit::getUnitCount( paragraph, " " ); + S32 lineWidth = 0; + S32 wordStartIndex = 0; + S32 wordEndIndex = 0; + + while( true ) + { + // Do we have any words left? + if ( wordEndIndex < wordCount ) + { + // Yes, so fetch the word. + const char* pWord = StringUnit::getUnit( paragraph, wordEndIndex, " " ); + + // Add word length. + const S32 wordLength = (S32)font->getStrWidth( pWord ) + spaceWidth; + + // Do we still have room? + if ( (lineWidth + wordLength) < maxWidth ) + { + // Yes, so add word length. + lineWidth += wordLength; + + // Next word. + wordEndIndex++; + + continue; + } + + // Do we have any lines left? + if ( lineCount < maxLines ) + { + // Yes, so insert line. + lines[lineCount++] = StringUnit::getUnits( paragraph, wordStartIndex, wordEndIndex-1, " " ); + + // Update horizontal text bounds. + if ( lineWidth > widestLine ) + widestLine = lineWidth; + } + + // Set new line length. + lineWidth = wordLength; + + // Set word start. + wordStartIndex = wordEndIndex; + + // Next word. + wordEndIndex++; + + continue; + } + + // Do we have any words left? + if ( wordStartIndex < wordCount ) + { + // Yes, so do we have any lines left? + if ( lineCount < maxLines ) + { + // Yes, so insert line. + lines[lineCount++] = StringUnit::getUnits( paragraph, wordStartIndex, wordCount-1, " " ); + + // Update horizontal text bounds. + if ( lineWidth > widestLine ) + widestLine = lineWidth; + } + } + + break; + } + + return lineCount; +} + bool GuiControl::renderTooltip(Point2I &cursorPos, const char* tipText ) { #if !defined(TORQUE_OS_IOS) && !defined(TORQUE_OS_ANDROID) && !defined(TORQUE_OS_EMSCRIPTEN) @@ -976,83 +1060,31 @@ bool GuiControl::renderTooltip(Point2I &cursorPos, const char* tipText ) // Fetch the maximum allowed tooltip extent. const S32 maxTooltipWidth = mTooltipWidth; - // Fetch word count. - const S32 wordCount = StringUnit::getUnitCount( renderTip, " " ); - // Reset line storage. const S32 tooltipLineStride = (S32)font->getHeight() + 4; const S32 maxTooltipLines = 20; S32 tooltipLineCount = 0; - S32 tooltipLineWidth = 0; FrameTemp tooltipLines( maxTooltipLines ); - // Reset word indexing. - S32 wordStartIndex = 0; - S32 wordEndIndex = 0; - - // Search for end word. - while( true ) + // Paragraph by paragraph, wrapping each to the tooltip width. Breaking on + // newlines first is what lets a tip say what a thing is on one line and + // what it does on the next. + const string tip(renderTip); + string::size_type paragraphStart = 0; + while ( paragraphStart <= tip.length() ) { - // Do we have any words left? - if ( wordEndIndex < wordCount ) - { - // Yes, so fetch the word. - const char* pWord = StringUnit::getUnit( renderTip, wordEndIndex, " " ); - - // Add word length. - const S32 wordLength = (S32)font->getStrWidth( pWord ) + spaceWidth; - - // Do we still have room? - if ( (tooltipLineWidth + wordLength) < maxTooltipWidth ) - { - // Yes, so add word length. - tooltipLineWidth += wordLength; - - // Next word. - wordEndIndex++; - - continue; - } - - // Do we have any lines left? - if ( tooltipLineCount < maxTooltipLines ) - { - // Yes, so insert line. - tooltipLines[tooltipLineCount++] = StringUnit::getUnits( renderTip, wordStartIndex, wordEndIndex-1, " " ); - - // Update horizontal text bounds. - if ( tooltipLineWidth > textBounds.x ) - textBounds.x = tooltipLineWidth; - } - - // Set new line length. - tooltipLineWidth = wordLength; + const string::size_type breakAt = tip.find('\n', paragraphStart); + const string paragraph = (breakAt == string::npos) + ? tip.substr(paragraphStart) + : tip.substr(paragraphStart, breakAt - paragraphStart); - // Set word start. - wordStartIndex = wordEndIndex; + tooltipLineCount = wrapTooltipParagraph( paragraph.c_str(), font, maxTooltipWidth, + spaceWidth, tooltipLines, tooltipLineCount, maxTooltipLines, textBounds.x ); - // Next word. - wordEndIndex++; + if ( breakAt == string::npos ) + break; - continue; - } - - // Do we have any words left? - if ( wordStartIndex < wordCount ) - { - // Yes, so do we have any lines left? - if ( tooltipLineCount < maxTooltipLines ) - { - // Yes, so insert line. - tooltipLines[tooltipLineCount++] = StringUnit::getUnits( renderTip, wordStartIndex, wordCount-1, " " ); - - // Update horizontal text bounds. - if ( tooltipLineWidth > textBounds.x ) - textBounds.x = tooltipLineWidth; - } - } - - break; + paragraphStart = breakAt + 1; } // Controls the size of the inside (gutter) tooltip region. diff --git a/tests/run.ps1 b/tests/run.ps1 index 9e58f5fcd..cdd86a2f5 100644 --- a/tests/run.ps1 +++ b/tests/run.ps1 @@ -184,8 +184,8 @@ foreach ($test in $tests) { } $summary = if ($Shots) { "$wrote shot$(if ($wrote -ne 1) { 's' })" } else { "$pass passed" } - $colour = if ($ok) { 'Green' } else { 'Red' } - Write-Host ("{0,-14} {1}" -f $summary, $note) -ForegroundColor $colour + $color = if ($ok) { 'Green' } else { 'Red' } + Write-Host ("{0,-14} {1}" -f $summary, $note) -ForegroundColor $color if (-not $ok) { $lines | Select-String 'FAIL:' | Select-Object -First 6 | ForEach-Object { diff --git a/tests/shots/inspectorPane.cs b/tests/shots/inspectorPane.cs index a243351f4..56bcc5bc2 100644 --- a/tests/shots/inspectorPane.cs +++ b/tests/shots/inspectorPane.cs @@ -65,12 +65,21 @@ function sStep1() createPath(testRoot("shots/")); - // case TAB object global + // An inherited-role control, whose text block lives in the Text section + // rather than the header, and the one control that owns none of its own + // position. + $sInput = sPlace("GuiInputCtrl"); + $sMenuBar = sPlace("GuiMenuBarCtrl"); + + // case TAB object global [TAB "bottom" to scroll down first] $sCases = "pane-button" TAB "$sButton" NL + "pane-sections" TAB "$sButton" TAB "bottom" NL "pane-window" TAB "$sWindow" NL "pane-tabpage" TAB "$sPage" NL - "pane-chainkid" TAB "$sChainKid"; + "pane-chainkid" TAB "$sChainKid" NL + "pane-input" TAB "$sInput" NL + "pane-menubar" TAB "$sMenuBar"; $sIndex = 0; schedule(1000, 0, "sShoot"); } @@ -109,6 +118,18 @@ function sShoot() } %pane.dynamicPanel.setExpanded(true); + // The pane is taller than its frame, so the sections that come after the + // header are only visible from the bottom of the scroller. + %scroller = GuiEditor.inspectorWindow.scroller; + if(getField(%rec, 2) $= "bottom") + { + %scroller.scrollToBottom(); + } + else + { + %scroller.scrollToTop(); + } + // Let the chain, the grids and the panels settle before grabbing. schedule(500, 0, "sGrab", %name); } diff --git a/tests/smoke/inspectorPane.cs b/tests/smoke/inspectorPane.cs index b0807b4c4..41c290df7 100644 --- a/tests/smoke/inspectorPane.cs +++ b/tests/smoke/inspectorPane.cs @@ -215,7 +215,12 @@ function pStep4() { $pWindow = pAdd("GuiWindowCtrl"); %pane = pBind($pWindow); - pCheck("window has its Window section", pRowBuilt(%pane, "canClose")); + // The six switches are icons in the header's value block now, sharing a line + // with Title Height rather than costing a section of six checkboxes. + pCheck("window has no Window section", !pRowBuilt(%pane, "canClose")); + pCheck("its switches are in the header", + isObject(%pane.header.windowToggleRow) && + isObject(%pane.header.windowButton["canClose"])); pCheck("window has its Grips section", pRowBuilt(%pane, "resizeRightWidth")); pCheck("window keeps its title text", %pane.header.textGrid.isVisible()); pCheck("window promotes titleHeight", pRowBuilt(%pane, "titleHeight")); @@ -223,7 +228,8 @@ function pStep4() // Moving to a slider must take the window's fields away again. $pSlider = pAdd("GuiSliderCtrl"); %pane = pBind($pSlider); - pCheck("window fields gone after reselect", !pRowBuilt(%pane, "canClose")); + pCheck("window fields gone after reselect", !pRowBuilt(%pane, "resizeRightWidth")); + pCheck("and its switch row went with them", !isObject(%pane.header.windowToggleRow)); pCheck("slider has its ticks", pRowBuilt(%pane, "ticks")); pCheck("slider promotes range", pRowBuilt(%pane, "range")); pCheck("slider hides the header text block", !%pane.header.textGrid.isVisible()); @@ -232,7 +238,8 @@ function pStep4() // And back again, to prove the rebuild is not one-way. %pane = pBind($pWindow); - pCheck("window fields returned", pRowBuilt(%pane, "canClose")); + pCheck("window fields returned", pRowBuilt(%pane, "resizeRightWidth")); + pCheck("and so did its switch row", isObject(%pane.header.windowToggleRow)); pCheck("slider fields gone", !pRowBuilt(%pane, "ticks")); schedule(200, 0, "pStep5"); diff --git a/tests/smoke/inspectorSpec.cs b/tests/smoke/inspectorSpec.cs index fb6b6c4fb..ed4528140 100644 --- a/tests/smoke/inspectorSpec.cs +++ b/tests/smoke/inspectorSpec.cs @@ -322,9 +322,14 @@ function sStep5() sCheck("tree view has both its sections", %s.listHas(%s.sectionKeys("GuiTreeViewCtrl"), "List") && %s.listHas(%s.sectionKeys("GuiTreeViewCtrl"), "Tree")); - sCheck("window has its two sections", - %s.listHas(%s.sectionKeys("GuiWindowCtrl"), "Window") && + // The window's six switches are an icon row in the header beside Title + // Height, not a section of six checkboxes, so Grips is all it has left. + sCheck("window has its Grips section", %s.listHas(%s.sectionKeys("GuiWindowCtrl"), "Grips")); + sCheck("and no Window section", !%s.listHas(%s.sectionKeys("GuiWindowCtrl"), "Window")); + sCheck("its switches are named for the header instead", + getWordCount(%s.windowToggles()) == 6 && + %s.listHas(%s.windowToggles(), "canClose")); sCheck("plain control has no sections", %s.sectionKeys("GuiControl") $= ""); sCheck("window section titled", %s.sectionTitle("GuiWindowCtrl", "Grips") $= "Resize Grips"); diff --git a/tests/smoke/inspectorText.cs b/tests/smoke/inspectorText.cs index 5b9804f5e..edea5de69 100644 --- a/tests/smoke/inspectorText.cs +++ b/tests/smoke/inspectorText.cs @@ -181,7 +181,7 @@ function tStep4() %pane = tBind($tHeading); tCheck("font size is reachable", tRowShown(%pane, "fontSizeAdjust")); - tCheck("font colour is reachable", tRowShown(%pane, "fontColor")); + tCheck("font color is reachable", tRowShown(%pane, "fontColor")); tCheck("the text box is reachable", tRowShown(%pane, "text")); %block = %pane.activeTextBlock(); @@ -290,13 +290,18 @@ function tStep5() %block.extendButton.performClick(); tCheck("extend reached the control", $tHeading.textExtend); - tCheck("its tooltip says which way it grows", - %block.extendButton.Tooltip $= "Grows wider to fit the text"); + // The tip is two lines: what the switch is and how it is set, then what that + // means. The second line is the one that has to follow the wrap state, since + // extend grows a different axis depending on it. + tCheck("its tooltip names the switch and its state", + getRecord(%block.extendButton.Tooltip, 0) $= "Extend To Fit Text - On"); + tCheck("and says which way it grows", + strstr(getRecord(%block.extendButton.Tooltip, 1), "wider") >= 0); %block.wrapButton.performClick(); tCheck("wrap reached the control", $tHeading.textWrap); tCheck("and the tooltip changed with it", - %block.extendButton.Tooltip $= "Grows taller to fit the wrapped text"); + strstr(getRecord(%block.extendButton.Tooltip, 1), "taller") >= 0); // Off again, and loaded back the same way on a rebind. %block.wrapButton.performClick(); @@ -312,7 +317,7 @@ function tStep5() } //----------------------------------------------------------------------------- -// Font colour, which is two fields wearing one swatch. +// Font color, which is two fields wearing one swatch. //----------------------------------------------------------------------------- function tStep6() @@ -321,20 +326,20 @@ function tStep6() %row = %pane.row["fontColor"]; tCheck("nothing is overridden to begin with", !$tHeading.overrideFontColor); - tCheck("so the swatch shows the profile's colour", + tCheck("so the swatch shows the profile's color", %row.getValue() $= tProfileOf($tHeading).fontColor); tCheck("and there is nothing to revert", !%row.resetButton.isVisible()); %row.editor.setColorI("10 20 30 255"); %row.commit(); - tCheck("picking a colour wrote it", $tHeading.fontColor $= "10 20 30 255"); + tCheck("picking a color wrote it", $tHeading.fontColor $= "10 20 30 255"); tCheck("and turned the override on", $tHeading.overrideFontColor); tCheck("the revert appeared", %row.resetButton.isVisible()); - // The revert is the only way back to the profile's colour. + // The revert is the only way back to the profile's color. %pane.onProfileRowReset(%row); tCheck("revert turned the override off", !$tHeading.overrideFontColor); - tCheck("the swatch fell back to the profile's colour", + tCheck("the swatch fell back to the profile's color", %row.getValue() $= tProfileOf($tHeading).fontColor); tCheck("and the revert went away", !%row.resetButton.isVisible()); diff --git a/tests/smoke/toggleTip.cs b/tests/smoke/toggleTip.cs new file mode 100644 index 000000000..525567e97 --- /dev/null +++ b/tests/smoke/toggleTip.cs @@ -0,0 +1,105 @@ +//----------------------------------------------------------------------------- +// Two-line tooltips on the properties pane's toggle icons. Driven by +// toggleTip.input.ps1, which hovers a real mouse over one of them long enough +// for the tip to appear, so what is screenshotted is what a user would see. +// +// A toggle says what it is and which way it is set on the first line, and what +// that means on the second: +// +// Visible - On +// Draws when the game runs... +// +// Which needed the tooltip renderer taught about line breaks: it does its own +// word wrapping, splitting on spaces alone, so a newline used to be part of a +// word -- and once GFont stopped drawing a glyph for it, the two lines would +// have run together into one. +//----------------------------------------------------------------------------- + +setLogMode(2); +setScriptExecEcho(false); +trace(false); +$Scripts::ignoreDSOs = true; +setCompanyAndProduct("Torque Game Engines", "Torque2D"); +ModuleDatabase.EchoInfo = false; +AssetDatabase.EchoInfo = false; + +testExec("editor/main.cs"); + +function ttCheck(%label, %condition) +{ + if(%condition) echo("TTSMOKE PASS: " @ %label); + else echo("TTSMOKE FAIL: " @ %label); +} + +schedule(2500, 0, "ttOpenProject"); + +// A tip has to be hovered to be seen, so unlike the other pane suites this one +// needs the editor actually on screen: a real project, loaded the way the +// selector loads it, and then the editor brought up the way ctrl+~ does. +function ttOpenProject() +{ + ProjectManager.setProjectFolder("PlanetX"); + EditorCore.projectSelector.onProjectSelected(pathConcat(getMainDotCsDir(), "PlanetX")); + schedule(2500, 0, "ttOpenEditor"); +} + +function ttOpenEditor() +{ + EditorCore.toggleEditor(); + EditorCore.tabBook.selectPage(3); + schedule(1500, 0, "ttStep1"); +} + +function ttStep1() +{ + $ttTheme = GuiEditor.themeLibrary.createTheme("TTSmoke"); + GuiEditor.themeName = $ttTheme.getName(); + + $ttCtrl = new GuiControl() + { + Position = "40 40"; + Extent = "200 60"; + }; + GuiEditor.rootGui.add($ttCtrl); + GuiEditor.themeApplier.applyToBranch($ttCtrl, $ttTheme, true); + + %pane = GuiEditor.inspectorWindow.pane; + %pane.bind($ttCtrl); + + // --- The format, which needs no mouse. --- + %visible = %pane.header.visibleButton; + %tip = %visible.Tooltip; + echo("TTSMOKE: visible tip = [" @ %tip @ "]"); + + ttCheck("the tip leads with the name and the state", + getRecord(%tip, 0) $= "Visible - On"); + ttCheck("and explains that state underneath", + strstr(getRecord(%tip, 1), "Draws when the game runs") >= 0); + ttCheck("it is two lines, not one", getRecordCount(%tip) == 2); + + // Turning it off rewrites both lines. + %visible.setValue(false); + echo("TTSMOKE: off tip = [" @ %visible.Tooltip @ "]"); + ttCheck("off says off", getRecord(%visible.Tooltip, 0) $= "Visible - Off"); + ttCheck("with the other explanation", + strstr(getRecord(%visible.Tooltip, 1), "does not draw") >= 0 || + strstr(getRecord(%visible.Tooltip, 1), "Does not draw") >= 0); + %visible.setValue(true); + + // A segmented row's buttons are choices rather than switches, so they keep + // the one line they had -- "Centre text - On" would be a worse caption. + %align = %pane.header.alignRow; + ttCheck("a choice button has no On/Off heading", + getRecordCount(%align.choiceButton[2].Tooltip) == 1); + + // The driver hovers the Visible toggle during this window; the tip is + // screenshotted so the two lines can be seen to actually render. + schedule(6000, 0, "ttStep2"); +} + +function ttStep2() +{ + screenShot(testRoot("shots/toggleTip.png"), "PNG"); + echo("TTSMOKE DONE"); + schedule(400, 0, "quit"); +} diff --git a/tests/smoke/toggleTip.input.ps1 b/tests/smoke/toggleTip.input.ps1 new file mode 100644 index 000000000..969ab583f --- /dev/null +++ b/tests/smoke/toggleTip.input.ps1 @@ -0,0 +1,17 @@ +# Input for toggleTip.cs. A tooltip only draws while the pointer has been still +# over a control for longer than its hover time, so this parks the mouse on the +# Visible toggle and leaves it there while the shot is taken. +param([IntPtr]$Hwnd) + +. "$PSScriptRoot\..\lib\input.ps1" + +# The project loads at t=2.5s, the editor comes up at t=5s and the pane is bound +# at t=6.5s. The shot is taken at t=12.5s, so hover well inside that. +Start-Sleep -Seconds 8 + +# The state toggles sit in one row in the header: hidden, locked, then Visible, +# Active, Accepts Input and Accepts Children at 28px intervals. The row is at +# y~364 for a bare GuiControl, which is the one class that also carries a +# Category row above it -- 52px lower than it sits for anything else. +Send-EngineMouseMove -Hwnd $Hwnd -X 83 -Y 364 +Write-Host " hovering the Visible toggle at (83,364)" From b03115b6130e346420d1e3e20c59571a1fd24bdb Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Wed, 29 Jul 2026 12:08:06 -0400 Subject: [PATCH 10/55] Gui Editor: draw the controls, so the palette can stop naming them The control palette is a list of class names, which tells a person nothing about what any of them looks like -- GuiTextEditSliderCtrl sits above GuiTreeViewCtrl and below GuiTextEditCtrl, and none of the three is any the wiser for it. This is the art and the pipeline for a visual palette; the palette itself is the next piece. Thirty-one icons, each a miniature of the control rather than a metaphor, so a slider looks like a slider and nothing has to be learned. The one exception is GuiInputCtrl, which has no appearance to miniaturise. A bare GuiControl gets four of them. In 4.0 it is the wrapper, the backdrop, the line of text and the modal scrim, and the properties pane already asks which; giving each a tile lets it be answered by picking rather than by correcting afterwards. That is also why the table is keyed by palette entry instead of by class -- four entries share GuiControl and differ by category. Frame 0 is the fallback, and deliberately not the last cell: frameFor answers 0 for a key it has never heard of, and an unset Frame field reads as 0 too, so both failures land on a legible question mark rather than on an arbitrary wrong picture. A control class added to the engine after this was generated is meant to reach the palette anyway and simply wear it -- which is why the build script reports undrawn classes instead of asserting on them. The drawing rules are measured off the Mono set rather than guessed at: the ink ramp runs 255 at the top of a tile to 218 at the bottom, corners are one shared radius, negative space is knocked out of the fill instead of stroked, and the art bleeds to the tile edge. White only, because EditorIconButton tints the sprite from the theme's font colour -- the art is a mask, and the ramp survives underneath it as shading. Everything is placed in a 64-unit square with primary geometry on multiples of four, which is a whole pixel at 16, 32, 64 and 128 alike; that is what would make a 16px sheet for the Explorer tree a rerun rather than a redraw. Two silent failures had to be cleared. GuiEditor's module.taml carried no DeclaredAssets block at all -- it was the one editor module shipping no assets -- so without it the sheets would have sat on disk unregistered, with no error anywhere. And GuiEditor::create execs every script by name, so the generated file needed a line there or it would never have run. The shot harness exists mostly for the first of those. A missing image renders as nothing rather than throwing, so the way that failure shows up is a page of labels over blank space, and nothing else would have caught it. Sources and build script live beside the Mono set, outside the repo. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FVXsRigjXMEjMrbyHYawJD --- .../2026-07-29-gui-control-icon-set-design.md | 170 ++++++++++++++++++ editor/GuiEditor/GuiEditor.cs | 14 ++ .../images/controlIcons128.asset.taml | 8 + editor/GuiEditor/images/controlIcons128.png | Bin 0 -> 45502 bytes .../images/controlIcons64.asset.taml | 8 + editor/GuiEditor/images/controlIcons64.png | Bin 0 -> 22024 bytes editor/GuiEditor/module.taml | 7 +- .../scripts/GuiEditorControlIcons.cs | 133 ++++++++++++++ tests/shots/controlIcons.cs | 117 ++++++++++++ 9 files changed, 456 insertions(+), 1 deletion(-) create mode 100644 docs/superpowers/specs/2026-07-29-gui-control-icon-set-design.md create mode 100644 editor/GuiEditor/images/controlIcons128.asset.taml create mode 100644 editor/GuiEditor/images/controlIcons128.png create mode 100644 editor/GuiEditor/images/controlIcons64.asset.taml create mode 100644 editor/GuiEditor/images/controlIcons64.png create mode 100644 editor/GuiEditor/scripts/GuiEditorControlIcons.cs create mode 100644 tests/shots/controlIcons.cs diff --git a/docs/superpowers/specs/2026-07-29-gui-control-icon-set-design.md b/docs/superpowers/specs/2026-07-29-gui-control-icon-set-design.md new file mode 100644 index 000000000..91c4c1887 --- /dev/null +++ b/docs/superpowers/specs/2026-07-29-gui-control-icon-set-design.md @@ -0,0 +1,170 @@ +# Gui Control Icon Set — Design + +Date: 2026-07-29 +Branch: `gui-editor-improvements` +Status: approved (Peter, 2026-07-29) + +## Context + +The Gui Editor's control palette (`editor/GuiEditor/scripts/GuiEditorControlListWindow.cs`) is a plain-text `GuiListBoxCtrl` fed by `enumerateConsoleClasses("GuiControl")` minus the plumbing — `GuiTextEditSliderCtrl` above `GuiTreeViewCtrl` and below `GuiTextEditCtrl`, telling a person nothing about what any of them looks like. The intent is a visual palette: a grid of 128px tiles, or rows of 64px tiles with names, user's choice. + +This spec covers **the art and its pipeline**. The palette rewrite that consumes it is a separate follow-on, so the art can be reviewed on its own before UI work depends on it. + +The set is modelled on the purchased **Mono Icon Set** (`C:\Users\Peter\Documents\Circuit Hive\T2D Rocket Edition\Mono Icon Set`), which is what every editor icon already uses. New art lives in a sibling folder so the bought art stays untouched — the separation `Mono Icon Set/derived/` already keeps. + +## Decisions (Peter, 2026-07-29) + +1. **Two palette modes** — a grid of 128px tiles (icon only), or rows of 64px tiles with names. In grid mode the icon carries the whole load. +2. **Drawing language: miniature of the control**, not metaphor. A slider looks like a slider. Metaphor only where a control has no appearance (`GuiInputCtrl`). +3. **Scope: palette only (128 + 64)**, but every drawing constrained so the Explorer tree could adopt it later without a redraw. +4. **`GuiControl` gets four tiles**, one per profile category — Empty, Panel, Label, Overlay — so the commonest control in the editor drops pre-categorised instead of guessed at. +5. **Output lands in `editor/GuiEditor/`**, the only consumer. +6. **A generated entry table**, not `$ControlIcon::` constants, because an entry key is not always a class name. +7. **A fallback icon, not a build break**, for a class with no icon defined. + +## Design + +### The drawing language + +Measured off the Mono art rather than guessed at (`app_window_icon&48.png` studied at 4×): + +- Solid white, negative space knocked **out** of the fill rather than stroked. A frame is a filled rect with a smaller rect removed. +- Ink ramp `255` at the top of the tile falling linearly to `218` at the bottom — the same ramp `derive_valign.py` fits by least squares (≈ −0.84/row at 48px). Held as endpoints so it is size-independent, and applied from one function so 31 icons cannot drift apart tonally. +- Corner radius **6 units**, one constant, for the same reason. +- Art **bleeds to the tile edge**; Mono's padding is near zero. Breathing room is the tile control's job. +- **White only.** `EditorIconButton` (`editor/EditorCore/EditorIconButton.cs`) tints its `GuiSpriteCtrl` with `ImageColor` from the theme font colour and fades that on hover, press and disable. The art is a tint mask; the ramp survives underneath as shading. + +### The unit grid + +Every icon is placed in a **64-unit square**. Primary structure — frames, bars, dividers, major splits — snaps to multiples of **4 units**, which is a whole pixel at 16, 32, 64 and 128 alike. Secondary detail may use multiples of 2. + +The honest limit: the rule keeps structure *aligned* at 16px, it does not promise *legibility* there. `GuiTreeViewCtrl`, `GuiMenuBarCtrl`, `GuiFrameSetCtrl` and `SceneWindow` carry detail that will merge at 16 and would want simplified variants if the tree ever adopts the set. + +### Rendering + +Each size is drawn at **8× supersample** and downsampled with Lanczos. Corners get real antialiasing; straight edges cost nothing, because primary geometry lands on exact pixel boundaries at every size. + +### The 31 icons + +Frame index is display order in an 8×4 sheet — 31 used, 1 spare. Grouped by what a person is looking for, not alphabetically. + +| # | key | icon | +|---|---|---| +| 0 | `unknown` | Rounded frame with a **?** — the fallback | +| 1 | `GuiControl:Empty` | Four corner brackets — bounds, nothing painted | +| 2 | `GuiControl:Panel` | Border band, inset cut, filled space inside | +| 3 | `GuiControl:Label` | Two unframed text bars, ragged right | +| 4 | `GuiControl:Overlay` | Diagonal-hatched sheet with a dialog floating on it | +| 5 | `GuiButtonCtrl` | Rounded rect, centred label bar knocked out | +| 6 | `GuiCheckBoxCtrl` | Box with a check, label bars right | +| 7 | `GuiRadioCtrl` | Ring with a dot, label bars right | +| 8 | `GuiDropDownCtrl` | Field, divider, ▼ chevron | +| 9 | `GuiColorPopupCtrl` | 3×2 swatch block + the same chevron | +| 10 | `GuiImageButtonCtrl` | Button with a small picture **on** it | +| 11 | `GuiTextEditCtrl` | Field, text bar, I-beam caret | +| 12 | `GuiTextEditSliderCtrl` | The same field with ▲▼ spinner | +| 13 | `GuiSliderCtrl` | Thin track, thumb, tick marks | +| 14 | `GuiProgressCtrl` | Pill filled from the left | +| 15 | `GuiColorPickerCtrl` | Blend field with a selector ring, banded hue strip | +| 16 | `GuiSpriteCtrl` | Closed frame, peak, sun — a photo | +| 17 | `SceneWindow` | Heavier frame, ground line, ball and crate — a game view | +| 18 | `GuiListBoxCtrl` | Framed rows, one inverted | +| 19 | `GuiTreeViewCtrl` | Spine, elbows, disclosure triangle | +| 20 | `GuiMenuBarCtrl` | Title bar with a menu dropped open | +| 21 | `GuiChainCtrl` | Three equal unframed bars, equal gaps | +| 22 | `GuiGridCtrl` | 3×3 cells with gutters | +| 23 | `GuiScrollCtrl` | Content with a track and thumb | +| 24 | `GuiFrameSetCtrl` | Panes split by thick dividers | +| 25 | `GuiPanelCtrl` | Captioned header, chevron right, body | +| 26 | `GuiExpandCtrl` | Bare header, centred double chevron, body with content | +| 27 | `GuiTabBookCtrl` | Three tabs, first merged into the page | +| 28 | `GuiTabPageCtrl` | One tab and its page, content drawn | +| 29 | `GuiWindowCtrl` | Title bar with three buttons, body | +| 30 | `GuiInputCtrl` | Keycap with a pointer over it — the one metaphor | + +### The eight pairs that must stay apart + +Each is a review checkpoint, and `review.py pairs` renders them side by side: + +| pair | differentiator | +|---|---| +| Sprite / ImageButton | button margin around the picture | +| Sprite / SceneWindow | photo furniture vs. physics furniture | +| Empty / SceneWindow | open brackets vs. closed frame | +| ColorPicker / ColorPopup | chevron only on the popup | +| Panel / Expand | caption present or absent | +| TabBook / TabPage | three tabs or one | +| Slider / Progress | thin track vs. pill | +| Label / Chain | ragged lengths vs. equal blocks | + +### Files + +Authoring, in `T2D Rocket Edition/Control Icon Set/` (not a git repo — it is where the Mono set lives): + +| | | +|---|---| +| `icons.py` | primitives, the 64-unit space, the renderer, the ink ramp, all 31 drawings, and `ICONS` — the ordered palette table | +| `build_control_icon_sheets.py` | renders, asserts, packs, and generates | +| `review.py` | contact sheets: `all`, `probe`, `pairs` | + +**Run with `py`, not `python`** — 3.11 has PIL 12.3.0, the 3.10 first on PATH does not. + +Generated into the repo: + +``` +editor/GuiEditor/images/controlIcons128.png 8×4, 1024×512 +editor/GuiEditor/images/controlIcons128.asset.taml +editor/GuiEditor/images/controlIcons64.png 8×4, 512×256 +editor/GuiEditor/images/controlIcons64.asset.taml +editor/GuiEditor/scripts/GuiEditorControlIcons.cs +``` + +8×4 rather than 6×6: 31 icons need more than 30 cells, and 8×4 lands both sheets on power-of-two dimensions. Both are well inside the 2048px cap that silently renders oversized textures black. The two sizes are **source resolution**, not tile size — `sheetFor()` picks the 128 sheet above 96px and the 64 sheet below. + +### The lookup + +`GuiEditorControlIcons` is a generated `ScriptObject` holding every entry in display order — `key`, `ctrlClass`, `category`, `label`, `frame` — with `keys()`, `frameFor()`, `classFor()`, `categoryFor()`, `labelFor()`, `isKnown()` and `sheetFor()`. + +The array is `ctrlClass`, not `class`: `class` is a live SimObject field, and `GuiEditorControlSpec` already documents dodging exactly that collision. + +### Drift: a fallback, not a build break + +Trading a runtime enumeration for a build-time list means a control class added to the engine could silently never reach the palette. That is handled by degrading, not by failing: + +1. **Frame 0 is the fallback**, deliberately not last. `frameFor` returns 0 for any unknown key, and an unset `Frame` field reads as 0 too — so both failures land on a legible "unknown control" rather than an arbitrary wrong picture. +2. **The palette keeps its runtime enumeration as a tail.** It walks the curated table for order, then appends anything `enumerateConsoleClasses("GuiControl")` yields that the table does not name, filtered by the rule `populate` uses today. A new class still appears and still drags; it wears the `?` until someone draws it. *(This half lands in the follow-on palette spec; the contract — curated ordered head plus a zero fallback — is established here.)* +3. **The build script prints, without failing**, both directions: placeable classes with no icon, and icons naming a class the engine no longer registers. + +Determining "placeable" needs the real ancestry, not a name prefix. Two traps found writing it: + +- `GuiControl` registers with **`IMPLEMENT_CONOBJECT_CHILDREN`**, not `IMPLEMENT_CONOBJECT`. Matching only the plain macro loses the one class the palette needs most. +- `GuiCursor`, `GuiControlProfile`, `GuiBorderProfile` and `GuiProfileTheme` are all registered and all named `Gui…`, but derive from `SimObject` and never appear in the palette. The script walks first-base ancestry to `GuiControl` — which is what `enumerateConsoleClasses("GuiControl")` actually answers. + +### Module wiring — two silent failures + +- **`editor/GuiEditor/module.taml` had no `` block and no `images` folder** — the one editor module shipping no assets. Without the block the PNGs sit on disk and never register, with no error anywhere. Added, matching `EditorCore/module.taml`; note the extension is `asset.taml`, not `image.taml`. Asset ids are `GuiEditor:controlIcons64` / `…128`. +- **`GuiEditor::create` execs every script by name.** Added the exec, plus the `ScriptObject` beside `themeLibrary` and its `delete()` in `GuiEditor::destroy`, per the ownership chain in `TORQUE_SCRIPT.md`. + +## Verification + +**Build-script assertions**, run every build: + +- every icon renders at both sizes, exactly `size × size` +- opaque bbox reaches at least two tile edges — catches an icon that silently drew small (this caught `GuiGridCtrl` stopping 2 units short of the right edge) +- **every opaque pixel carries the exact ramp value for its row.** Sampling the tile's top and bottom rows does not work: most icons are inset and touch neither. The first attempt asserted on the sheet's row 0 and failed against correct art, because no icon in the first sheet row happens to reach y=0. +- frame 0 is the fallback; no duplicate keys; 31 entries in 32 cells + +**`review.py`** writes contact sheets on a dark ground — the whole set at both sizes, the style probe at 2×/4×, and the eight pairs side by side. + +**`tests/shots/controlIcons.cs`** renders both sheets in-engine at the sizes the palette will draw them, tinted the way `EditorIconButton` tints, labelled by key, asking `GuiEditor.controlIcons` for the list so the page cannot drift from the sheet. Its real job is proving the assets load: a missing image renders as nothing rather than throwing, so the `DeclaredAssets` failure mode is a page of labels over blank space — which nothing else would catch. + +Full smoke suite re-run after the wiring changes. + +## Out of scope + +- The palette rewrite — grid/rows toggle, tile control, `populate` walking the table then appending the enumerated tail. +- 16/24/32 sheets and the Explorer tree adopting them. The 4-unit grid rule keeps that a rerun. + +## Open item for the follow-on + +The Control List window is **250px wide** by default (`GuiEditor.cs`, `Extent = "250 380"`). A true 128px grid gets one column at that width. The palette spec will need to widen the default extent, draw the 128 art at ~96px for two columns, or both. diff --git a/editor/GuiEditor/GuiEditor.cs b/editor/GuiEditor/GuiEditor.cs index 2a9a1cf48..ffcce5e90 100644 --- a/editor/GuiEditor/GuiEditor.cs +++ b/editor/GuiEditor/GuiEditor.cs @@ -23,6 +23,7 @@ function GuiEditor::create( %this ) { exec("./scripts/GuiEditorBrain.cs"); + exec("./scripts/GuiEditorControlIcons.cs"); exec("./scripts/GuiEditorControlListWindow.cs"); exec("./scripts/GuiEditorControlListBox.cs"); exec("./scripts/GuiEditorInspectorWindow.cs"); @@ -62,6 +63,14 @@ %this.guiPage = EditorCore.RegisterEditor("Gui Editor", %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 @@ -321,6 +330,11 @@ class = "SimulatedCanvas"; { %this.themeLibrary.delete(); } + + if(isObject(%this.controlIcons)) + { + %this.controlIcons.delete(); + } } function GuiEditor::open(%this, %content) 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 0000000000000000000000000000000000000000..6428004036e79094cb4bee14f9389db081b56e89 GIT binary patch literal 45502 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 diff --git a/editor/GuiEditor/images/controlIcons64.png b/editor/GuiEditor/images/controlIcons64.png new file mode 100644 index 0000000000000000000000000000000000000000..c31f3d4abb83f0ac0225c69616e3a9d33b2edb88 GIT binary patch literal 22024 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�{so@bz(5x^=yB1PK$sfHBm#3>2thA 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 H*@gcPjAf10 literal 0 HcmV?d00001 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/GuiEditorControlIcons.cs b/editor/GuiEditor/scripts/GuiEditorControlIcons.cs new file mode 100644 index 000000000..fe18f1bb4 --- /dev/null +++ b/editor/GuiEditor/scripts/GuiEditorControlIcons.cs @@ -0,0 +1,133 @@ +//----------------------------------------------------------------------------- +// The control palette's entries and their icons. GENERATED - do not hand-edit. +// +// GuiEditor:controlIcons64 and controlIcons128 are the same 31 drawings at two +// sizes in the same 8x4 grid, so one index names the same icon in both and a +// tile can change size without changing its Frame. The two sizes are source +// resolution, not tile size: the grid view draws from the 128 sheet and the row +// view from the 64. +// +// 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. +// +// Indices are display order, so inserting an icon reflows them. Nothing should +// ever write a raw frame number; ask frameFor. +//----------------------------------------------------------------------------- + +function GuiEditorControlIcons::onAdd(%this) +{ + // key TAB class TAB category TAB label + %table = + "unknown" TAB "" TAB "" TAB "Unknown" NL + "GuiControl:Empty" TAB "GuiControl" TAB "Empty" TAB "Empty" NL + "GuiControl:Panel" TAB "GuiControl" TAB "Panel" TAB "Panel" NL + "GuiControl:Label" TAB "GuiControl" TAB "Label" TAB "Label" NL + "GuiControl:Overlay" TAB "GuiControl" TAB "Overlay" TAB "Overlay" NL + "GuiButtonCtrl" TAB "GuiButtonCtrl" TAB "" TAB "Button" NL + "GuiCheckBoxCtrl" TAB "GuiCheckBoxCtrl" TAB "" TAB "Check Box" NL + "GuiRadioCtrl" TAB "GuiRadioCtrl" TAB "" TAB "Radio Button" NL + "GuiDropDownCtrl" TAB "GuiDropDownCtrl" TAB "" TAB "Drop Down" NL + "GuiColorPopupCtrl" TAB "GuiColorPopupCtrl" TAB "" TAB "Color Popup" NL + "GuiImageButtonCtrl" TAB "GuiImageButtonCtrl" TAB "" TAB "Image Button" NL + "GuiTextEditCtrl" TAB "GuiTextEditCtrl" TAB "" TAB "Text Edit" NL + "GuiTextEditSliderCtrl" TAB "GuiTextEditSliderCtrl" TAB "" TAB "Number Box" NL + "GuiSliderCtrl" TAB "GuiSliderCtrl" TAB "" TAB "Slider" NL + "GuiProgressCtrl" TAB "GuiProgressCtrl" TAB "" TAB "Progress" NL + "GuiColorPickerCtrl" TAB "GuiColorPickerCtrl" TAB "" TAB "Color Picker" NL + "GuiSpriteCtrl" TAB "GuiSpriteCtrl" TAB "" TAB "Sprite" NL + "SceneWindow" TAB "SceneWindow" TAB "" TAB "Scene Window" NL + "GuiListBoxCtrl" TAB "GuiListBoxCtrl" TAB "" TAB "List Box" NL + "GuiTreeViewCtrl" TAB "GuiTreeViewCtrl" TAB "" TAB "Tree View" NL + "GuiMenuBarCtrl" TAB "GuiMenuBarCtrl" TAB "" TAB "Menu Bar" NL + "GuiChainCtrl" TAB "GuiChainCtrl" TAB "" TAB "Chain" NL + "GuiGridCtrl" TAB "GuiGridCtrl" TAB "" TAB "Grid" NL + "GuiScrollCtrl" TAB "GuiScrollCtrl" TAB "" TAB "Scroll" NL + "GuiFrameSetCtrl" TAB "GuiFrameSetCtrl" TAB "" TAB "Frame Set" NL + "GuiPanelCtrl" TAB "GuiPanelCtrl" TAB "" TAB "Panel" NL + "GuiExpandCtrl" TAB "GuiExpandCtrl" TAB "" TAB "Expand" NL + "GuiTabBookCtrl" TAB "GuiTabBookCtrl" TAB "" TAB "Tab Book" NL + "GuiTabPageCtrl" TAB "GuiTabPageCtrl" TAB "" TAB "Tab Page" NL + "GuiWindowCtrl" TAB "GuiWindowCtrl" TAB "" TAB "Window" NL + "GuiInputCtrl" TAB "GuiInputCtrl" TAB "" 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.label[%key] = trim(getField(%rec, 3)); + + // The fallback is not something anyone drags, so it stays out of the + // list the palette walks. + if(%i > 0) + { + %this.keyList = (%this.keyList $= "") ? %key : (%this.keyList TAB %key); + } + } +} + +// The palette's entries, in display order, tab separated. The fallback is not +// among them. +function GuiEditorControlIcons::keys(%this) +{ + return %this.keyList; +} + +// 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] !$= ""; +} + +// The sheet to draw from at a given tile size. Anything under the halfway point +// takes the smaller source rather than downsampling the large one. +function GuiEditorControlIcons::sheetFor(%this, %tileSize) +{ + return (%tileSize > 96) ? "GuiEditor:controlIcons128" : "GuiEditor:controlIcons64"; +} diff --git a/tests/shots/controlIcons.cs b/tests/shots/controlIcons.cs new file mode 100644 index 000000000..92e3426ff --- /dev/null +++ b/tests/shots/controlIcons.cs @@ -0,0 +1,117 @@ +// Renders the control palette's icon sheets, tinted the way the editor will tint +// them, with each entry's key beside its picture. +// +// Two jobs. The obvious one is looking at the art at the size it will actually be +// drawn -- the review sheets the build script writes are on a dark ground of +// their own choosing, and this is the real theme. +// +// The load-bearing one is proving the assets load at all. GuiEditor's module.taml +// carried no block until these sheets arrived; without it the +// PNGs sit on disk and are never registered, with no error anywhere. A missing +// image renders as nothing rather than throwing, so the failure is a page of +// labels with blank space where the icons should be -- which is exactly what this +// shot shows, and nothing else would. +// +// Page 0 is the 128 sheet at 96px, which is what the grid view will draw. Page 1 +// is the 64 sheet at 48px, which is the row view. +setLogMode(2); +setScriptExecEcho(false); +trace(false); +$Scripts::ignoreDSOs = true; +setCompanyAndProduct("Torque Game Engines", "Torque2D"); +ModuleDatabase.EchoInfo = false; +AssetDatabase.EchoInfo = false; + +testExec("editor/main.cs"); +schedule(2500, 0, "controlIconsShot"); + +$controlIcons::page = 0; +$controlIcons::pages = 2; + +function controlIconsShot() +{ + if($controlIcons::page >= $controlIcons::pages) + { + echo("SHOTS DONE"); + quit(); + return; + } + + // The table the Gui Editor built at create time. Asking it rather than + // hard-coding 31 means this page cannot drift from the sheet. + %icons = GuiEditor.controlIcons; + %keys = %icons.keys(); + %count = getFieldCount(%keys); + + %big = ($controlIcons::page == 0); + %tile = %big ? 96 : 48; + %sheet = %icons.sheetFor(%tile); + %cell = %big ? 128 : 64; + + %page = new GuiControl() { Position = "0 0"; Extent = "1024 768"; }; + ThemeManager.setProfile(%page, "panelProfile"); + $controlIcons::gui = %page; + + %cols = %big ? 6 : 8; + %stepX = %big ? 168 : 126; + %stepY = %big ? 128 : 76; + + // The fallback is not in keys() -- it is what the palette falls back TO, not + // something anyone drags -- so it is drawn first and by hand. It is the one + // frame a wrong answer from frameFor would land on, so seeing it is the point. + controlIconsTile(%page, 12, 8, %tile, %sheet, %cell, 0, "unknown"); + + for(%i = 0; %i < %count; %i++) + { + %key = getField(%keys, %i); + %slot = %i + 1; + %x = 12 + ((%slot % %cols) * %stepX); + %y = 8 + (mFloor(%slot / %cols) * %stepY); + controlIconsTile(%page, %x, %y, %tile, %sheet, %cell, %icons.frameFor(%key), %key); + } + + Canvas.pushDialog(%page); + schedule(1000, 0, "controlIconsGrab"); +} + +// One tile: the sprite, tinted from the theme exactly as EditorIconButton tints +// its icon, with the entry's key under it. +function controlIconsTile(%page, %x, %y, %tile, %sheet, %cell, %frame, %label) +{ + %icon = new GuiSpriteCtrl() + { + Position = %x SPC %y; + Extent = %tile SPC %tile; + Image = %sheet; + ImageSize = %cell SPC %cell; + Frame = %frame; + ImageColor = ThemeManager.activeTheme.iconButtonProfile.FontColor; + constrainProportions = "1"; + fullSize = "0"; + }; + ThemeManager.setProfile(%icon, "spriteProfile"); + %page.add(%icon); + + %text = new GuiControl() + { + Position = %x SPC (%y + %tile + 2); + Extent = (%tile + 60) SPC 16; + Text = %label; + }; + ThemeManager.setProfile(%text, "labelProfile"); + %page.add(%text); +} + +function controlIconsGrab() +{ + screenShot(testRoot("shots/controlIcons" @ $controlIcons::page @ ".png"), "PNG"); + schedule(500, 0, "controlIconsNext"); +} + +function controlIconsNext() +{ + Canvas.popDialog($controlIcons::gui); + $controlIcons::gui.delete(); + $controlIcons::page++; + schedule(200, 0, "controlIconsShot"); +} From d51f8e61367ff80cf7bd207ed6392a88add9954d Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Wed, 29 Jul 2026 12:09:48 -0400 Subject: [PATCH 11/55] Gui Editor: let the shot harness make the folder it writes into screenShot does not create its destination, and it reports failure by logging rather than by throwing. A tree that has never run a shot before -- a fresh clone, or a git worktree, where shots/ is gitignored and so absent -- therefore runs the harness green all the way to SHOTS DONE and writes nothing at all. The runner counts files on disk, so it reads as zero shots with no indication why. Every other harness in shots/ has the same latent hole and is only saved by inheriting a folder that already exists. Fixing this one where it was found; the rest want the same line. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FVXsRigjXMEjMrbyHYawJD --- tests/shots/controlIcons.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/shots/controlIcons.cs b/tests/shots/controlIcons.cs index 92e3426ff..1260f7ad3 100644 --- a/tests/shots/controlIcons.cs +++ b/tests/shots/controlIcons.cs @@ -104,6 +104,11 @@ function controlIconsTile(%page, %x, %y, %tile, %sheet, %cell, %frame, %label) function controlIconsGrab() { + // screenShot does not create the folder, and it fails by logging rather than + // throwing -- so in a tree that has never run one (a fresh clone, or a git + // worktree, where shots/ is gitignored and therefore absent) the harness + // otherwise runs green all the way to SHOTS DONE and writes nothing. + createPath(testRoot("shots/")); screenShot(testRoot("shots/controlIcons" @ $controlIcons::page @ ".png"), "PNG"); schedule(500, 0, "controlIconsNext"); } From 50f1d376c684f55cb21ed117fde00835910a47c7 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Wed, 29 Jul 2026 15:03:14 -0400 Subject: [PATCH 12/55] Gui Editor: make the control palette a palette It was a list box of class names -- GuiTextEditSliderCtrl above GuiTreeViewCtrl above GuiTextEditCtrl -- which told a person nothing about what any of them was. The icons went in last time; this is the thing that shows them. Two views, switched by a pair of buttons at the top. Grid gives 80-pixel pictures three to a row with the class name in the tooltip; rows gives a 32-pixel picture with a human-readable name beside it. Both are the same GuiGridCtrl with different cell metrics rather than two layouts, which is possible because a grid treats CellSizeX as the narrowest a column may be and shares the remainder -- so grid mode reflows to two columns or four as the frame is dragged, and rows mode asks for a cell wider than the pane and can only ever be one. Vertically the mode is absolute; variable sizes a row to its tallest child, which put a 100-pixel tile in a 40-pixel row. The controls are in four collapsible groups, ordered so the kinds people reach for most sit at the top. Grouping is a column on the generated table rather than a list here, and deliberately independent of frame order: regrouping the palette must not repack the sheets, or a frame index would quietly start naming a different picture. The buttons are a GuiEditorChoiceRow, which already describes itself as a radio group that looks like a segmented control. Nothing new was needed. A click now places a control as well as a drag. It routes through the brain's own onControlDropped rather than adding anything itself -- that path is where theming, selection, the AddControl event and undo recording live, and a second way into the document would have had to reproduce all of it and would have gone stale the first time one changed. A click is a drop that never moved. Two things had to be handled: a button keeps mDepressed through a drag, so releasing after one fires onAction too and would have placed a second control; and a press has to travel five pixels before it counts as a drag, or a shaky click starts one. The four faces of a bare GuiControl can finally be dropped as what they are. The theme applier guessed the category from what a control held, and two of the four were unreachable that way -- a Panel that is not the root, and Overlay ever. The palette now says outright which it dropped, and the applier consumes that on the first pass: what the control ends up wearing is the lasting record, so a request left lying about would overrule a later change made in the properties pane. Three things this cost: - A panel starts collapsed and measures its open height from the children it has at the moment it opens, so the groups had to be expanded after their tiles rather than with them, and remeasured on every mode change. - The tiles sit in an inner grid, never on the panel, because GuiExpandCtrl force-writes mVisible on every direct child whenever it opens or closes. - A fixed bar above a stretching pane has no sizing flag: fill discards the position and height needs the border sizes, which script cannot ask for. The scroller is built filling so the engine measures the content rect, then moved down under the bar and switched to height. isKnown answers about a key; coversClass answers about a class. Only one class needs the difference, and it is the one that matters: nothing is keyed "GuiControl", so the sweep for undrawn classes offered a fifth, iconless copy of a control the palette already showed four ways. Verified by tests/smoke/palette.cs (111 checks) and a three-shot harness. The drag half is not covered -- startDragging wants real mouse state, and the input-driven suites are the flaky ones -- but it shares makePayload with the click, which is. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FVXsRigjXMEjMrbyHYawJD --- editor/GuiEditor/GuiEditor.cs | 10 +- .../scripts/GuiEditorControlGroup.cs | 140 ++++++++ .../scripts/GuiEditorControlIcons.cs | 175 +++++++--- .../scripts/GuiEditorControlListBox.cs | 38 --- .../scripts/GuiEditorControlListWindow.cs | 299 ++++++++++++++---- .../GuiEditor/scripts/GuiEditorControlTile.cs | 269 ++++++++++++++++ .../scripts/GuiEditorThemeApplier.cs | 16 + tests/shots/controlPalette.cs | 87 +++++ tests/smoke/palette.cs | 293 +++++++++++++++++ 9 files changed, 1186 insertions(+), 141 deletions(-) create mode 100644 editor/GuiEditor/scripts/GuiEditorControlGroup.cs delete mode 100644 editor/GuiEditor/scripts/GuiEditorControlListBox.cs create mode 100644 editor/GuiEditor/scripts/GuiEditorControlTile.cs create mode 100644 tests/shots/controlPalette.cs create mode 100644 tests/smoke/palette.cs diff --git a/editor/GuiEditor/GuiEditor.cs b/editor/GuiEditor/GuiEditor.cs index ffcce5e90..82e6663ef 100644 --- a/editor/GuiEditor/GuiEditor.cs +++ b/editor/GuiEditor/GuiEditor.cs @@ -25,7 +25,8 @@ 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/GuiEditorExplorerWindow.cs"); exec("./scripts/GuiEditorExplorerTree.cs"); @@ -287,7 +288,12 @@ 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); diff --git a/editor/GuiEditor/scripts/GuiEditorControlGroup.cs b/editor/GuiEditor/scripts/GuiEditorControlGroup.cs new file mode 100644 index 000000000..14761c1d7 --- /dev/null +++ b/editor/GuiEditor/scripts/GuiEditorControlGroup.cs @@ -0,0 +1,140 @@ +//----------------------------------------------------------------------------- +// 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 +// (AssetAdmin::buildDictionary, 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. +//----------------------------------------------------------------------------- + +$GuiEditorControlGroup::gridCell = 100; +$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 100-pixel tile in a 40-pixel row. + CellModeX = "variable"; + CellModeY = "absolute"; + CellSizeX = $GuiEditorControlGroup::gridCell; + CellSizeY = $GuiEditorControlGroup::gridCell; + 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::gridCell; + 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::gridCell; + } + + // 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 index fe18f1bb4..9c9c6408d 100644 --- a/editor/GuiEditor/scripts/GuiEditorControlIcons.cs +++ b/editor/GuiEditor/scripts/GuiEditorControlIcons.cs @@ -19,45 +19,63 @@ // palette -- see GuiEditorControlListWindow::populate -- it simply wears the // question mark until someone draws it. // -// Indices are display order, so inserting an icon reflows them. Nothing should -// ever write a raw frame number; ask frameFor. +// 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) { - // key TAB class TAB category TAB label + // 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 two are real controls + // that nobody places by hand -- GuiDragAndDropCtrl is built at runtime to + // carry a drag payload, and GuiMenuItemCtrl means nothing outside a menu bar. + %this.refusedNames = "GuiCanvas GuiDragAndDropCtrl GuiGraphCtrl GuiMenuItemCtrl GuiMessageVectorCtrl GuiParticleGraphInspector GuiSceneObjectCtrl"; + %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 "Unknown" NL - "GuiControl:Empty" TAB "GuiControl" TAB "Empty" TAB "Empty" NL - "GuiControl:Panel" TAB "GuiControl" TAB "Panel" TAB "Panel" NL - "GuiControl:Label" TAB "GuiControl" TAB "Label" TAB "Label" NL - "GuiControl:Overlay" TAB "GuiControl" TAB "Overlay" TAB "Overlay" NL - "GuiButtonCtrl" TAB "GuiButtonCtrl" TAB "" TAB "Button" NL - "GuiCheckBoxCtrl" TAB "GuiCheckBoxCtrl" TAB "" TAB "Check Box" NL - "GuiRadioCtrl" TAB "GuiRadioCtrl" TAB "" TAB "Radio Button" NL - "GuiDropDownCtrl" TAB "GuiDropDownCtrl" TAB "" TAB "Drop Down" NL - "GuiColorPopupCtrl" TAB "GuiColorPopupCtrl" TAB "" TAB "Color Popup" NL - "GuiImageButtonCtrl" TAB "GuiImageButtonCtrl" TAB "" TAB "Image Button" NL - "GuiTextEditCtrl" TAB "GuiTextEditCtrl" TAB "" TAB "Text Edit" NL - "GuiTextEditSliderCtrl" TAB "GuiTextEditSliderCtrl" TAB "" TAB "Number Box" NL - "GuiSliderCtrl" TAB "GuiSliderCtrl" TAB "" TAB "Slider" NL - "GuiProgressCtrl" TAB "GuiProgressCtrl" TAB "" TAB "Progress" NL - "GuiColorPickerCtrl" TAB "GuiColorPickerCtrl" TAB "" TAB "Color Picker" NL - "GuiSpriteCtrl" TAB "GuiSpriteCtrl" TAB "" TAB "Sprite" NL - "SceneWindow" TAB "SceneWindow" TAB "" TAB "Scene Window" NL - "GuiListBoxCtrl" TAB "GuiListBoxCtrl" TAB "" TAB "List Box" NL - "GuiTreeViewCtrl" TAB "GuiTreeViewCtrl" TAB "" TAB "Tree View" NL - "GuiMenuBarCtrl" TAB "GuiMenuBarCtrl" TAB "" TAB "Menu Bar" NL - "GuiChainCtrl" TAB "GuiChainCtrl" TAB "" TAB "Chain" NL - "GuiGridCtrl" TAB "GuiGridCtrl" TAB "" TAB "Grid" NL - "GuiScrollCtrl" TAB "GuiScrollCtrl" TAB "" TAB "Scroll" NL - "GuiFrameSetCtrl" TAB "GuiFrameSetCtrl" TAB "" TAB "Frame Set" NL - "GuiPanelCtrl" TAB "GuiPanelCtrl" TAB "" TAB "Panel" NL - "GuiExpandCtrl" TAB "GuiExpandCtrl" TAB "" TAB "Expand" NL - "GuiTabBookCtrl" TAB "GuiTabBookCtrl" TAB "" TAB "Tab Book" NL - "GuiTabPageCtrl" TAB "GuiTabPageCtrl" TAB "" TAB "Tab Page" NL - "GuiWindowCtrl" TAB "GuiWindowCtrl" TAB "" TAB "Window" NL - "GuiInputCtrl" TAB "GuiInputCtrl" TAB "" TAB "Input"; + "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 + "GuiImageButtonCtrl" TAB "GuiImageButtonCtrl" TAB "" TAB "Input & Data" TAB "Image Button" 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); @@ -69,24 +87,85 @@ %this.frame[%key] = %i; %this.ctrlClass[%key] = trim(getField(%rec, 1)); %this.category[%key] = trim(getField(%rec, 2)); - %this.label[%key] = trim(getField(%rec, 3)); + %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 the - // list the palette walks. + // 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); + + %group = %this.group[%key]; + %held = %this.groupKeys[%group]; + %this.groupKeys[%group] = (%held $= "") ? %key : (%held 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; } } } -// The palette's entries, in display order, tab separated. The fallback is not -// among them. +// 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) { @@ -125,9 +204,21 @@ return %this.frame[%key] !$= ""; } -// The sheet to draw from at a given tile size. Anything under the halfway point -// takes the smaller source rather than downsampling the large one. +// 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. The threshold is the SMALL +// sheet's own resolution, not the midpoint between the two: above 64 the small +// sheet would have to be enlarged, and enlarging is what looks soft. Taking the +// 128 and shrinking it costs nothing and stays sharp. function GuiEditorControlIcons::sheetFor(%this, %tileSize) { - return (%tileSize > 96) ? "GuiEditor:controlIcons128" : "GuiEditor:controlIcons64"; + return (%tileSize > 64) ? "GuiEditor:controlIcons128" : "GuiEditor:controlIcons64"; } 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 0ebc1f9c6..5bb90c99f 100644 --- a/editor/GuiEditor/scripts/GuiEditorControlListWindow.cs +++ b/editor/GuiEditor/scripts/GuiEditorControlListWindow.cs @@ -1,77 +1,258 @@ //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) { - // "fill", not "width"/"height": this is the window's only child, so it wants - // the whole content rect. Those two only preserve whatever gap the authored - // extent started with, and the authored extent was measured against a 20-pixel - // title bar -- the default is 28 now, so every one of these windows was - // clipping its last eight pixels. Fill measures against the parent's inner - // rect, which already has the title taken out of it, so it cannot drift again. - %this.scroller = new GuiScrollCtrl() + %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 355"; - hScrollBar="alwaysOff"; - vScrollBar="alwaysOn"; - constantThumbHeight="0"; - showArrowButtons="1"; - scrollBarThickness="14"; + 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(); + + %this.groupChain = new GuiChainCtrl() + { + HorizSizing = "width"; + Position = "0 0"; + Extent = "242 4"; + IsVertical = true; + ChildSpacing = 2; + }; + ThemeManager.setProfile(%this.groupChain, "emptyProfile"); + %this.scroller.add(%this.groupChain); + + %this.populate(); +} - %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); - - %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); + + %inner = %this.scroller.getExtent(); + %bar = $GuiEditorControlListWindow::modeBarHeight; + + %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 palette is what a person can drag into a Gui, which is not the same as -// every class deriving from GuiControl. Two kinds are filtered out: the ones -// that are editor or engine plumbing (the console, the edit control, the -// inspector types), and the ones that are real controls but are never placed by -// hand -- GuiDragAndDropCtrl is created at runtime to carry a drag payload, and -// GuiMenuItemCtrl only means anything as a child of a menu bar (it does not -// even register GuiControl's fields, calling SimObject::initPersistFields -// directly, so it has no profile, position or extent to give it). +//----------------------------------------------------------------------------- +// The view switch. GuiEditorChoiceRow 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 = "GuiEditorChoiceRow"; + 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 icons"); + %this.modeRow.addChoice("rows", $EditorIcon::list_bullets, "Show controls as rows with names"); + %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. +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); - - 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" && - %field !$= "GuiDragAndDropCtrl" && %field !$= "GuiMenuItemCtrl") - { - %this.listBox.addItem(%field); - } + class = "GuiEditorControlGroup"; + 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/GuiEditorControlTile.cs b/editor/GuiEditor/scripts/GuiEditorControlTile.cs new file mode 100644 index 000000000..b3417223c --- /dev/null +++ b/editor/GuiEditor/scripts/GuiEditorControlTile.cs @@ -0,0 +1,269 @@ +//----------------------------------------------------------------------------- +// 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 an 80px picture centred in the cell, no caption, name in the tooltip +// rows a 32px picture at the left with the name beside it +// +// The creator sets key and owner inline, then calls setMode. +//----------------------------------------------------------------------------- + +// Grid mode wants the 128 sheet (sheetFor shrinks it, which stays sharp); rows +// mode is under the small sheet's own resolution and takes it 1:1. +$GuiEditorControlTile::gridArt = 80; +$GuiEditorControlTile::rowArt = 32; +$GuiEditorControlTile::rowTextLeft = 40; + +// 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 -- and in grid + // mode, where there is no caption at all, the tooltip is the only name. + %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"); +} + +//----------------------------------------------------------------------------- +// Layout. +//----------------------------------------------------------------------------- + +function GuiEditorControlTile::setMode(%this, %mode) +{ + %this.mode = %mode; + %w = getWord(%this.getExtent(), 0); + %h = getWord(%this.getExtent(), 1); + + 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.setVisible(true); + } + else + { + %art = $GuiEditorControlTile::gridArt; + + %this.icon.HorizSizing = "center"; + %this.icon.VertSizing = "center"; + %this.icon.setExtent(%art, %art); + %this.icon.setPosition((%w - %art) / 2, (%h - %art) / 2); + + %this.caption.setVisible(false); + } + + // 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; + } + + return %payload; +} + +//----------------------------------------------------------------------------- +// 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; + } + + // 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 = %this.dropPosition(%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". + GuiEditor.brain.onControlDropped(%payload, %payload.Position); +} + +// Centred in the current add set, which is the container the editor is already +// adding to; addNewControl falls back to the root when there is none. +function GuiEditorControlTile::dropPosition(%this, %payload) +{ + %target = GuiEditor.brain.getCurrentAddSet(); + if(!isObject(%target)) + { + %target = GuiEditor.rootGui; + } + + %at = %target.getGlobalPosition(); + %room = %target.getExtent(); + %size = %payload.getExtent(); + + %x = getWord(%at, 0) + ((getWord(%room, 0) - getWord(%size, 0)) / 2); + %y = getWord(%at, 1) + ((getWord(%room, 1) - getWord(%size, 1)) / 2); + + return mFloor(%x) SPC mFloor(%y); +} diff --git a/editor/GuiEditor/scripts/GuiEditorThemeApplier.cs b/editor/GuiEditor/scripts/GuiEditorThemeApplier.cs index 29d89c512..ce81a9cfd 100644 --- a/editor/GuiEditor/scripts/GuiEditorThemeApplier.cs +++ b/editor/GuiEditor/scripts/GuiEditorThemeApplier.cs @@ -154,6 +154,13 @@ %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; } @@ -414,6 +421,15 @@ { 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"; diff --git a/tests/shots/controlPalette.cs b/tests/shots/controlPalette.cs new file mode 100644 index 000000000..5e79f3bc9 --- /dev/null +++ b/tests/shots/controlPalette.cs @@ -0,0 +1,87 @@ +// Visual harness for the Gui Editor's control palette. Three shots: +// +// 0 grid mode, every group open -- the default view +// 1 row mode, every group open -- icon plus human-readable name +// 2 grid mode with two groups collapsed +// +// The third is not decoration. GuiExpandCtrl force-writes mVisible on every +// direct child of a panel whenever it expands or collapses, which is why the +// tiles live in an inner grid; collapsing and reopening is what proves the tiles +// survived it. +// +// Run: tests/run.ps1 -Shots controlPalette ; look in shots/. + +setLogMode(1); +$Scripts::ignoreDSOs = true; +setScriptExecEcho(false); +trace(false); + +testExec("editor/main.cs"); +schedule(2500, 0, "pOpenProject"); + +function pOpenProject() +{ + ProjectManager.setProjectFolder("PlanetX"); + EditorCore.projectSelector.onProjectSelected(pathConcat(getMainDotCsDir(), "PlanetX")); + schedule(2500, 0, "pOpenEditor"); +} + +// Pages register in load order: EditorConsole, ProjectManager, AssetAdmin, +// GuiEditor. +function pOpenEditor() +{ + EditorCore.toggleEditor(); + EditorCore.tabBook.selectPage(3); + schedule(1500, 0, "pGridShot"); +} + +function pGrab(%name) +{ + // screenShot does not create its folder and reports failure by logging, so a + // tree that has never run a shot writes nothing and says nothing. + createPath(testRoot("shots/")); + screenShot(testRoot("shots/controlPalette" @ %name @ ".png"), "PNG"); +} + +function pGridShot() +{ + pGrab(0); + GuiEditor.ctrlListWindow.setMode("rows"); + GuiEditor.ctrlListWindow.modeRow.setValue("rows"); + schedule(800, 0, "pRowShot"); +} + +function pRowShot() +{ + pGrab(1); + GuiEditor.ctrlListWindow.setMode("grid"); + GuiEditor.ctrlListWindow.modeRow.setValue("grid"); + schedule(800, 0, "pCollapseShot"); +} + +// Collapse the FIRST group, not a later one: the palette column is short enough +// that everything past Basics is below the fold, so closing group 3 would look +// identical to not closing anything. +function pCollapseShot() +{ + %window = GuiEditor.ctrlListWindow; + %window.group[0].setExpanded(false); + %window.group[2].setExpanded(false); + %window.relayout(); + schedule(800, 0, "pFinish"); +} + +function pFinish() +{ + pGrab(2); + + // Reopen what was closed, so the last thing the shot proves is that a tile + // survives a collapse rather than being left hidden by the expand control. + %window = GuiEditor.ctrlListWindow; + %window.group[0].setExpanded(true); + %window.group[2].setExpanded(true); + %window.relayout(); + + echo("SHOTS DONE"); + schedule(500, 0, "quit"); +} diff --git a/tests/smoke/palette.cs b/tests/smoke/palette.cs new file mode 100644 index 000000000..5aa73ca2c --- /dev/null +++ b/tests/smoke/palette.cs @@ -0,0 +1,293 @@ +//----------------------------------------------------------------------------- +// The Gui Editor's control palette: the generated entry table it is built from, +// its two view modes, and the two things a tile can do. +// +// Driven through script rather than posted input on purpose. A tile's screen +// position depends on the scroll offset, which group is collapsed and how many +// columns the grid decided on, and none of that is exposed to script -- so a +// coordinate-based test would be asserting arithmetic it cannot check. The +// input-driven suites are also the flaky ones. +//----------------------------------------------------------------------------- + +setLogMode(2); +setScriptExecEcho(false); +trace(false); +$Scripts::ignoreDSOs = true; +setCompanyAndProduct("Torque Game Engines", "Torque2D"); +ModuleDatabase.EchoInfo = false; +AssetDatabase.EchoInfo = false; +AssetDatabase.IgnoreAutoUnload = true; + +testExec("editor/main.cs"); + +$Pass = 0; +$Fail = 0; + +function palCheck(%label, %condition) +{ + if(%condition) + { + $Pass++; + echo("PAL PASS: " @ %label); + } + else + { + $Fail++; + echo("PAL FAIL: " @ %label); + } +} + +schedule(2000, 0, "palSetup"); + +// A project, so there is a theme. Every control the editor places is themed on +// arrival, and the four faces of a bare GuiControl are told apart by the profile +// they end up wearing -- with no theme loaded, onControlDropped skips theming +// altogether and there is nothing to check. +function palSetup() +{ + ProjectManager.setProjectFolder("PlanetX"); + ModuleDatabase.ScanModules("PlanetX"); + ModuleDatabase.LoadExplicit("AppCore", 1); + + GuiEditor.open(); + palCheck("the editor adopted a theme", GuiEditor.themeName !$= ""); + + schedule(500, 0, "palTableChecks"); +} + +//----------------------------------------------------------------------------- +// The generated table. Checked before anything is built on it, because every +// failure here shows up much later and much less legibly -- a group whose keys +// come back empty renders as a collapsible section with nothing in it. +//----------------------------------------------------------------------------- + +function palTableChecks() +{ + %icons = GuiEditor.controlIcons; + palCheck("the icon table exists", isObject(%icons)); + + %groups = %icons.groups(); + palCheck("four groups (" @ getFieldCount(%groups) @ ")", getFieldCount(%groups) == 4); + + // The one that could quietly fail: "Input & Data" carries a space and an + // ampersand, and it is used as a dynamic-field subscript. If TorqueScript + // cannot hold that, keysInGroup answers "" and the group renders empty. + %awkward = getField(%groups, 2); + palCheck("third group is named \"" @ %awkward @ "\"", %awkward $= "Input & Data"); + palCheck("a group name with a space and an ampersand still keys its entries (" @ + getFieldCount(%icons.keysInGroup(%awkward)) @ ")", + getFieldCount(%icons.keysInGroup(%awkward)) == 7); + + // Every entry lands in exactly one group, and the groups account for all of + // them -- no entry silently dropped, none listed twice. + %all = %icons.keys(); + %total = getFieldCount(%all); + palCheck("30 entries outside the fallback (" @ %total @ ")", %total == 30); + + %sum = 0; + for(%g = 0; %g < getFieldCount(%groups); %g++) + { + %group = getField(%groups, %g); + %keys = %icons.keysInGroup(%group); + %count = getFieldCount(%keys); + palCheck("group \"" @ %group @ "\" has entries (" @ %count @ ")", %count > 0); + %sum += %count; + + for(%i = 0; %i < %count; %i++) + { + %key = getField(%keys, %i); + palCheck(%key @ " reports the group it was listed under", + %icons.groupFor(%key) $= %group); + } + } + palCheck("the groups account for every entry (" @ %sum @ " of " @ %total @ ")", %sum == %total); + + // The fallback is reachable but never offered. + palCheck("the fallback is not in any group", %icons.groupFor("unknown") $= ""); + palCheck("the fallback is not in the key list", strstr(%all, "unknown") == -1); + palCheck("an unknown key resolves to frame 0", %icons.frameFor("NoSuchCtrl") == 0); + palCheck("an unknown key is not known", !%icons.isKnown("NoSuchCtrl")); + palCheck("a real key is known", %icons.isKnown("GuiButtonCtrl")); + + // The four faces of a bare GuiControl: same class, different category. + %faces = "Empty" TAB "Panel" TAB "Label" TAB "Overlay"; + for(%i = 0; %i < 4; %i++) + { + %face = getField(%faces, %i); + %key = "GuiControl:" @ %face; + palCheck(%key @ " builds a GuiControl", %icons.classFor(%key) $= "GuiControl"); + palCheck(%key @ " carries category " @ %face, %icons.categoryFor(%key) $= %face); + } + + // Anything else pins its own category, so it asks for none. + palCheck("an ordinary class asks for no category", %icons.categoryFor("GuiButtonCtrl") $= ""); + palCheck("a class key builds its own class", %icons.classFor("GuiSliderCtrl") $= "GuiSliderCtrl"); + + // The sheet threshold is the small sheet's own resolution: above 64 the big + // sheet is shrunk rather than the small one enlarged. + palCheck("64px art takes the 64 sheet", %icons.sheetFor(64) $= "GuiEditor:controlIcons64"); + palCheck("80px art takes the 128 sheet", %icons.sheetFor(80) $= "GuiEditor:controlIcons128"); + palCheck("32px art takes the 64 sheet", %icons.sheetFor(32) $= "GuiEditor:controlIcons64"); + + palPaletteChecks(); +} + +//----------------------------------------------------------------------------- +// The palette the window actually built. +//----------------------------------------------------------------------------- + +function palPaletteChecks() +{ + %window = GuiEditor.ctrlListWindow; + %icons = GuiEditor.controlIcons; + + palCheck("the mode row was built", isObject(%window.modeRow)); + palCheck("the mode row offers two views", %window.modeRow.choiceCount == 2); + palCheck("it starts in grid mode", %window.mode $= "grid"); + + // A stock engine should have an icon for everything the palette can place, + // so the sweep finds nothing and there is no fifth group. When that is not + // true, say which class -- "5 groups" on its own sends the next person + // hunting. + %undrawn = ""; + %classes = enumerateConsoleClasses("GuiControl"); + for(%i = 0; %i < getFieldCount(%classes); %i++) + { + %name = trim(getField(%classes, %i)); + if(%icons.isPlaceableClass(%name) && !%icons.coversClass(%name) && + strstr(%undrawn, %name) == -1) + { + %undrawn = %undrawn SPC %name; + } + } + palCheck("every placeable class has an icon (" @ trim(%undrawn) @ ")", %undrawn $= ""); + palCheck("four groups were built (" @ %window.groupCount @ ")", %window.groupCount == 4); + + // Every group open, with its tiles in the inner grid rather than on the + // panel -- GuiExpandCtrl rewrites mVisible on direct children only. + %tiles = 0; + for(%g = 0; %g < %window.groupCount; %g++) + { + %group = %window.group[%g]; + palCheck("group " @ %g @ " is open", %group.getExpanded()); + palCheck("group " @ %g @ " has tiles", %group.tileCount > 0); + palCheck("group " @ %g @ " taller than its header (" @ + getWord(%group.getExtent(), 1) @ ")", + getWord(%group.getExtent(), 1) > $GuiEditorControlGroup::headerHeight); + %tiles += %group.tileCount; + } + palCheck("30 tiles across the groups (" @ %tiles @ ")", %tiles == 30); + + palModeChecks(); +} + +//----------------------------------------------------------------------------- +// Switching views, and surviving a collapse. +//----------------------------------------------------------------------------- + +function palModeChecks() +{ + %window = GuiEditor.ctrlListWindow; + %group = %window.group[0]; + %tile = %group.tile[0]; + + palCheck("grid mode hides the caption", !%tile.caption.isVisible()); + palCheck("grid mode draws 80px art (" @ getWord(%tile.icon.getExtent(), 0) @ ")", + getWord(%tile.icon.getExtent(), 0) == $GuiEditorControlTile::gridArt); + palCheck("grid mode takes the 128 sheet", %tile.icon.Image $= "GuiEditor:controlIcons128"); + + %window.setMode("rows"); + palCheck("row mode shows the caption", %tile.caption.isVisible()); + palCheck("row mode names the control, not the class", + %tile.caption.getText() $= GuiEditor.controlIcons.labelFor(%tile.key)); + palCheck("the tooltip still names the class", + %tile.tooltip $= GuiEditor.controlIcons.classFor(%tile.key)); + palCheck("row mode draws 32px art (" @ getWord(%tile.icon.getExtent(), 0) @ ")", + getWord(%tile.icon.getExtent(), 0) == $GuiEditorControlTile::rowArt); + palCheck("row mode takes the 64 sheet", %tile.icon.Image $= "GuiEditor:controlIcons64"); + palCheck("row mode is one tile per row", + %group.grid.CellSizeX >= getWord(%group.getExtent(), 0)); + + %window.setMode("grid"); + palCheck("switching back restores the grid cell", + %group.grid.CellSizeX == $GuiEditorControlGroup::gridCell); + palCheck("switching back hides the caption again", !%tile.caption.isVisible()); + + // Collapsing force-writes mVisible on the panel's direct children. The tiles + // are grandchildren, so they must come back. + %group.setExpanded(false); + %window.relayout(); + %group.setExpanded(true); + %window.relayout(); + palCheck("a tile survives its group being collapsed", %tile.isVisible()); + palCheck("the group reopened", %group.getExpanded()); + + palDropChecks(); +} + +//----------------------------------------------------------------------------- +// The two gestures. A click is a drop that never moved, and it has to reach the +// document by the same path a drag does -- that is where theming, selection and +// undo recording live. +//----------------------------------------------------------------------------- + +function palDropChecks() +{ + %window = GuiEditor.ctrlListWindow; + %root = GuiEditor.rootGui; + %before = %root.getCount(); + + %tile = palFindTile("GuiButtonCtrl"); + palCheck("found the button tile", isObject(%tile)); + %tile.onClick(); + + palCheck("clicking a tile added a control (" @ %root.getCount() @ ")", + %root.getCount() == %before + 1); + %added = %root.getObject(%root.getCount() - 1); + palCheck("it built the class the tile names", %added.getClassName() $= "GuiButtonCtrl"); + + // A drag ends with a mouse-up over the tile, which fires onClick as well -- + // a button keeps mDepressed through a drag. One gesture, one control. + %count = %root.getCount(); + %tile.dragged = true; + %tile.onClick(); + palCheck("a click that ended a drag adds nothing", %root.getCount() == %count); + palCheck("and the drag flag is cleared afterwards", !%tile.dragged); + + // The four faces of a bare GuiControl: the palette says which, so the + // applier must not fall back to guessing. + %faces = "Empty" TAB "Panel" TAB "Label" TAB "Overlay"; + for(%i = 0; %i < 4; %i++) + { + %face = getField(%faces, %i); + %tile = palFindTile("GuiControl:" @ %face); + palCheck("found the " @ %face @ " tile", isObject(%tile)); + + %tile.onClick(); + %ctrl = %root.getObject(%root.getCount() - 1); + palCheck(%face @ " dropped a bare GuiControl", %ctrl.getClassName() $= "GuiControl"); + palCheck(%face @ " wears a " @ %face @ " profile (" @ %ctrl.Profile.category @ ")", + %ctrl.Profile.category $= %face); + palCheck(%face @ " consumed its request", %ctrl.paletteCategory $= ""); + } + + echo("PAL DONE " @ $Pass @ " passed, " @ $Fail @ " failed"); + quit(); +} + +function palFindTile(%key) +{ + %window = GuiEditor.ctrlListWindow; + for(%g = 0; %g < %window.groupCount; %g++) + { + %group = %window.group[%g]; + for(%i = 0; %i < %group.tileCount; %i++) + { + if(%group.tile[%i].key $= %key) + { + return %group.tile[%i]; + } + } + } + return 0; +} From bfcf8c78d479915097d6b19556ce9f9881b2ab3e Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Wed, 29 Jul 2026 17:29:48 -0400 Subject: [PATCH 13/55] Gui Editor: undo and redo, on the machinery that was already there Ctrl+Z did nothing. The Edit menu was switched off in GuiEditor::open with a note saying these features still needed development, and twelve mutation sites in guiEditCtrl.cc carried a bare "// undo" comment marking where the recording used to be. Almost none of what was missing was transport. UndoManager, UndoAction and UndoScriptAction have been in collection/undo.h all along, fully bound to script; GuiEditCtrl owns a manager, registers it in onAdd and exposes it; the menu items and their accelerators were already wired to GuiEditor::Undo. The C++ even announces every edit it makes at the moment it makes it -- onPreEdit and onPostEdit around a drag or a handle-resize, a separate nudge pair so a run of arrow keys can be folded into one action, onTrashSelection fired deliberately before the controls reach the trash. Nothing had ever implemented a single one of those callbacks. What was absent was the layer that records; this is that layer. GuiEditorUndoRecorder owns it and does the writing itself, so a write that is not recorded is a write that did not happen. Everything the editor changes already funnelled through four places -- the properties pane's writeField, the theme applier, the dynamic fields section, GuiEditor's own menu commands -- and those now go through the recorder instead of straight to setEditFieldValue. A transaction groups whatever happens between begin and end into one action, so Set Theme is one Ctrl+Z however many hundred profile slots it fills, and a control drop is one however many things arriving involves. One action class holds an ordered list of ops, because a gesture mixes kinds: a drop is a move into the add set plus the theming plus a position. Undo replays the list backwards writing the before values, redo forwards writing the after. Deleting does not delete. GuiEditCtrl has always moved trashed controls into a SimGroup it owns and never empties, and that group is now the limbo for both directions: undoing an add puts the control there rather than freeing it, which is what leaves redo something to put back, wearing everything it had. It is also why a drop needs no geometry recorded -- the control keeps its own while it waits. Sibling rearrangement is recorded as one parent's whole child list rather than as per-control indices. Every index restored shifts the siblings around it, so a rearrangement touching several at once would depend on replay order; restoring a list cannot be got subtly wrong. Single-control add, delete and restack still use cheap index ops, and deletes are recorded descending, because undo replays backwards and only ascending re-insertion lands them all where they were. Records name objects by id, so the stack is dropped on New Gui, on opening a Gui, and on detaching a theme -- a Profile Editor revert frees the profiles the records name. Three things fought back, and each one turned out to be a thing the engine does that the recording had to answer. A selection is announced by select() and not by addSelection(). The second is the receiving half of the editor's event bus -- what the brain calls, under radio silence, once the tree or the pane has already announced -- so restoring a selection with clearSelection plus addSelection emptied the properties pane and never refilled it: the canvas drew handles round a control nothing else had heard of. GuiEditorBrain::restoreSelection makes the announcement itself, and short-circuits to a Replayed event -- the pane re-reads its rows, no rebuild -- when the wanted selection is already the current one, which is the change-a-setting-then-Ctrl+Z case. Setting an axis to center or fill hands that axis's geometry to the engine, which lays the control out then and there. Restoring the enum alone left the control sitting where centring had put it, and the two enum writes were landing as two separate steps besides. It is one action of four ops now, recorded geometry first and enums last, because ops replay in reverse for an undo and a control still on center overrides any position written under it. And a container that places its own children takes something from every child that arrives. 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. All of that 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 fields a container can take, at each end, and writes them again after the ops replay. After, because the write has to follow the move in both directions, which an ordinary field op cannot express. The engine additions are all of that same shape: announcements and access that were not there. GuiTreeViewCtrl::reorderFromDrag rearranged the real control hierarchy and told nobody, so it gets a pre and post pair -- the only document mutation that was neither menu-initiated nor already announced. GuiControl gains a childrenReordered binding, because reorderChild and the SimSet ordering methods change the list silently: a control put back into a chain kept the slot it briefly held at the end of the list, and the layout was stale rather than the order wrong. GuiTabBookCtrl gains a childrenReordered of its own, since its tab strip is drawn from a page vector independent of the children and it had no way to be told at all -- which also fixes reordering tabs by dragging in the tree, which never updated the strip either. GuiFrameSetCtrl gains getFrameLayout and setFrameLayout. It keeps its layout in a tree beside the child list and destroys a frame outright when the control in it is removed, so deleting a control collapsed the split and the sibling swallowed the space; the tree was built once in onAdd from dynamic fields, then cleared, and could afterwards be neither read nor written. It serialises one record per frame, parent before children, and names each frame's control by object id rather than by child index -- so a tree recorded before a delete restores with that frame standing empty, waiting for the control the undo is about to put back, instead of swallowing whichever control now occupies that slot. The rebuild goes through the same splitFrame call loadFrame makes, so a tree built from text is built the way one read from a file is. The Edit menu is switched on, greying Undo and Redo from the stacks. Cut, Copy and Paste are still empty functions and are greyed deliberately, so they read as unavailable rather than broken. Menu items cannot be relabelled from script, so getNextUndoName has no reader yet. tests/smoke/undo.cs covers it in 135 checks, and asserts stack depth as often as values: a gesture recording eleven steps is as broken as one recording none, and only the count says so. Typing a caption is one step, a run of nudges is one, a nudge with an edit between two of them is three. Its last step opens the real editor UI, which is how the container work got verified at all. smEditorHandle is only set in GuiEditCtrl::onWake, and calling GuiEditor.open() registers the editor without putting it on the canvas -- so the brain never wakes, isEditMode() is false throughout, and the first version of the chain test passed the very check it existed to make. Every suite here runs outside edit mode unless it says otherwise, which is worth knowing about the coverage that was already in place. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qi2pZ9UzF5rsLCNVErpzwt --- editor/EditorCore/EditorCore.cs | 18 +- editor/GuiEditor/GuiEditor.cs | 179 +++- editor/GuiEditor/scripts/GuiEditorBrain.cs | 114 +++ .../scripts/GuiEditorDynamicFields.cs | 7 +- .../scripts/GuiEditorExplorerTree.cs | 15 + .../scripts/GuiEditorInspectorPane.cs | 47 +- .../scripts/GuiEditorInspectorWindow.cs | 13 + .../scripts/GuiEditorThemeApplier.cs | 7 +- .../GuiEditor/scripts/GuiEditorUndoAction.cs | 478 +++++++++ .../scripts/GuiEditorUndoRecorder.cs | 784 ++++++++++++++ .../source/gui/containers/guiFrameSetCtrl.cc | 155 +++ .../source/gui/containers/guiFrameSetCtrl.h | 7 + .../guiFrameSetCtrl_ScriptBinding.h | 27 + .../source/gui/containers/guiTabBookCtrl.cc | 40 + engine/source/gui/containers/guiTabBookCtrl.h | 1 + engine/source/gui/editor/guiEditCtrl.cc | 32 +- engine/source/gui/guiControl_ScriptBinding.h | 15 + engine/source/gui/guiTreeViewCtrl.cc | 9 + tests/run.ps1 | 2 +- tests/smoke/undo.cs | 967 ++++++++++++++++++ 20 files changed, 2883 insertions(+), 34 deletions(-) create mode 100644 editor/GuiEditor/scripts/GuiEditorUndoAction.cs create mode 100644 editor/GuiEditor/scripts/GuiEditorUndoRecorder.cs create mode 100644 tests/smoke/undo.cs diff --git a/editor/EditorCore/EditorCore.cs b/editor/EditorCore/EditorCore.cs index e5f89b52b..1ef4e03cc 100644 --- a/editor/EditorCore/EditorCore.cs +++ b/editor/EditorCore/EditorCore.cs @@ -202,46 +202,46 @@ new GuiMenuItemCtrl() { Text = "-"; }; new GuiMenuItemCtrl() { Text = "Align Top"; - Command = "GuiEditor.brain.Justify(3);"; + Command = "GuiEditor.Justify(3);"; Accelerator = "Ctrl T"; }; new GuiMenuItemCtrl() { Text = "Align Bottom"; - Command = "GuiEditor.brain.Justify(4);"; + Command = "GuiEditor.Justify(4);"; Accelerator = "Ctrl B"; }; new GuiMenuItemCtrl() { Text = "Align Left"; - Command = "GuiEditor.brain.Justify(0);"; + Command = "GuiEditor.Justify(0);"; Accelerator = "Ctrl L"; }; new GuiMenuItemCtrl() { Text = "Align Right"; - Command = "GuiEditor.brain.Justify(2);"; + Command = "GuiEditor.Justify(2);"; Accelerator = "Ctrl R"; }; new GuiMenuItemCtrl() { Text = "-"; }; new GuiMenuItemCtrl() { Text = "Center Horizontally"; - Command = "GuiEditor.brain.Justify(1);"; + Command = "GuiEditor.Justify(1);"; }; new GuiMenuItemCtrl() { Text = "Space Vertically"; - Command = "GuiEditor.brain.Justify(5);"; + Command = "GuiEditor.Justify(5);"; }; new GuiMenuItemCtrl() { Text = "Space Horizontally"; - Command = "GuiEditor.brain.Justify(6);"; + Command = "GuiEditor.Justify(6);"; }; new GuiMenuItemCtrl() { Text = "-"; }; new GuiMenuItemCtrl() { Text = "Bring to Front"; - Command = "GuiEditor.brain.BringToFront();"; + Command = "GuiEditor.BringToFront();"; Accelerator = "Ctrl-Shift Up"; }; new GuiMenuItemCtrl() { Text = "Push to Back"; - Command = "GuiEditor.brain.PushToBack();"; + Command = "GuiEditor.PushToBack();"; Accelerator = "Ctrl-Shift Down"; }; new GuiMenuItemCtrl() { Text = "-"; }; diff --git a/editor/GuiEditor/GuiEditor.cs b/editor/GuiEditor/GuiEditor.cs index ffcce5e90..56bd7f679 100644 --- a/editor/GuiEditor/GuiEditor.cs +++ b/editor/GuiEditor/GuiEditor.cs @@ -61,6 +61,12 @@ exec("./scripts/GuiEditorDynamicFields.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"); + %this.guiPage = EditorCore.RegisterEditor("Gui Editor", %this); // What the control palette can offer and what each entry looks like. Built @@ -91,6 +97,15 @@ 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; + }; + %this.content = %this.createFrameSet(); %this.brain = new GuiEditCtrl() @@ -335,6 +350,15 @@ class = "SimulatedCanvas"; { %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(); + } } function GuiEditor::open(%this, %content) @@ -347,9 +371,17 @@ class = "SimulatedCanvas"; } EditorCore.menuBar.setMenuActive("File", true); - //EditorCore.menuBar.setMenuActive("Edit", true); //These features still need development + EditorCore.menuBar.setMenuActive("Edit", true); EditorCore.menuBar.setMenuActive("Layout", true); EditorCore.menuBar.setMenuActive("Select", true); + + // Undo and Redo are greyed from the stacks; the other three items on the Edit + // menu are still stubs and must not look live. + EditorCore.menuBar.setMenuActive("Cut", false); + EditorCore.menuBar.setMenuActive("Copy", false); + EditorCore.menuBar.setMenuActive("Paste", false); + %this.undoRecorder.forceRefreshMenu(); + editorMode(true); } @@ -374,6 +406,9 @@ class = "SimulatedCanvas"; %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(); @@ -436,6 +471,9 @@ class = "SimulatedCanvas"; %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; @@ -563,7 +601,11 @@ class = "GuiEditorThemeDialog"; %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 @@ -646,6 +688,11 @@ class = "GuiEditorThemeDialog"; // 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(); + %this.themeApplier.detach(%this.rootGui, %theme, %profile); } @@ -656,7 +703,13 @@ class = "GuiEditorThemeDialog"; %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(); } } @@ -731,19 +784,88 @@ class = "GuiEditorThemeDialog"; %this.module = %module; } +//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(); + + %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::selectAfterReplay(%this, %list) +{ + %wanted = ""; + + for(%i = 0; %i < getWordCount(%list); %i++) + { + %ctrl = getWord(%list, %i); - %count = %undoManager.getRedoCount(); + // 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; } function GuiEditor::Cut(%this) @@ -761,17 +883,70 @@ class = "GuiEditorThemeDialog"; } +//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) { %width = 300; diff --git a/editor/GuiEditor/scripts/GuiEditorBrain.cs b/editor/GuiEditor/scripts/GuiEditorBrain.cs index b21ce25d9..2eba9fd38 100644 --- a/editor/GuiEditor/scripts/GuiEditorBrain.cs +++ b/editor/GuiEditor/scripts/GuiEditorBrain.cs @@ -28,6 +28,7 @@ %x = getWord(%pos, 0); %y = getWord(%pos, 1); + // The add itself is the undo step, recorded from onAddNewCtrl below. %this.addNewCtrl(%payload); // A control arrives wearing whatever its C++ constructor named - a @@ -42,10 +43,18 @@ // 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(%payload, %theme, false); + GuiEditor.undoRecorder.resume(); + %this.postEvent("Rethemed", %payload); } @@ -150,6 +159,111 @@ %this.toggleMenuItems(); } +//----------------------------------------------------------------------------- +// 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. +function GuiEditorBrain::onPreEdit(%this, %selection) +{ + GuiEditor.undoRecorder.snapshot(%selection); +} + +function GuiEditorBrain::onPostEdit(%this, %selection) +{ + GuiEditor.undoRecorder.commitGeometry("", ""); +} + +// 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, ""); +} + +// The same for a whole set of them, which is how a loaded selection arrives. +function GuiEditorBrain::onAddNewCtrlSet(%this, %selection) +{ + GuiEditor.undoRecorder.begin("Add Controls", ""); + for(%i = 0; %i < %selection.getCount(); %i++) + { + GuiEditor.undoRecorder.recordAdd(%selection.getObject(%i), ""); + } + GuiEditor.undoRecorder.end(); +} + +// Put the selection on the controls a replay changed, and tell everyone. +// +// 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::restoreSelection(%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; +} + function GuiEditorBrain::toggleMenuItems(%this) { %count = %this.getSelected().getCount(); diff --git a/editor/GuiEditor/scripts/GuiEditorDynamicFields.cs b/editor/GuiEditor/scripts/GuiEditorDynamicFields.cs index 17b569c75..dbaf90886 100644 --- a/editor/GuiEditor/scripts/GuiEditorDynamicFields.cs +++ b/editor/GuiEditor/scripts/GuiEditorDynamicFields.cs @@ -189,8 +189,11 @@ class = "GuiProfileEditorFieldRow"; 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(); - %this.target.setFieldValue(%row.fieldName, %value); + GuiEditor.undoRecorder.writeDynamicField(%this.target, %row.fieldName, %value); %row.markClean(); %this.pane.afterCommit(); @@ -218,7 +221,7 @@ class = "GuiProfileEditorFieldRow"; return; } - %this.target.setFieldValue(%row.fieldName, ""); + GuiEditor.undoRecorder.writeDynamicField(%this.target, %row.fieldName, ""); %this.pane.afterCommit(); // Deferred: this call arrives from the button inside the row that is about diff --git a/editor/GuiEditor/scripts/GuiEditorExplorerTree.cs b/editor/GuiEditor/scripts/GuiEditorExplorerTree.cs index 0b519a1fe..d3db4fd96 100644 --- a/editor/GuiEditor/scripts/GuiEditorExplorerTree.cs +++ b/editor/GuiEditor/scripts/GuiEditorExplorerTree.cs @@ -43,6 +43,21 @@ %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) +{ + GuiEditor.undoRecorder.commitHierarchy("Reparent Control"); +} + function GuiEditorExplorerTree::onPostApply(%this, %obj) { %index = %this.findItemID(%obj.getId()); diff --git a/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs b/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs index f22ec03c0..4671ef85d 100644 --- a/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs +++ b/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs @@ -947,6 +947,21 @@ class = "GuiProfileEditorFieldRow"; 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, @@ -966,6 +981,20 @@ class = "GuiProfileEditorFieldRow"; %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(); } @@ -1061,10 +1090,15 @@ class = "GuiProfileEditorFieldRow"; function GuiEditorInspectorPane::writeField(%this, %field, %value) { - // setEditFieldValue rather than a plain assignment: it brackets the write - // with inspectPreApply / inspectPostApply, which is what lets a control - // react to being re-profiled or resized from here. - %this.target.setEditFieldValue(%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) @@ -1235,7 +1269,9 @@ class = "GuiProfileEditorFieldRow"; // 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 @@ -1517,8 +1553,11 @@ class = "GuiProfileEditorFieldRow"; 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); diff --git a/editor/GuiEditor/scripts/GuiEditorInspectorWindow.cs b/editor/GuiEditor/scripts/GuiEditorInspectorWindow.cs index 6f375222e..56a9c4d96 100644 --- a/editor/GuiEditor/scripts/GuiEditorInspectorWindow.cs +++ b/editor/GuiEditor/scripts/GuiEditorInspectorWindow.cs @@ -83,6 +83,19 @@ class = "GuiEditorInspectorPane"; } } +// 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 diff --git a/editor/GuiEditor/scripts/GuiEditorThemeApplier.cs b/editor/GuiEditor/scripts/GuiEditorThemeApplier.cs index 29d89c512..4b25e3b68 100644 --- a/editor/GuiEditor/scripts/GuiEditorThemeApplier.cs +++ b/editor/GuiEditor/scripts/GuiEditorThemeApplier.cs @@ -240,6 +240,11 @@ 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 @@ -253,7 +258,7 @@ // 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. - %ctrl.setEditFieldValue(%field, %target.getId()); + GuiEditor.undoRecorder.writeField(%ctrl, %field, %target.getId()); %changed++; } diff --git a/editor/GuiEditor/scripts/GuiEditorUndoAction.cs b/editor/GuiEditor/scripts/GuiEditorUndoAction.cs new file mode 100644 index 000000000..2018cf72a --- /dev/null +++ b/editor/GuiEditor/scripts/GuiEditorUndoAction.cs @@ -0,0 +1,478 @@ + +//----------------------------------------------------------------------------- +// 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. +// 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; +} + +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 + { + %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 or an order 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" || %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. + if(isObject(%this.recorder)) + { + %this.recorder.noteReplay(%this); + } + + 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; + } + + 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..4edc0124f --- /dev/null +++ b/editor/GuiEditor/scripts/GuiEditorUndoRecorder.cs @@ -0,0 +1,784 @@ + +//----------------------------------------------------------------------------- +// 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 +// two 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. +// snapshot + commitGeometry a gesture whose result is only known +// when it ends - a drag, a run of nudges. +// The C++ edit control brackets both with +// callbacks (onPreEdit/onPostEdit, +// onPreSelectionNudged/...) which is 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.hierCount = 0; + %this.hierCtrlCount = 0; + %this.watchCount = 0; + %this.replayTouched = ""; + + // What the Edit menu was last told, so a run of nudges does not walk the + // whole menu tree once per key. + %this.menuUndo = -1; + %this.menuRedo = -1; +} + +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(); + return; + } + + // Now that the operation is over, what any frame set involved in it ended up + // looking like. + %this.emitFrameFixes(%action); + + %action.addToManager(%this.manager()); + %this.lastAction = %action; + %this.lastKind = %this.pendingKind; + %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(); +} + +//----------------------------------------------------------------------------- +// 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); +} + +// 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; + if(%this.suspended || !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.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; +} + +//----------------------------------------------------------------------------- +// 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. Two jobs: hand GuiEditor the +// controls to re-select afterwards, and drop the coalescing state - the action +// on top of the undo stack is no longer the one a merge would be aiming at. +function GuiEditorUndoRecorder::noteReplay(%this, %action) +{ + %this.replayTouched = %action.touched(); + %this.lastAction = 0; + %this.lastKind = ""; +} + +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; + %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; +} + +// setMenuActive walks the whole menu tree by item text and re-applies every +// item's profile, so it is worth knowing what it was last told: a run of +// nudges would otherwise do that once per key press. +function GuiEditorUndoRecorder::refreshMenu(%this) +{ + %undo = %this.undoCount(); + %redo = %this.redoCount(); + + if(%undo == %this.menuUndo && %redo == %this.menuRedo) + { + return; + } + + %this.menuUndo = %undo; + %this.menuRedo = %redo; + + if(isObject(EditorCore) && isObject(EditorCore.menuBar)) + { + EditorCore.menuBar.setMenuActive("Undo", %undo > 0); + EditorCore.menuBar.setMenuActive("Redo", %redo > 0); + } +} + +// The menu is rebuilt-looking every time the editor is opened, and the cache +// above would otherwise decide there was nothing to say. +function GuiEditorUndoRecorder::forceRefreshMenu(%this) +{ + %this.menuUndo = -1; + %this.menuRedo = -1; + %this.refreshMenu(); +} diff --git a/engine/source/gui/containers/guiFrameSetCtrl.cc b/engine/source/gui/containers/guiFrameSetCtrl.cc index d748a001d..4b64f8c70 100644 --- a/engine/source/gui/containers/guiFrameSetCtrl.cc +++ b/engine/source/gui/containers/guiFrameSetCtrl.cc @@ -513,6 +513,161 @@ void GuiFrameSetCtrl::loadFrame(GuiFrameSetCtrl::Frame* frame, const U32 frameID } } +//----------------------------------------------------------------------------- +// The frame tree as text. +// +// A frame set keeps its layout in a tree beside the child list, and destroys a +// frame outright when the control in it is removed (onChildRemoved below), so +// deleting a control collapses the split it was in. Nothing could put that +// back: the tree is built once in onAdd from dynamic fields, which are then +// cleared, and there was no way to read it or write it afterwards. That made a +// frame set the one container the Gui Editor's undo could not fully restore. +// +// One record per frame, a parent always before its children, eight numbers +// each: +// +// id child1 child2 isVertical extentX extentY isAnchored controlID +// +// The control is named by object id rather than by its index among the +// children, so that restoring a tree recorded before a delete - one naming a +// control that is now sitting in the editor's trash - simply leaves that frame +// empty rather than putting the wrong control in it. +//----------------------------------------------------------------------------- + +const char* GuiFrameSetCtrl::getFrameLayout() +{ + char* buffer = Con::getReturnBuffer(4096); + buffer[0] = '\0'; + + appendFrameLayout(&mRootFrame, buffer, 4096); + + return buffer; +} + +void GuiFrameSetCtrl::appendFrameLayout(GuiFrameSetCtrl::Frame* frame, char* buffer, const U32 size) +{ + char record[128]; + dSprintf(record, sizeof(record), "%d %d %d %d %d %d %d %d ", + frame->id, + frame->child1 ? frame->child1->id : 0, + frame->child2 ? frame->child2->id : 0, + frame->isVertical ? 1 : 0, + frame->extent.x, + frame->extent.y, + frame->isAnchored ? 1 : 0, + frame->control ? frame->control->getId() : 0); + + if ((dStrlen(buffer) + dStrlen(record)) >= size) + { + Con::warnf("GuiFrameSetCtrl::getFrameLayout - frame tree is too large to write out"); + return; + } + dStrcat(buffer, record); + + if (frame->child1) + { + appendFrameLayout(frame->child1, buffer, size); + } + if (frame->child2) + { + appendFrameLayout(frame->child2, buffer, size); + } +} + +void GuiFrameSetCtrl::setFrameLayout(const char* layout) +{ + Vector values; + for (const char* at = layout; *at; ) + { + while (*at == ' ' || *at == '\t') + at++; + if (!*at) + break; + + values.push_back((U32)dAtoi(at)); + + while (*at && *at != ' ' && *at != '\t') + at++; + } + + if (values.size() < 8 || (values.size() % 8) != 0) + { + Con::warnf("GuiFrameSetCtrl::setFrameLayout - expected a multiple of eight values, got %d", values.size()); + return; + } + + // Back to a single frame. deleteChildren frees the subtree but leaves the + // pointers as they were, so they have to be cleared here or the rebuild + // would walk into freed frames. + mRootFrame.deleteChildren(); + mRootFrame.child1 = nullptr; + mRootFrame.child2 = nullptr; + mRootFrame.control = nullptr; + + buildFrameLayout(&mRootFrame, values[0], values); + + // Frames created from here on must not reuse an id the tree already holds. + for (U32 i = 0; i < (U32)values.size(); i += 8) + { + if (values[i] > mNextFrameID) + { + mNextFrameID = values[i]; + } + } + + resize(getPosition(), getExtent()); +} + +void GuiFrameSetCtrl::buildFrameLayout(GuiFrameSetCtrl::Frame* frame, const U32 frameID, const Vector& values) +{ + S32 at = -1; + for (U32 i = 0; i < (U32)values.size(); i += 8) + { + if (values[i] == frameID) + { + at = (S32)i; + break; + } + } + + if (at < 0) + { + return; + } + + frame->id = frameID; + frame->isVertical = values[at + 3] != 0; + frame->extent.set(values[at + 4], values[at + 5]); + frame->isAnchored = values[at + 6] != 0; + frame->control = nullptr; + + const U32 child1ID = values[at + 1]; + const U32 child2ID = values[at + 2]; + + if (child1ID && child2ID) + { + // The same call loadFrame makes, and for the same reason: it is what + // builds a pair of frames under this one. + splitFrame(frame, frame->isVertical ? GuiDirection::Up : GuiDirection::Left); + + buildFrameLayout(frame->child1, child1ID, values); + buildFrameLayout(frame->child2, child2ID, values); + return; + } + + // A leaf, so it may hold a control - but only one that is still a child of + // this frame set. + const U32 controlID = values[at + 7]; + if (controlID) + { + GuiControl* ctrl; + if (Sim::findObject(controlID, ctrl) && ctrl->getGroup() == this) + { + frame->control = ctrl; + } + } +} + void GuiFrameSetCtrl::onChildAdded(GuiControl* child) { //Ensure the child isn't positioned to the center diff --git a/engine/source/gui/containers/guiFrameSetCtrl.h b/engine/source/gui/containers/guiFrameSetCtrl.h index 7b605e5f0..4c6437325 100644 --- a/engine/source/gui/containers/guiFrameSetCtrl.h +++ b/engine/source/gui/containers/guiFrameSetCtrl.h @@ -105,6 +105,13 @@ class GuiFrameSetCtrl : public GuiEasingSupport bool onAdd(); virtual void parentResized(const Point2I& oldParentExtent, const Point2I& newParentExtent); void loadFrame(GuiFrameSetCtrl::Frame* frame, const U32 frameID); + + // The frame tree as text, readable and writable at runtime. See the note on + // getFrameLayout in the .cc for why this exists. + const char* getFrameLayout(); + void setFrameLayout(const char* layout); + void appendFrameLayout(GuiFrameSetCtrl::Frame* frame, char* buffer, const U32 size); + void buildFrameLayout(GuiFrameSetCtrl::Frame* frame, const U32 frameID, const Vector& values); void resize(const Point2I& newPosition, const Point2I& newExtent); void inspectPostApply(); bool onWake(); diff --git a/engine/source/gui/containers/guiFrameSetCtrl_ScriptBinding.h b/engine/source/gui/containers/guiFrameSetCtrl_ScriptBinding.h index 4c3b883bc..9866d45df 100644 --- a/engine/source/gui/containers/guiFrameSetCtrl_ScriptBinding.h +++ b/engine/source/gui/containers/guiFrameSetCtrl_ScriptBinding.h @@ -67,4 +67,31 @@ ConsoleMethodWithDocs(GuiFrameSetCtrl, setFrameSize, ConsoleVoid, 4, 4, (int fra object->setFrameSize(dAtoi(argv[2]), dAtoi(argv[3])); } +/*! Gets the frame tree as text: the splits, their sizes, and which control sits + in each frame. Opaque - hand it back to setFrameLayout unchanged. + + A frame set destroys a frame when the control in it is removed, so anything + that needs to put a removed control back where it was - the Gui Editor's + undo - has to keep the layout first. + @return The frame tree, as a string. +*/ +ConsoleMethodWithDocs(GuiFrameSetCtrl, getFrameLayout, ConsoleString, 2, 2, ()) +{ + return object->getFrameLayout(); +} + +/*! Rebuilds the frame tree from text taken earlier by getFrameLayout, and lays + the children out again. + + A frame naming a control that is no longer a child of this frame set comes + back empty, so a layout recorded before a delete can be restored before the + control is. + @param layout A string from getFrameLayout. + @return No return value. +*/ +ConsoleMethodWithDocs(GuiFrameSetCtrl, setFrameLayout, ConsoleVoid, 3, 3, (layout)) +{ + object->setFrameLayout(argv[2]); +} + ConsoleMethodGroupEndWithDocs(GuiFrameSetCtrl) \ No newline at end of file diff --git a/engine/source/gui/containers/guiTabBookCtrl.cc b/engine/source/gui/containers/guiTabBookCtrl.cc index ec1c42487..1971a22d7 100755 --- a/engine/source/gui/containers/guiTabBookCtrl.cc +++ b/engine/source/gui/containers/guiTabBookCtrl.cc @@ -127,6 +127,46 @@ void GuiTabBookCtrl::onChildRemoved( GuiControl* child ) } +// The tab strip is drawn from mPages, which is filled in the order pages are +// added and is otherwise independent of the child list. Anything that +// rearranges the children - a drag in the Gui Editor's tree, or an undo putting +// a deleted page back where it came from - therefore leaves a page sitting in +// the middle of the children and at the end of the tab strip. The children are +// the truth, so rebuild the order from them. +void GuiTabBookCtrl::childrenReordered() +{ + Vector ordered; + + for (iterator i = begin(); i != end(); i++) + { + GuiTabPageCtrl* page = dynamic_cast(*i); + if (!page) + continue; + + for (S32 p = 0; p < mPages.size(); p++) + { + if (mPages[p].Page == page) + { + ordered.push_back(mPages[p]); + break; + } + } + } + + // Anything the children did not account for is a page the book believes in + // and the child list does not; dropping it here would leak it out of the + // strip, so only take the rebuild when the two agree on the count. + if (ordered.size() != mPages.size()) + return; + + mPages.clear(); + for (S32 p = 0; p < ordered.size(); p++) + mPages.push_back(ordered[p]); + + calculatePageTabs(); + Parent::childrenReordered(); +} + void GuiTabBookCtrl::onChildAdded( GuiControl *child ) { GuiTabPageCtrl *page = dynamic_cast(child); diff --git a/engine/source/gui/containers/guiTabBookCtrl.h b/engine/source/gui/containers/guiTabBookCtrl.h index 60a308304..8e3c5f0d1 100755 --- a/engine/source/gui/containers/guiTabBookCtrl.h +++ b/engine/source/gui/containers/guiTabBookCtrl.h @@ -134,6 +134,7 @@ class GuiTabBookCtrl : public GuiControl /// @{ void onChildRemoved( GuiControl* child ); void onChildAdded( GuiControl *child ); + void childrenReordered(); /// @} /// @name Rendering methods diff --git a/engine/source/gui/editor/guiEditCtrl.cc b/engine/source/gui/editor/guiEditCtrl.cc index a5a2b9549..bb95a9640 100755 --- a/engine/source/gui/editor/guiEditCtrl.cc +++ b/engine/source/gui/editor/guiEditCtrl.cc @@ -30,6 +30,17 @@ #include "io/fileStream.h" #include "gui/containers/guiScrollCtrl.h" +// Undo lives in script (editor/GuiEditor/scripts/GuiEditorUndoRecorder.cs), on +// the UndoManager this class owns. What is here is the announcements it needs: +// every edit made below is bracketed by a callback, and the paired ones - +// onPreEdit/onPostEdit for a drag or a handle-resize, onPreSelectionNudged and +// its post for a run of arrow keys - are pairs because what a gesture did is +// only known once it ends. onTrashSelection deliberately fires before the +// controls reach the trash, while each one still knows where it came from. +// +// The three edits with no callback - justifySelection, bringToFront and +// pushToBack - are reached only from the Layout menu, and the Gui Editor's +// script wraps those commands to record either side of the call. IMPLEMENT_CONOBJECT(GuiEditCtrl); GuiEditCtrl::GuiEditCtrl() : mCurrentAddSet(NULL), mEditorRoot(NULL), mGridSnap(10, 10), mDragBeginPoint(-1, -1) @@ -360,7 +371,6 @@ void GuiEditCtrl::addNewControl(GuiControl* ctrl) mSelectedControls.push_back(ctrl); Con::executef(this, 2, "onAddSelected", Con::getIntArg(ctrl->getId())); //} - // undo Con::executef(this, 2, "onAddNewCtrl", Con::getIntArg(ctrl->getId())); } @@ -775,7 +785,6 @@ void GuiEditCtrl::onTouchDown(const GuiEvent& event) if ((mSizingMode = (GuiEditCtrl::sizingModes)getSizingHitKnobs(mLastMousePos, box)) != 0) { mMouseDownMode = SizingSelection; - // undo Con::executef(this, 2, "onPreEdit", Con::getIntArg(getSelectedSet().getId())); return; } @@ -836,7 +845,6 @@ void GuiEditCtrl::onTouchDown(const GuiEvent& event) // Set Mouse Mode mMouseDownMode = MovingSelection; - // undo Con::executef(this, 2, "onPreEdit", Con::getIntArg(getSelectedSet().getId())); } } @@ -968,7 +976,6 @@ void GuiEditCtrl::onTouchUp(const GuiEvent& event) // deliver post edit event if we've been editing // note: paxorr: this may need to be moved earlier, if the selection has changed. - // undo if (mMouseDownMode == SizingSelection || mMouseDownMode == MovingSelection) Con::executef(this, 2, "onPostEdit", Con::getIntArg(getSelectedSet().getId())); @@ -1224,7 +1231,6 @@ void GuiEditCtrl::moveAndSnapSelection(const Point2I& delta) { // move / nudge gets a special callback so that multiple small moves can be // coalesced into one large undo action. - // undo Con::executef(this, 2, "onPreSelectionNudged", Con::getIntArg(getSelectedSet().getId())); Vector::iterator i; @@ -1236,7 +1242,6 @@ void GuiEditCtrl::moveAndSnapSelection(const Point2I& delta) (*i)->resize(newPos, (*i)->mBounds.extent); } - // undo Con::executef(this, 2, "onPostSelectionNudged", Con::getIntArg(getSelectedSet().getId())); // allow script to update the inspector @@ -1247,8 +1252,7 @@ void GuiEditCtrl::moveAndSnapSelection(const Point2I& delta) void GuiEditCtrl::moveSelection(const Point2I& delta) { // move / nudge gets a special callback so that multiple small moves can be - // coalesced into one large undo action. - // undo + // coalesced into one large undo action. Con::executef(this, 2, "onPreSelectionNudged", Con::getIntArg(getSelectedSet().getId())); Vector::iterator i; @@ -1261,7 +1265,6 @@ void GuiEditCtrl::moveSelection(const Point2I& delta) (*i)->resize((*i)->mBounds.point + delta, (*i)->mBounds.extent); } - // undo Con::executef(this, 2, "onPostSelectionNudged", Con::getIntArg(getSelectedSet().getId())); // allow script to update the inspector @@ -1369,7 +1372,8 @@ void GuiEditCtrl::justifySelection(Justification j) void GuiEditCtrl::deleteSelection(void) { - // undo + // Before the move below, while each control still knows the parent and the + // index it would have to go back to. Con::executef(this, 2, "onTrashSelection", Con::getIntArg(getSelectedSet().getId())); Vector::iterator i; @@ -1404,7 +1408,6 @@ void GuiEditCtrl::loadSelection(const char* filename) Con::executef(this, 2, "onAddSelected", Con::getIntArg(ctrl->getId())); } } - // Undo Con::executef(this, 2, "onAddNewCtrlSet", Con::getIntArg(getSelectedSet().getId())); } set->deleteObject(); @@ -1456,9 +1459,10 @@ void GuiEditCtrl::selectAll() } } +// No callback: the Layout menu is the only way in, and the Gui Editor's script +// records around its own command (GuiEditor::BringToFront). void GuiEditCtrl::bringToFront() { - // undo if (mSelectedControls.size() != 1) return; @@ -1466,9 +1470,9 @@ void GuiEditCtrl::bringToFront() mCurrentAddSet->pushObjectToBack(ctrl); } +// As above: recorded by GuiEditor::PushToBack. void GuiEditCtrl::pushToBack() { - // undo if (mSelectedControls.size() != 1) return; @@ -1544,13 +1548,11 @@ void GuiEditCtrl::setSnapToGrid(U32 gridsize) void GuiEditCtrl::controlInspectPreApply(GuiControl* object) { - // undo Con::executef(this, 2, "onControlInspectPreApply", Con::getIntArg(object->getId())); } void GuiEditCtrl::controlInspectPostApply(GuiControl* object) { - // undo Con::executef(this, 2, "onControlInspectPostApply", Con::getIntArg(object->getId())); } diff --git a/engine/source/gui/guiControl_ScriptBinding.h b/engine/source/gui/guiControl_ScriptBinding.h index 6e1544e0f..cea1940f1 100644 --- a/engine/source/gui/guiControl_ScriptBinding.h +++ b/engine/source/gui/guiControl_ScriptBinding.h @@ -75,6 +75,21 @@ ConsoleMethodWithDocs(GuiControl, reorderChild, ConsoleVoid, 4,4, (child1, chil } } +/*! Tell this control that the order of its children has changed, so that one + which lays its children out - a chain, a grid, a frame set - places them + again. + + reorderChild and the SimSet ordering methods change the list and announce + nothing, so anything that rearranges children from script has to say so + afterwards. Adding and removing a child already notify on their own; this is + for the case where the same children are simply in a different order. + @return No return value +*/ +ConsoleMethodWithDocs(GuiControl, childrenReordered, ConsoleVoid, 2, 2, ()) +{ + object->childrenReordered(); +} + /*! @return Returns the Id of the parent control */ ConsoleMethodWithDocs( GuiControl, getParent, ConsoleInt, 2, 2, ()) diff --git a/engine/source/gui/guiTreeViewCtrl.cc b/engine/source/gui/guiTreeViewCtrl.cc index c0abddbad..c88754b2f 100755 --- a/engine/source/gui/guiTreeViewCtrl.cc +++ b/engine/source/gui/guiTreeViewCtrl.cc @@ -284,6 +284,13 @@ void GuiTreeViewCtrl::reorderFromDrag() return; } + // Past every bail, so the rearrangement below is certain to happen. A drop + // can move any number of selected items into any number of containers, and + // the only record of where they came from is the hierarchy itself - hence a + // pair, rather than one callback afterwards. The Gui Editor's tree listens + // to both and turns the difference into an undo step. + Con::executef(this, 1, "onPreReorder"); + vector objectAboveTargetList; if (mReorderMethod != ReorderMethod::Insert) { @@ -335,6 +342,8 @@ void GuiTreeViewCtrl::reorderFromDrag() control->childrenReordered(); } + Con::executef(this, 1, "onPostReorder"); + refreshTree(); } diff --git a/tests/run.ps1 b/tests/run.ps1 index cdd86a2f5..75802885c 100644 --- a/tests/run.ps1 +++ b/tests/run.ps1 @@ -73,7 +73,7 @@ $KeepProject = @('bitmapPathRead') $Order = @( 'profileEditor', 'profileForm', 'border', 'borderPane', 'standalone', 'headerPane', 'colorPopup', 'themeApply', 'font', 'assetPicker', - 'tooltipProfile', 'textClick', 'bitmapPathWrite', 'bitmapPathRead', + 'tooltipProfile', 'textClick', 'undo', 'bitmapPathWrite', 'bitmapPathRead', 'toybox', 'planetX' ) diff --git a/tests/smoke/undo.cs b/tests/smoke/undo.cs new file mode 100644 index 000000000..a99ed8d86 --- /dev/null +++ b/tests/smoke/undo.cs @@ -0,0 +1,967 @@ +//----------------------------------------------------------------------------- +// Undo / redo in the Gui Editor. Boots the editor, opens the PlanetX project so +// there is a real theme to work with, and puts every kind of edit through a +// round trip: does undo put it back, does redo do it again, and - the part that +// is easy to get wrong - is one thing the user did one step on the stack. +// +// Stack depth is checked as often as the values are. A gesture that records +// eleven steps is as broken as one that records none, and only the count says +// so. Run: tests/run.ps1 undo ; grep UNDO in console.log. +//----------------------------------------------------------------------------- + +setLogMode(2); +setScriptExecEcho(false); +trace(false); +$Scripts::ignoreDSOs = true; +setCompanyAndProduct("Torque Game Engines", "Torque2D"); +ModuleDatabase.EchoInfo = false; +AssetDatabase.EchoInfo = false; +AssetDatabase.IgnoreAutoUnload = true; + +testExec("editor/main.cs"); + +function uCheck(%label, %condition) +{ + echo(%condition ? ("UNDO PASS: " @ %label) : ("UNDO FAIL: " @ %label)); +} + +function uUndoCount() +{ + return GuiEditor.undoRecorder.undoCount(); +} + +function uRedoCount() +{ + return GuiEditor.undoRecorder.redoCount(); +} + +// Every pane write needs a bound target, and a replay moves the selection - so +// the pane is re-bound before each case rather than once. +function uBind(%ctrl) +{ + GuiEditor.inspectorWindow.pane.bind(%ctrl); + return GuiEditor.inspectorWindow.pane; +} + +function uIndexOf(%parent, %ctrl) +{ + for(%i = 0; %i < %parent.getCount(); %i++) + { + if(%parent.getObject(%i) == %ctrl) + { + return %i; + } + } + return -1; +} + +function uSelect(%ctrl) +{ + GuiEditor.brain.clearSelection(); + GuiEditor.brain.select(%ctrl); +} + +// Does the control really carry this dynamic field? Comparing against "" proves +// nothing: an absent field and an empty one read back the same, because an empty +// one is exactly what the engine deletes. +function uHasDynamicField(%ctrl, %name) +{ + for(%i = 0; %i < %ctrl.getDynamicFieldCount(); %i++) + { + if(getWord(%ctrl.getDynamicField(%i), 0) $= %name) + { + return true; + } + } + return false; +} + +schedule(2000, 0, "uStep1"); + +function uStep1() +{ + ProjectManager.setProjectFolder("PlanetX"); + ModuleDatabase.ScanModules("PlanetX"); + ModuleDatabase.LoadExplicit("AppCore", 1); + + // By id: a bare identifier in TorqueScript is a string, and everything here + // compares object handles. + $uTheme = nameToID("PlanetX"); + uCheck("PlanetX theme loaded", isObject($uTheme)); + + GuiEditor.open(); + uCheck("the editor built a recorder", isObject(GuiEditor.undoRecorder)); + uCheck("which found the brain's UndoManager", isObject(GuiEditor.undoRecorder.manager())); + + // The stage: a panel with three children, which is enough to test index + // restoration without being hard to read. + $uPanel = new GuiControl() { Position = "10 10"; Extent = "400 300"; }; + GuiEditor.rootGui.add($uPanel); + + $uA = new GuiButtonCtrl() { Position = "10 10"; Extent = "80 30"; Text = "A"; }; + $uPanel.add($uA); + $uB = new GuiButtonCtrl() { Position = "10 50"; Extent = "80 30"; Text = "B"; }; + $uPanel.add($uB); + $uC = new GuiButtonCtrl() { Position = "10 90"; Extent = "80 30"; Text = "C"; }; + $uPanel.add($uC); + + $uOther = new GuiControl() { Position = "10 320"; Extent = "200 100"; }; + GuiEditor.rootGui.add($uOther); + + GuiEditor.setTheme($uTheme, false); + + GuiEditor.undoRecorder.clear(); + uCheck("the stack starts empty", uUndoCount() == 0 && uRedoCount() == 0); + + schedule(300, 0, "uStepFields"); +} + +//----------------------------------------------------------------------------- +// Field edits, which all arrive through GuiEditorInspectorPane::writeField. +//----------------------------------------------------------------------------- + +function uStepFields() +{ + GuiEditor.undoRecorder.clear(); + + // Geometry. Position and Extent are protected fields, so writing them runs + // setPosition/setExtent rather than poking mBounds - which is what makes an + // undone resize a real resize. + %pane = uBind($uA); + %before = $uA.getExtent(); + %pane.writeField("Extent", "120 40"); + + uCheck("a field write is one step", uUndoCount() == 1); + uCheck("and it reached the control", $uA.getExtent() $= "120 40"); + + GuiEditor.Undo(); + uCheck("undo put the extent back", $uA.getExtent() $= %before); + uCheck("and moved the step to the redo stack", uUndoCount() == 0 && uRedoCount() == 1); + + GuiEditor.Redo(); + uCheck("redo did it again", $uA.getExtent() $= "120 40"); + uCheck("and moved it back", uUndoCount() == 1 && uRedoCount() == 0); + + // A profile slot, which is recorded by id because a profile made this + // session carries a name the Sim never registered. + %pane = uBind($uA); + %pane.writeField("Profile", PlanetXPanelProfile.getId()); + uCheck("a profile write is one step", uUndoCount() == 2); + uCheck("and it landed", $uA.getFieldValue("Profile") $= "PlanetXPanelProfile"); + + GuiEditor.Undo(); + uCheck("undo put the old profile back", + $uA.getFieldValue("Profile") $= "PlanetXButtonProfile"); + GuiEditor.Redo(); + uCheck("and redo re-applied it", $uA.getFieldValue("Profile") $= "PlanetXPanelProfile"); + GuiEditor.Undo(); + + // A toggle. + %pane = uBind($uB); + %pane.writeField("Visible", false); + uCheck("a toggle is one step", uUndoCount() == 2); + uCheck("and it took", !$uB.Visible); + GuiEditor.Undo(); + uCheck("undo turned it back on", $uB.Visible); + + // A write that changes nothing is not a step. + %depth = uUndoCount(); + %pane = uBind($uB); + %pane.writeField("Visible", true); + uCheck("writing the value it already had records nothing", uUndoCount() == %depth); + + // A dynamic field, which has no setEditFieldValue path of its own. + GuiEditor.undoRecorder.writeDynamicField($uB, "uSmokeTag", "hello"); + uCheck("a dynamic field write is one step", uUndoCount() == %depth + 1); + uCheck("and the field exists", uHasDynamicField($uB, "uSmokeTag")); + + GuiEditor.Undo(); + uCheck("undo removed the dynamic field", !uHasDynamicField($uB, "uSmokeTag")); + GuiEditor.Redo(); + uCheck("redo put it back", $uB.uSmokeTag $= "hello"); + GuiEditor.Undo(); + + schedule(300, 0, "uStepSelection"); +} + +//----------------------------------------------------------------------------- +// What the user is looking at after a replay. +// +// Changing a setting and pressing Ctrl+Z is the commonest undo there is, and the +// control whose setting it was is the one already selected -- so the properties +// pane has to still be on it, with the old value back in the row. Losing the +// selection there empties the whole panel, which reads as though the undo did +// something much larger than it did. +//----------------------------------------------------------------------------- + +function uStepSelection() +{ + GuiEditor.undoRecorder.clear(); + + uSelect($uC); + %pane = GuiEditor.inspectorWindow.pane; + uCheck("selecting a control put it in the pane", %pane.target == $uC); + + %before = $uC.getExtent(); + %pane.writeField("Extent", "150 60"); + + GuiEditor.Undo(); + + %selection = GuiEditor.brain.getSelected(); + uCheck("undo kept the selection", %selection.getCount() == 1); + uCheck("and kept it on the control that changed", %selection.getObject(0) == $uC); + uCheck("the pane is still showing that control", %pane.target == $uC); + uCheck("and the Explorer row is still selected", + GuiEditor.explorerWindow.tree.getSelCount() == 1); + uCheck("the control's value went back", $uC.getExtent() $= %before); + uCheck("and the row it is shown in went back with it", + %pane.header.extentRow.getValue() $= %before); + + GuiEditor.Redo(); + uCheck("redo leaves it selected too", %pane.target == $uC); + uCheck("with the row showing the new value", + %pane.header.extentRow.getValue() $= "150 60"); + GuiEditor.Undo(); + + // An undo whose controls are not the ones selected moves the selection to + // them, which is the other half of the same promise: you see what changed. + GuiEditor.undoRecorder.clear(); + uSelect($uA); + %pane.writeField("Visible", false); + + uSelect($uC); + uCheck("the selection moved away", %pane.target == $uC); + + GuiEditor.Undo(); + %selection = GuiEditor.brain.getSelected(); + uCheck("undo selected what it actually changed", %selection.getObject(0) == $uA); + uCheck("and the pane followed", %pane.target == $uA); + + // Two controls, from one step that moved both. A nudge rather than an align: + // aligning left never moves the leftmost control, so only one of the two + // would have changed - and the selection lands on what changed. + GuiEditor.undoRecorder.clear(); + GuiEditor.brain.clearSelection(); + GuiEditor.brain.select($uA); + GuiEditor.brain.addSelection($uC); + GuiEditor.brain.moveSelection(1, 0); + + GuiEditor.Undo(); + %selection = GuiEditor.brain.getSelected(); + uCheck("undoing a two-control step selects both", %selection.getCount() == 2); + + schedule(300, 0, "uStepSizing"); +} + +//----------------------------------------------------------------------------- +// Sizing, which is a field write that moves the control. +// +// Setting an axis to center or fill hands that axis's geometry to the engine, +// which lays the control out there and then. The enum and the position it took +// are one change, so undo has to give back both -- restoring the enum alone +// leaves the control sitting where centring put it. +//----------------------------------------------------------------------------- + +function uStepSizing() +{ + GuiEditor.undoRecorder.clear(); + + %pane = uBind($uA); + $uA.setPosition(20, 40); + %posBefore = $uA.getPosition(); + %horizBefore = $uA.getFieldValue("HorizSizing"); + + %pane.onSizingChanged("center", $uA.getFieldValue("VertSizing")); + %centred = $uA.getPosition(); + + uCheck("centring moved the control", %centred !$= %posBefore); + uCheck("and the enum and the move it caused are one step", uUndoCount() == 1); + + GuiEditor.Undo(); + uCheck("undo restored the sizing", $uA.getFieldValue("HorizSizing") $= %horizBefore); + uCheck("and the position centring took", $uA.getPosition() $= %posBefore); + + GuiEditor.Redo(); + uCheck("redo centred it again", $uA.getPosition() $= %centred); + uCheck("with the sizing back on center", $uA.getFieldValue("HorizSizing") $= "center"); + GuiEditor.Undo(); + + // Fill takes the extent as well as the position. + GuiEditor.undoRecorder.clear(); + %pane = uBind($uA); + %posBefore = $uA.getPosition(); + %extentBefore = $uA.getExtent(); + + %pane.onSizingChanged("fill", $uA.getFieldValue("VertSizing")); + uCheck("filling resized the control", $uA.getExtent() !$= %extentBefore); + + GuiEditor.Undo(); + uCheck("undo restored the extent fill took", $uA.getExtent() $= %extentBefore); + uCheck("and the position with it", $uA.getPosition() $= %posBefore); + + schedule(300, 0, "uStepText"); +} + +//----------------------------------------------------------------------------- +// Typing, which reaches the control once per keystroke so the canvas keeps up, +// and reaches the stack once. +//----------------------------------------------------------------------------- + +function uStepText() +{ + GuiEditor.undoRecorder.clear(); + + %pane = uBind($uC); + %block = %pane.activeTextBlock(); + %row = %pane.row["text"]; + %before = $uC.text; + + // What a keystroke does: the box's buffer changes and the engine runs its + // Command. Set the buffer and call the handler that Command names. + %row.editor.setText("Ca"); + %block.onTextTyped(); + %row.editor.setText("Cat"); + %block.onTextTyped(); + + uCheck("the control filled in as we typed", $uC.text $= "Cat"); + uCheck("and nothing was recorded yet", uUndoCount() == 0); + + %row.commit(); + uCheck("three keystrokes are one step", uUndoCount() == 1); + uCheck("and the control kept the typed text", $uC.text $= "Cat"); + + GuiEditor.Undo(); + uCheck("undo restored the whole caption at once", $uC.text $= %before); + GuiEditor.Redo(); + uCheck("redo typed it again", $uC.text $= "Cat"); + + schedule(300, 0, "uStepAdd"); +} + +//----------------------------------------------------------------------------- +// Adding a control, which is a move op with the trash on one end. Undo does not +// delete: the control keeps living in the trash, which is the whole reason redo +// can put it back wearing everything it had. +//----------------------------------------------------------------------------- + +function uStepAdd() +{ + GuiEditor.undoRecorder.clear(); + + GuiEditor.brain.setCurrentAddSet($uPanel); + + $uDropped = new GuiCheckBoxCtrl() { Position = "0 0"; Extent = "120 30"; Text = "Dropped"; }; + GuiEditor.brain.onControlDropped($uDropped, "50 50"); + + uCheck("a drop is one step, not one per thing it did", uUndoCount() == 1); + uCheck("the control arrived", $uDropped.getParent() == $uPanel); + uCheck("and was themed on arrival", + $uDropped.getFieldValue("Profile") $= "PlanetXCheckBoxProfile"); + + // After the drop's own schedule(40) has run, so the undo is not racing it. + schedule(200, 0, "uStepAddUndo"); +} + +function uStepAddUndo() +{ + %trash = GuiEditor.brain.getTrash(); + + GuiEditor.Undo(); + + // getGroup, not getParent: getParent is a GuiControl's view of the hierarchy + // and dynamic_casts the group it is in, so a control sitting in the trash -- + // a plain SimGroup -- reads as having no parent at all. + uCheck("undo took the control out of the Gui", $uDropped.getGroup() == %trash); + uCheck("but did not delete it", isObject($uDropped)); + + GuiEditor.Redo(); + uCheck("redo put it back", $uDropped.getParent() == $uPanel); + uCheck("still wearing its theme profile", + $uDropped.getFieldValue("Profile") $= "PlanetXCheckBoxProfile"); + + GuiEditor.Undo(); + + schedule(300, 0, "uStepDelete"); +} + +//----------------------------------------------------------------------------- +// Deleting, which has to restore the index as well as the parent - a control +// put back at the end of its parent is a control that changed z-order. +//----------------------------------------------------------------------------- + +function uStepDelete() +{ + GuiEditor.undoRecorder.clear(); + %trash = GuiEditor.brain.getTrash(); + + // One control, from the middle. + uCheck("B starts at index 1", uIndexOf($uPanel, $uB) == 1); + uSelect($uB); + GuiEditor.brain.deleteSelection(); + + uCheck("a delete is one step", uUndoCount() == 1); + uCheck("and the control went to the trash", $uB.getGroup() == %trash); + + GuiEditor.Undo(); + uCheck("undo put it back in its parent", $uB.getParent() == $uPanel); + uCheck("at the index it came from", uIndexOf($uPanel, $uB) == 1); + + GuiEditor.Redo(); + uCheck("redo trashed it again", $uB.getGroup() == %trash); + GuiEditor.Undo(); + + // Two at once, from either side of a third. + GuiEditor.undoRecorder.clear(); + %indexA = uIndexOf($uPanel, $uA); + %indexC = uIndexOf($uPanel, $uC); + + GuiEditor.brain.clearSelection(); + GuiEditor.brain.select($uA); + GuiEditor.brain.addSelection($uC); + GuiEditor.brain.deleteSelection(); + + uCheck("deleting two controls is still one step", uUndoCount() == 1); + uCheck("both went to the trash", + $uA.getGroup() == %trash && $uC.getGroup() == %trash); + + GuiEditor.Undo(); + uCheck("undo restored both", $uA.getParent() == $uPanel && $uC.getParent() == $uPanel); + uCheck("A at its old index", uIndexOf($uPanel, $uA) == %indexA); + uCheck("C at its old index", uIndexOf($uPanel, $uC) == %indexC); + + schedule(300, 0, "uStepNudge"); +} + +//----------------------------------------------------------------------------- +// Nudging. Holding an arrow key down is one move, which is what the C++'s +// separate onPreSelectionNudged callback was always for. +//----------------------------------------------------------------------------- + +function uStepNudge() +{ + GuiEditor.undoRecorder.clear(); + + %before = $uA.getPosition(); + uSelect($uA); + GuiEditor.brain.moveSelection(1, 0); + GuiEditor.brain.moveSelection(1, 0); + GuiEditor.brain.moveSelection(1, 0); + + // Not "moved by three": snap to grid is on by default (the brain turns it on + // in onAdd), so a nudge of 1 is a nudge to the next grid line. What the run + // landed on is what undo has to take back. + %after = $uA.getPosition(); + uCheck("three nudges are one step", uUndoCount() == 1); + uCheck("and the control moved", %after !$= %before); + + GuiEditor.Undo(); + uCheck("undo takes back the whole run", $uA.getPosition() $= %before); + GuiEditor.Redo(); + uCheck("redo replays the whole run", $uA.getPosition() $= %after); + + // Anything in between breaks the run, or a nudge made after a different edit + // would be folded into the move before it. + GuiEditor.undoRecorder.clear(); + uSelect($uA); + GuiEditor.brain.moveSelection(0, 1); + + %pane = uBind($uA); + %pane.writeField("Visible", false); + + uSelect($uA); + GuiEditor.brain.moveSelection(0, 1); + + uCheck("an edit between two nudges keeps them apart", uUndoCount() == 3); + + %pane = uBind($uA); + %pane.writeField("Visible", true); + + // A mouse-down that selects without dragging is not an edit. + GuiEditor.undoRecorder.clear(); + %selection = GuiEditor.brain.getSelected(); + GuiEditor.undoRecorder.snapshot(%selection); + GuiEditor.undoRecorder.commitGeometry("", ""); + uCheck("a gesture that moved nothing records nothing", uUndoCount() == 0); + + schedule(300, 0, "uStepLayout"); +} + +//----------------------------------------------------------------------------- +// The Layout menu, which records in script because the C++ says nothing when it +// aligns or restacks. +//----------------------------------------------------------------------------- + +function uStepLayout() +{ + GuiEditor.undoRecorder.clear(); + + $uA.setPosition(10, 10); + $uC.setPosition(60, 90); + %beforeA = $uA.getPosition(); + %beforeC = $uC.getPosition(); + + GuiEditor.brain.clearSelection(); + GuiEditor.brain.select($uA); + GuiEditor.brain.addSelection($uC); + GuiEditor.Justify(0); + + uCheck("aligning is one step", uUndoCount() == 1); + uCheck("and it moved something", $uC.getPosition() !$= %beforeC); + + GuiEditor.Undo(); + uCheck("undo put both back", + $uA.getPosition() $= %beforeA && $uC.getPosition() $= %beforeC); + GuiEditor.Redo(); + uCheck("redo aligned them again", $uC.getPosition() !$= %beforeC); + + // Restacking. The C++ works on the current add set, so that has to be the + // control's parent - and setting it clears the selection, so it goes first. + GuiEditor.undoRecorder.clear(); + GuiEditor.brain.setCurrentAddSet($uPanel); + uSelect($uA); + + %before = uIndexOf($uPanel, $uA); + GuiEditor.BringToFront(); + + uCheck("bring to front is one step", uUndoCount() == 1); + uCheck("and it moved the control up the list", uIndexOf($uPanel, $uA) != %before); + + GuiEditor.Undo(); + uCheck("undo restored the index", uIndexOf($uPanel, $uA) == %before); + GuiEditor.Redo(); + uCheck("redo restacked it again", uIndexOf($uPanel, $uA) != %before); + GuiEditor.Undo(); + + schedule(300, 0, "uStepTheme"); +} + +//----------------------------------------------------------------------------- +// Set Theme, which writes a profile into every slot of every control in the +// document and is one Ctrl+Z. +//----------------------------------------------------------------------------- + +function uStepTheme() +{ + GuiEditor.undoRecorder.clear(); + + %library = GuiEditor.getThemeLibrary(); + $uThemeB = %library.createTheme("UndoThemeB"); + uCheck("second theme created", isObject($uThemeB)); + + GuiEditor.setTheme($uThemeB, false); + uCheck("a whole-document re-theme is one step", uUndoCount() == 1); + uCheck("and it re-profiled the controls", + $uA.getFieldValue("Profile") $= "UndoThemeBButtonProfile"); + + GuiEditor.Undo(); + uCheck("undo put the old theme's profiles back", + $uA.getFieldValue("Profile") $= "PlanetXButtonProfile"); + uCheck("all of them", $uPanel.getFieldValue("Profile") $= "PlanetXPanelProfile"); + + GuiEditor.Redo(); + uCheck("redo re-applied the theme", + $uA.getFieldValue("Profile") $= "UndoThemeBButtonProfile"); + + GuiEditor.Undo(); + GuiEditor.setTheme($uTheme, false); + + // Deleting the theme detaches the document from it first, which is one of the + // paths that has to empty the stack: every record holds profile ids that stop + // resolving. + %library.deleteTheme($uThemeB); + uCheck("deleting a theme emptied the stack", uUndoCount() == 0 && uRedoCount() == 0); + + schedule(300, 0, "uStepReparent"); +} + +//----------------------------------------------------------------------------- +// Dragging in the Explorer tree, which rearranges the hierarchy in C++ and can +// move any number of controls into any number of parents at once. The engine +// now brackets it with onPreReorder / onPostReorder; those are called directly +// here, around the same hierarchy change the drag makes. +//----------------------------------------------------------------------------- + +function uStepReparent() +{ + GuiEditor.undoRecorder.clear(); + %tree = GuiEditor.explorerWindow.tree; + + %index = uIndexOf($uPanel, $uA); + + %tree.onPreReorder(); + $uOther.add($uA); + %tree.onPostReorder(); + + uCheck("a reparent is one step", uUndoCount() == 1); + uCheck("and the control changed parent", $uA.getParent() == $uOther); + + GuiEditor.Undo(); + uCheck("undo returned it to its old parent", $uA.getParent() == $uPanel); + uCheck("at its old index", uIndexOf($uPanel, $uA) == %index); + + GuiEditor.Redo(); + uCheck("redo reparented it again", $uA.getParent() == $uOther); + GuiEditor.Undo(); + + // A reorder inside one parent, which is the same op restoring a list rather + // than a parent. + GuiEditor.undoRecorder.clear(); + %first = $uPanel.getObject(0); + %last = $uPanel.getObject($uPanel.getCount() - 1); + + %tree.onPreReorder(); + $uPanel.reorderChild(%last, %first); + %tree.onPostReorder(); + + uCheck("a reorder is one step", uUndoCount() == 1); + uCheck("and the order changed", $uPanel.getObject(0) == %last); + + GuiEditor.Undo(); + uCheck("undo restored the order", $uPanel.getObject(0) == %first); + + schedule(300, 0, "uStepMenu"); +} + +//----------------------------------------------------------------------------- +// The Edit menu, which is what tells the user whether there is anything to take +// back. It was inactive entirely until now. +//----------------------------------------------------------------------------- + +// Menu items are nested controls, so this walks rather than indexes. +function uMenuItem(%parent, %text) +{ + for(%i = 0; %i < %parent.getCount(); %i++) + { + %item = %parent.getObject(%i); + if(%item.Text $= %text) + { + return %item; + } + + %found = uMenuItem(%item, %text); + if(isObject(%found)) + { + return %found; + } + } + + return 0; +} + +function uStepMenu() +{ + %editMenu = uMenuItem(EditorCore.menuBar, "Edit"); + %undoItem = uMenuItem(EditorCore.menuBar, "Undo"); + %redoItem = uMenuItem(EditorCore.menuBar, "Redo"); + %cutItem = uMenuItem(EditorCore.menuBar, "Cut"); + + uCheck("the Edit menu exists", isObject(%editMenu)); + uCheck("and is offered at last", %editMenu.Active); + uCheck("Cut is still a stub and is not offered", !%cutItem.Active); + + GuiEditor.undoRecorder.clear(); + uCheck("Undo is greyed with an empty stack", !%undoItem.Active); + uCheck("Redo is greyed with an empty stack", !%redoItem.Active); + + %pane = uBind($uC); + %pane.writeField("Visible", false); + uCheck("an edit lights Undo", %undoItem.Active); + uCheck("and leaves Redo greyed", !%redoItem.Active); + + GuiEditor.Undo(); + uCheck("undoing the last step greys Undo", !%undoItem.Active); + uCheck("and lights Redo", %redoItem.Active); + + GuiEditor.Redo(); + uCheck("redoing lights Undo again", %undoItem.Active); + uCheck("and greys Redo", !%redoItem.Active); + + GuiEditor.Undo(); + + schedule(300, 0, "uStepStale"); +} + +//----------------------------------------------------------------------------- +// The two ways the stack goes stale. +//----------------------------------------------------------------------------- + +function uStepStale() +{ + GuiEditor.undoRecorder.clear(); + + // A record naming a control that has since been deleted for real. Nothing + // clears the stack for this - a control the editor did not delete can still + // go away - so the replay has to survive it. + %doomed = new GuiButtonCtrl() { Position = "10 10"; Extent = "60 20"; Text = "X"; }; + $uOther.add(%doomed); + + %pane = uBind(%doomed); + %pane.writeField("Extent", "90 40"); + uCheck("the doomed control's edit was recorded", uUndoCount() == 1); + + %doomed.delete(); + GuiEditor.Undo(); + uCheck("undoing an edit on a deleted control is a no-op, not a crash", + uUndoCount() == 0 && uRedoCount() == 1); + + // A new document. Every id on the stack has just been freed. + GuiEditor.NewGui(); + uCheck("New Gui empties both stacks", uUndoCount() == 0 && uRedoCount() == 0); + + schedule(300, 0, "uStepChain"); +} + +//----------------------------------------------------------------------------- +// Deleting out of a layout container. +// +// A GuiChainCtrl lays its children out in list order and zeroes the position of +// any child added to it while the editor is open (guiChainCtrl.cc, +// onChildAdded) -- both right for a control being dropped in, both wrong for +// one being put back, which knows where it was and which slot it held. +// +// Last, and on the real editor UI, because that second half only happens when +// isEditMode() is true: that needs the brain awake, which needs the editor +// pushed onto the canvas rather than merely registered. Everything above runs +// without it, so this is where the canvas gets taken over. +//----------------------------------------------------------------------------- + +function uStepChain() +{ + EditorCore.open(); + EditorCore.tabBook.selectPageName("Gui Editor"); + + schedule(500, 0, "uStepChainRun"); +} + +function uStepChainRun() +{ + GuiEditor.undoRecorder.clear(); + + $uChain = new GuiChainCtrl() + { + Position = "10 10"; + Extent = "200 40"; + IsVertical = true; + ChildSpacing = 4; + }; + GuiEditor.rootGui.add($uChain); + + // Proof by behaviour that the canvas really is in edit mode, since that is + // what this whole step turns on: zeroing an added child's position is + // something a chain only does then. + %probe = new GuiButtonCtrl() { Extent = "80 24"; }; + %probe.setPosition(33, 33); + $uChain.add(%probe); + uCheck("the editor is really in edit mode now (a chain zeroes what it is given)", + getWord(%probe.getPosition(), 0) == 0); + %probe.delete(); + + // The x is set after the add, because the add is what zeroes it. + for(%i = 0; %i < 4; %i++) + { + %button = new GuiButtonCtrl() { Extent = "80 24"; Text = "chain" @ %i; }; + $uChain.add(%button); + %button.setPosition(%i * 5, 0); + $uButton[%i] = %button; + } + + uCheck("the chain holds four buttons", $uChain.getCount() == 4); + %xBefore = getWord($uButton[1].getPosition(), 0); + uCheck("and the second one has an x of its own", %xBefore == 5); + + uSelect($uButton[1]); + GuiEditor.brain.deleteSelection(); + uCheck("deleting left three", $uChain.getCount() == 3); + + GuiEditor.Undo(); + + uCheck("undo put it back in the chain", $uButton[1].getParent() == $uChain); + uCheck("at the index it came from", uIndexOf($uChain, $uButton[1]) == 1); + uCheck("keeping the x the chain does not own (" @ + getWord($uButton[1].getPosition(), 0) @ " was " @ %xBefore @ ")", + getWord($uButton[1].getPosition(), 0) == %xBefore); + + // The chain owns y, so the proof it laid out again is that the four are back + // in flow order rather than the restored one sitting where the list briefly + // had it -- last. + %y0 = getWord($uButton[0].getPosition(), 1); + %y1 = getWord($uButton[1].getPosition(), 1); + %y2 = getWord($uButton[2].getPosition(), 1); + %y3 = getWord($uButton[3].getPosition(), 1); + uCheck("and the chain laid them out in order again (" @ + %y0 SPC %y1 SPC %y2 SPC %y3 @ ")", %y0 < %y1 && %y1 < %y2 && %y2 < %y3); + + schedule(300, 0, "uStepContainers"); +} + +//----------------------------------------------------------------------------- +// Every other container that places its own children. +// +// A chain is not a special case: a grid, a tab book and a frame set all take +// something from a child when it arrives and all lay their children out by list +// order, so all of them can hand back the wrong thing after an undo. The test +// each one gets is the same, and it is the strongest one available: the whole +// container must look exactly as it did before the delete. +//----------------------------------------------------------------------------- + +// Every child's identity, position and extent, in list order. TAB-delimited +// because a position has a space in it. +function uLayoutOf(%parent) +{ + %text = ""; + for(%i = 0; %i < %parent.getCount(); %i++) + { + %child = %parent.getObject(%i); + %entry = %child @ "[" @ %child.getPosition() @ "][" @ %child.getExtent() @ "]"; + %text = (%text $= "") ? %entry : (%text TAB %entry); + } + + return %text; +} + +// Delete the second child of %parent and undo it. Everything has to come back: +// the list order, and every child's geometry, which is the container's to give. +function uRoundTrip(%label, %parent) +{ + GuiEditor.undoRecorder.clear(); + + %before = uLayoutOf(%parent); + %child = %parent.getObject(1); + + uSelect(%child); + GuiEditor.brain.deleteSelection(); + uCheck(%label @ ": the delete took", %parent.getCount() == 3); + + GuiEditor.Undo(); + + uCheck(%label @ ": undo put it back at index 1", uIndexOf(%parent, %child) == 1); + + %after = uLayoutOf(%parent); + uCheck(%label @ ": and the container looks as it did" NL + " before: " @ %before NL + " after: " @ %after, %after $= %before); +} + +function uStepContainers() +{ + // A grid, which places children into cells by list order and forces a + // child's sizing off center and fill when it arrives. + $uGrid = new GuiGridCtrl() + { + Position = "10 200"; + Extent = "300 120"; + CellSizeX = 60; + CellSizeY = 30; + MaxColCount = 2; + }; + GuiEditor.rootGui.add($uGrid); + + for(%i = 0; %i < 4; %i++) + { + %cell = new GuiButtonCtrl() { Extent = "50 20"; Text = "cell" @ %i; }; + $uGrid.add(%cell); + } + uRoundTrip("grid", $uGrid); + + // A container takes a child's sizing as well as its geometry: a grid forces + // center and fill off on arrival. A child put back on center afterwards + // would lose it again on the way in. + %cell = $uGrid.getObject(1); + %cell.setEditFieldValue("HorizSizing", "center"); + uCheck("grid: the cell is on center to start", + %cell.getFieldValue("HorizSizing") $= "center"); + + GuiEditor.undoRecorder.clear(); + uSelect(%cell); + GuiEditor.brain.deleteSelection(); + GuiEditor.Undo(); + uCheck("grid: undo restored the sizing the grid takes on arrival (" @ + %cell.getFieldValue("HorizSizing") @ ")", + %cell.getFieldValue("HorizSizing") $= "center"); + + // A tab book, whose tab strip is drawn from a page list of its own rather + // than from the children. + $uBook = new GuiTabBookCtrl() + { + Position = "330 200"; + Extent = "300 120"; + }; + GuiEditor.rootGui.add($uBook); + + for(%i = 0; %i < 4; %i++) + { + %page = new GuiTabPageCtrl() { Text = "page" @ %i; }; + $uBook.add(%page); + $uPage[%i] = %page; + } + uRoundTrip("tab book", $uBook); + + // The pages all share the book's page rect, so their geometry cannot say + // whether the tab strip came back in the right order. Selecting by index + // can: it selects the page list's second entry, which has to be the child + // that is second. + $uBook.selectPage(1); + uCheck("tab book: the second tab is the second page", + $uPage[1].isVisible()); + + schedule(300, 0, "uStepFrameSet"); +} + +function uStepFrameSet() +{ + // A frame set, which places each child in a frame of a tree it keeps beside + // the child list. + $uFrames = new GuiFrameSetCtrl() + { + Position = "10 340"; + Extent = "400 200"; + DividerThickness = 4; + }; + GuiEditor.rootGui.add($uFrames); + + %ids = $uFrames.createHorizontalSplit(1); + %left = getWord(%ids, 0); + %right = getWord(%ids, 1); + %ids = $uFrames.createVerticalSplit(%left); + %ids = $uFrames.createVerticalSplit(%right); + + for(%i = 0; %i < 4; %i++) + { + %panel = new GuiControl() { Extent = "40 20"; }; + $uFrames.add(%panel); + } + + // Settle the layout before anything is measured. A frame set places its + // children when it resizes, and until then they are still sitting at the + // bounds they were built with -- which would make the before-and-after + // comparison meaningless. + $uFrames.childrenReordered(); + + // The frame tree itself has to come back, not just the child list: removing + // a control destroys the frame it stood in and merges the split into its + // twin, so without that the sibling keeps the space it swallowed. + $uFrameLayout = $uFrames.getFrameLayout(); + uCheck("frame set: its layout can be read", $uFrameLayout !$= ""); + + uRoundTrip("frame set", $uFrames); + + uCheck("frame set: and the frame tree came back with it", + $uFrames.getFrameLayout() $= $uFrameLayout); + + // Redo has to take it apart again, or a second undo would be restoring + // against a tree that no longer matches. + GuiEditor.Redo(); + uCheck("frame set: redo collapsed the frame again", + $uFrames.getFrameLayout() !$= $uFrameLayout); + GuiEditor.Undo(); + uCheck("frame set: and undoing again rebuilt it", + $uFrames.getFrameLayout() $= $uFrameLayout); + + schedule(300, 0, "uDone"); +} + +function uDone() +{ + echo("UNDO DONE"); + quit(); +} From 01d1f8f80846aee2e1c73eb26d1cfe1ff4af7085 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Wed, 29 Jul 2026 17:57:42 -0400 Subject: [PATCH 14/55] Gui Editor: check that placing by click is undoable The palette's click-to-place was built as a synthetic drop -- it calls the brain's own onControlDropped rather than adding a control itself -- on the reasoning that this reaches GuiEditCtrl::addNewControl, which fires onAddNewCtrl, which is where an undo recorder would hook in. At the time there was no recorder to test that against. There is now, so assert it: a click is one undo step, undoing takes the control back out, and redo restores the same class. If a click ever grows its own way into the document, this is what notices. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FVXsRigjXMEjMrbyHYawJD --- tests/smoke/palette.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/smoke/palette.cs b/tests/smoke/palette.cs index 5aa73ca2c..c94e6cd29 100644 --- a/tests/smoke/palette.cs +++ b/tests/smoke/palette.cs @@ -239,6 +239,8 @@ function palDropChecks() %tile = palFindTile("GuiButtonCtrl"); palCheck("found the button tile", isObject(%tile)); + + %depth = GuiEditor.undoRecorder.undoCount(); %tile.onClick(); palCheck("clicking a tile added a control (" @ %root.getCount() @ ")", @@ -246,6 +248,22 @@ function palDropChecks() %added = %root.getObject(%root.getCount() - 1); palCheck("it built the class the tile names", %added.getClassName() $= "GuiButtonCtrl"); + // The reason a click is routed through onControlDropped rather than adding + // the control itself: that path reaches GuiEditCtrl::addNewControl, which + // fires onAddNewCtrl, which is where the recorder hooks in. If a click ever + // grows its own way into the document, this is the check that notices. + palCheck("clicking a tile is one undo step (" @ + (GuiEditor.undoRecorder.undoCount() - %depth) @ ")", + GuiEditor.undoRecorder.undoCount() == %depth + 1); + + GuiEditor.Undo(); + palCheck("undoing a click takes the control back out (" @ %root.getCount() @ ")", + %root.getCount() == %before); + GuiEditor.Redo(); + palCheck("and redo puts it back", %root.getCount() == %before + 1); + %added = %root.getObject(%root.getCount() - 1); + palCheck("redo restores the same class", %added.getClassName() $= "GuiButtonCtrl"); + // A drag ends with a mouse-up over the tile, which fires onClick as well -- // a button keeps mDepressed through a drag. One gesture, one control. %count = %root.getCount(); From 5659bf36a72c90141e64b2626700895198cda0fa Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Wed, 29 Jul 2026 18:24:10 -0400 Subject: [PATCH 15/55] Tests: let every harness that screenshots make the folder it writes into screenShot does not create its destination, and it reports failure by logging rather than by throwing. shots/ is gitignored, so a tree that has never run one does not have the folder -- a fresh clone, or a git worktree. The harness then runs green all the way to SHOTS DONE and writes nothing, and the runner reports "0 shots" with no indication why. Eight files already guarded against it and nine did not, including six smoke tests: the screenshots there are diagnostics rather than the point, so they were easy to overlook. Placed as a bare top-level line before the kickoff schedule, matching shots/planetX.cs, which is where the pattern already was. The reason is now written down once in the README's traps rather than nine times in comments. Not re-run: the line is the same one shots/planetX.cs has carried at top level all along, and the full suite passed on this tree immediately before this change. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FVXsRigjXMEjMrbyHYawJD --- tests/README.md | 6 ++++++ tests/shots/deleteConfirm.cs | 1 + tests/shots/iconSheet.cs | 1 + tests/shots/theme.cs | 1 + tests/smoke/bitmapPathRead.cs | 1 + tests/smoke/planetX.cs | 1 + tests/smoke/textClick.cs | 1 + tests/smoke/textEdit.cs | 1 + tests/smoke/toggleTip.cs | 1 + tests/smoke/toybox.cs | 1 + 10 files changed, 15 insertions(+) diff --git a/tests/README.md b/tests/README.md index 21bc11850..795651fde 100644 --- a/tests/README.md +++ b/tests/README.md @@ -110,6 +110,12 @@ sequence; otherwise it is picked up automatically and run last, alphabetically. exit code is 0 — so a test that "does nothing" is usually a test that did not compile. TorqueScript has no comma operator and no method chaining on a call result (`Canvas.getContent().add(%x)` is a parse error; take the two steps). +- **`screenShot` does not create its folder, and it fails by logging.** `shots/` is + gitignored, so a tree that has never run a shot does not have one — a fresh clone, + or a git worktree. The harness then runs green all the way to `SHOTS DONE` and + writes nothing, and the runner reports `0 shots` with no reason given. Every test + that screenshots calls `createPath(testRoot("shots/"))` before its first + `schedule` for this reason; keep doing it in new ones. - **A debug-build `AssertFatal` is a modal message box.** A test that trips one hangs rather than crashing, which is why the runner kills on a timeout. If a test "hangs", suspect an undismissed assert before suspecting a loop. diff --git a/tests/shots/deleteConfirm.cs b/tests/shots/deleteConfirm.cs index 1df65f53e..20bad5d8f 100644 --- a/tests/shots/deleteConfirm.cs +++ b/tests/shots/deleteConfirm.cs @@ -12,6 +12,7 @@ testExec("editor/main.cs"); +createPath(testRoot("shots/")); schedule(2500, 0, "shotStep1"); function shotStep1() diff --git a/tests/shots/iconSheet.cs b/tests/shots/iconSheet.cs index 0f8762e2f..ee7dc11d2 100644 --- a/tests/shots/iconSheet.cs +++ b/tests/shots/iconSheet.cs @@ -15,6 +15,7 @@ AssetDatabase.EchoInfo = false; testExec("editor/main.cs"); +createPath(testRoot("shots/")); schedule(2500, 0, "sheetShot"); $iconSheet::perPage = 60; diff --git a/tests/shots/theme.cs b/tests/shots/theme.cs index 1f1049068..128be0859 100644 --- a/tests/shots/theme.cs +++ b/tests/shots/theme.cs @@ -10,6 +10,7 @@ testExec("editor/main.cs"); // The splash animates for ~2.8s before the project selector appears; give it room. +createPath(testRoot("shots/")); schedule(7000, 0, "shot1"); function shot1() diff --git a/tests/smoke/bitmapPathRead.cs b/tests/smoke/bitmapPathRead.cs index f5c683c94..fb4b95a53 100644 --- a/tests/smoke/bitmapPathRead.cs +++ b/tests/smoke/bitmapPathRead.cs @@ -28,6 +28,7 @@ function smokeCheck(%label, %condition) } } +createPath(testRoot("shots/")); schedule(2500, 0, "readStep1"); function readStep1() diff --git a/tests/smoke/planetX.cs b/tests/smoke/planetX.cs index e9eefcff2..983074196 100644 --- a/tests/smoke/planetX.cs +++ b/tests/smoke/planetX.cs @@ -20,6 +20,7 @@ function smokeCheck(%label, %condition) echo(%condition ? ("SMOKE PASS: " @ %label) : ("SMOKE FAIL: " @ %label)); } +createPath(testRoot("shots/")); schedule(4000, 0, "planetXSmoke"); function planetXSmoke() diff --git a/tests/smoke/textClick.cs b/tests/smoke/textClick.cs index 247ba26d7..552c284e9 100644 --- a/tests/smoke/textClick.cs +++ b/tests/smoke/textClick.cs @@ -32,6 +32,7 @@ function smokeCheck(%label, %condition) $clickText = "toybox/themes/image/ironWindow.png and then some more text"; +createPath(testRoot("shots/")); schedule(2500, 0, "clickStep1"); function clickStep1() diff --git a/tests/smoke/textEdit.cs b/tests/smoke/textEdit.cs index cfbdd8a8a..ef75c4b6d 100644 --- a/tests/smoke/textEdit.cs +++ b/tests/smoke/textEdit.cs @@ -56,6 +56,7 @@ function teProbe(%stage, %x, %text) $teText = "abc def"; +createPath(testRoot("shots/")); schedule(2500, 0, "teStep1"); function teStep1() diff --git a/tests/smoke/toggleTip.cs b/tests/smoke/toggleTip.cs index 525567e97..b2944ca76 100644 --- a/tests/smoke/toggleTip.cs +++ b/tests/smoke/toggleTip.cs @@ -31,6 +31,7 @@ function ttCheck(%label, %condition) else echo("TTSMOKE FAIL: " @ %label); } +createPath(testRoot("shots/")); schedule(2500, 0, "ttOpenProject"); // A tip has to be hovered to be seen, so unlike the other pane suites this one diff --git a/tests/smoke/toybox.cs b/tests/smoke/toybox.cs index a5f43f0f9..a9889eb35 100644 --- a/tests/smoke/toybox.cs +++ b/tests/smoke/toybox.cs @@ -17,6 +17,7 @@ function smokeCheck(%label, %condition) echo(%condition ? ("SMOKE PASS: " @ %label) : ("SMOKE FAIL: " @ %label)); } +createPath(testRoot("shots/")); schedule(5000, 0, "toySmoke"); function toySmoke() From f6127b5acceea036ccbd7d727501b17ed37b0f17 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Wed, 29 Jul 2026 21:36:13 -0400 Subject: [PATCH 16/55] SimObject: a deep clone that copies the data and runs no lifecycle Copy and paste in the Gui Editor has to duplicate a control tree, and nothing here could. clone() makes a shell of the right class and copies no fields at all -- SimObject::copyTo carries the class names and nothing else -- and assignFieldsFrom copies name and parentGroup like any other field. The second of those is the trap: setParentGroup calls parent->addObject(), so a "copy" made that way silently moves itself into the original's group, building a frame in a frame set or getting its position zeroed by a chain on the way past. deepClone() copies the fields, the dynamic fields and the whole child tree, and promises the two things a clipboard needs. Both are orderings rather than code, so both are commented where they happen and tested: copyTo runs LAST, because copyTo is what links the script class's namespaces. Until it has run there is no script class on the clone for anything to find a callback on, so registerObject's onAdd and the parent's onChildAdded stay silent -- and a class whose onAdd builds children of its own cannot build a second set on top of the ones being copied. class and superclass are ordinary persist fields whose setters link the namespaces too, so the field copy skips them as well; without that half the first child added defeats the whole rule, which is how this was found. A child is added to its new parent BEFORE its fields are written, so a container that takes something from an arriving child -- a chain zeroing a position, a grid forcing sizing off center -- does it before the values the copy is carrying land, rather than after. assignFieldsFrom becomes copyFieldsFrom(source, 0) and is unchanged for its two callers, save for one bug it always had: it read every field through the destination while passing the source's raw data. A protected field whose get function ignores that pointer and answers from the object instead -- GuiControl's "text" does exactly that -- therefore read back what the destination already held and wrote it straight back. Those fields never copied at all, a control's caption among them, and new Foo(:other) had the same hole. It now reads through the source. GuiFrameSetCtrl overrides deepCloneChildren to rebuild its frame tree, which is the one piece of layout in the gui tree that is not a persist field: it lives in TAML custom nodes and nowhere else, so a copy built from fields alone comes back holding children with no frames to put them in. Copied structurally rather than through getFrameLayout, whose text names each frame's control by an id the copy's children do not have, and after the children exist, because assignChildToFrame only fills frames that are already empty -- it never makes one. The recursion is on SimGroup, not SimSet: a group owns what it holds, while a set only references objects some group owns, and duplicating those would be inventing objects nobody asked for. Verified: nine new tests in simObjectCloneTests.cc, including a ScriptObject class with a counting onAdd that has to count zero and a frame set whose copy must hold its own children in the same tree. Whole unit-test run green. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qi2pZ9UzF5rsLCNVErpzwt --- cmake/EngineSources.cmake | 1 + .../source/gui/containers/guiFrameSetCtrl.cc | 92 ++++ .../source/gui/containers/guiFrameSetCtrl.h | 10 + engine/source/sim/simObject.cc | 108 ++++- engine/source/sim/simObject.h | 56 +++ engine/source/sim/simObject_ScriptBinding.h | 20 + engine/source/sim/simSet.cc | 31 ++ engine/source/sim/simSet.h | 11 + .../testing/tests/simObjectCloneTests.cc | 392 ++++++++++++++++++ 9 files changed, 720 insertions(+), 1 deletion(-) create mode 100644 engine/source/testing/tests/simObjectCloneTests.cc diff --git a/cmake/EngineSources.cmake b/cmake/EngineSources.cmake index fbfe8ffb4..6881bfd59 100644 --- a/cmake/EngineSources.cmake +++ b/cmake/EngineSources.cmake @@ -342,6 +342,7 @@ set(TORQUE_ENGINE_SOURCES ${TORQUE_SRC}/testing/tests/platformFileIoTests.cc ${TORQUE_SRC}/testing/tests/platformMemoryTests.cc ${TORQUE_SRC}/testing/tests/platformStringTests.cc + ${TORQUE_SRC}/testing/tests/simObjectCloneTests.cc # ---- platform ---- ${TORQUE_SRC}/platform/CursorManager.cc ${TORQUE_SRC}/platform/Tickable.cc diff --git a/engine/source/gui/containers/guiFrameSetCtrl.cc b/engine/source/gui/containers/guiFrameSetCtrl.cc index 4b64f8c70..222239d3b 100644 --- a/engine/source/gui/containers/guiFrameSetCtrl.cc +++ b/engine/source/gui/containers/guiFrameSetCtrl.cc @@ -618,6 +618,98 @@ void GuiFrameSetCtrl::setFrameLayout(const char* layout) resize(getPosition(), getExtent()); } +//----------------------------------------------------------------------------- +// Copying. A frame set is the one control in the engine that keeps a layout of +// its own beside the child list, and none of it is a persist field - it is +// written as TAML custom nodes and nothing else. So a copy of a frame set that +// only copied fields and children would come back with four children and no +// frames to put them in. +// +// This runs after the children have been copied, because a frame only takes a +// control that is already a child (onChildAdded/assignChildToFrame fills empty +// frames, it never makes one). +//----------------------------------------------------------------------------- + +void GuiFrameSetCtrl::deepCloneChildren(SimObject* clone) +{ + Parent::deepCloneChildren(clone); + + GuiFrameSetCtrl* pCloneFrameSet = dynamic_cast(clone); + if (pCloneFrameSet) + { + pCloneFrameSet->copyFrameTreeFrom(this); + } +} + +void GuiFrameSetCtrl::copyFrameTreeFrom(GuiFrameSetCtrl* source) +{ + if (!source) + { + return; + } + + // Back to a single frame first, exactly as setFrameLayout does: the children + // arriving have already been handed whichever frames were free, and those + // assignments are about to be made again properly. + mRootFrame.deleteChildren(); + mRootFrame.child1 = nullptr; + mRootFrame.child2 = nullptr; + mRootFrame.control = nullptr; + + copyFrame(&mRootFrame, &source->mRootFrame, source); + + resize(getPosition(), getExtent()); +} + +// The tree is copied structurally rather than through getFrameLayout, because +// the layout text names each frame's control by id - and the copy's children are +// new objects with new ids. What pairs them instead is position in the child +// list: deepCloneChildren copies children in order, so the source's nth child +// and this control's nth child are the same control. +void GuiFrameSetCtrl::copyFrame(GuiFrameSetCtrl::Frame* frame, const GuiFrameSetCtrl::Frame* sourceFrame, GuiFrameSetCtrl* source) +{ + frame->id = sourceFrame->id; + frame->isVertical = sourceFrame->isVertical; + frame->extent = sourceFrame->extent; + frame->isAnchored = sourceFrame->isAnchored; + frame->control = nullptr; + + // Frames split from here on must not reuse an id the copied tree holds. + if (sourceFrame->id > mNextFrameID) + { + mNextFrameID = sourceFrame->id; + } + + if (sourceFrame->child1 && sourceFrame->child2) + { + // The same call buildFrameLayout makes, and for the same reason: it is + // what builds a pair of frames under this one. The ids it assigns are + // overwritten by the recursion below with the source's own. + splitFrame(frame, frame->isVertical ? GuiDirection::Up : GuiDirection::Left); + + copyFrame(frame->child1, sourceFrame->child1, source); + copyFrame(frame->child2, sourceFrame->child2, source); + return; + } + + if (!sourceFrame->control) + { + return; + } + + for (U32 i = 0; i < (U32)source->size(); i++) + { + if ((*source)[i] == sourceFrame->control) + { + if (i < (U32)size()) + { + frame->control = dynamic_cast((*this)[i]); + } + break; + } + } +} + void GuiFrameSetCtrl::buildFrameLayout(GuiFrameSetCtrl::Frame* frame, const U32 frameID, const Vector& values) { S32 at = -1; diff --git a/engine/source/gui/containers/guiFrameSetCtrl.h b/engine/source/gui/containers/guiFrameSetCtrl.h index 4c6437325..a8500f508 100644 --- a/engine/source/gui/containers/guiFrameSetCtrl.h +++ b/engine/source/gui/containers/guiFrameSetCtrl.h @@ -164,6 +164,16 @@ class GuiFrameSetCtrl : public GuiEasingSupport void setDataField(const char* tag, const U32 id, const U32 value); const char* getDataField(const char* tag, const U32 id); + // Rebuild %source's frame tree here, with this control's own children in it. + // Public for the sake of deepCloneChildren, which is called on the source and + // has to reach the copy. + void copyFrameTreeFrom(GuiFrameSetCtrl* source); + +protected: + virtual void deepCloneChildren(SimObject* clone); + void copyFrame(Frame* frame, const Frame* sourceFrame, GuiFrameSetCtrl* source); + +public: DECLARE_CONOBJECT(GuiFrameSetCtrl); }; diff --git a/engine/source/sim/simObject.cc b/engine/source/sim/simObject.cc index 58ecbbb86..820b5f99c 100755 --- a/engine/source/sim/simObject.cc +++ b/engine/source/sim/simObject.cc @@ -286,6 +286,30 @@ void SimObject::assignDynamicFieldsFrom(SimObject* parent) void SimObject::assignFieldsFrom(SimObject *parent) { + copyFieldsFrom(parent, 0); +} + +void SimObject::copyFieldsFrom(SimObject *parent, const U32 flags) +{ + // What the caller asked to be left out. Every one of these is an ordinary + // persist field, so without this they would be copied like any other - and + // each does something no copy wants: + // + // name two objects answering to one name, which is the same bug + // whether or not the Sim is currently registering them. + // parentGroup setParentGroup() calls parent->addObject(), so copying the + // field does not record where the object lives, it MOVES it + // there - into the group the original is in. + // class setClass()/setSuperClass() link the object's namespaces, so + // superclass writing either one makes the object's script class live from + // that moment. A deep clone leaves both to copyTo, at the very + // end, so that nothing it does on the way can find a script + // callback to run. + static StringTableEntry nameField = StringTable->insert("name"); + static StringTableEntry parentGroupField = StringTable->insert("parentGroup"); + static StringTableEntry classField = StringTable->insert("class"); + static StringTableEntry superClassField = StringTable->insert("superclass"); + // only allow field assigns from objects of the same class: if(getClassRep() == parent->getClassRep()) { @@ -295,10 +319,28 @@ void SimObject::assignFieldsFrom(SimObject *parent) for(U32 i = 0; i < (U32)list.size(); i++) { const AbstractClassRep::Field* f = &list[i]; + + if((flags & CopyFields_SkipName) && f->pFieldname == nameField) + continue; + + if((flags & CopyFields_SkipParentGroup) && f->pFieldname == parentGroupField) + continue; + + if((flags & CopyFields_SkipScriptClass) && + (f->pFieldname == classField || f->pFieldname == superClassField)) + continue; + S32 lastField = f->elementCount - 1; for(S32 j = 0; j <= lastField; j++) { - const char* fieldVal = (*f->getDataFn)( this, Con::getData(f->type, (void *) (((const char *)parent) + f->offset), j, f->table, f->flag)); + // Read through the SOURCE, not through this object. A protected + // field's get function is free to ignore the raw data it is handed + // and answer from the object instead - GuiControl's "text" does + // exactly that (getTextProperty returns obj->getText()) - so asking + // this object for the value read back what the copy already held and + // wrote it straight back: nothing was ever copied. Every such field + // silently did not copy until now, a caption among them. + const char* fieldVal = (*f->getDataFn)( parent, Con::getData(f->type, (void *) (((const char *)parent) + f->offset), j, f->table, f->flag)); //if(fieldVal) // Con::setData(f->type, (void *) (((const char *)this) + f->offset), j, 1, &fieldVal, f->table); if(fieldVal) @@ -1186,6 +1228,70 @@ SimObject* SimObject::clone( const bool copyDynamicFields ) return pCloneObject; } +//----------------------------------------------------------------------------- +// A copy of an object, everything in it, and everything below it. +// +// The contract is that a deep clone is data: it holds what the original held, +// and no script lifecycle callback fires while it is being built. Two orderings +// are what deliver that, and both are load-bearing - see cloneInto below. +//----------------------------------------------------------------------------- + +SimObject* SimObject::deepClone() +{ + SimObject* pCloneObject = allocClone(); + + if ( pCloneObject == NULL ) + return NULL; + + cloneInto( pCloneObject ); + + return pCloneObject; +} + +// A registered but empty object of this object's class. getClassName() is the +// class rep's name - the C++ class - so this works for an object carrying a +// script class too; the script class itself is copied by copyTo, later. +SimObject* SimObject::allocClone() +{ + SimObject* pCloneObject = dynamic_cast( ConsoleObject::create(getClassName()) ); + if (!pCloneObject) + { + Con::errorf("SimObject::deepClone() - Unable to create cloned object of class '%s'.", getClassName()); + return NULL; + } + + if ( !pCloneObject->registerObject() ) + { + Con::warnf("SimObject::deepClone() - Unable to register cloned object."); + delete pCloneObject; + return NULL; + } + + return pCloneObject; +} + +void SimObject::cloneInto(SimObject* pCloneObject) +{ + // Not the name and not the parent group: the clone is nameless and belongs to + // nothing until whoever asked for it puts it somewhere. And not the script + // class, which is left to copyTo below. + pCloneObject->copyFieldsFrom( this, + CopyFields_SkipName | CopyFields_SkipParentGroup | CopyFields_SkipScriptClass ); + + deepCloneChildren( pCloneObject ); + + // Last, and this is the point. copyTo is what sets the script class and links + // the namespaces, so until now the clone had no script class for anything to + // find a callback on: registerObject fires script onAdd, and adding a child + // fires the parent's script onChildAdded (guiControl.cc). A class whose onAdd + // or onChildAdded builds children would otherwise build a second set of them + // on top of the ones being copied - which is exactly what a copy must not do. + // + // Skipping the class in the field copy above is half of the same rule: class + // and superclass are ordinary persist fields, and their setters link the + // namespaces too, so copying them early would defeat this entirely. + copyTo( pCloneObject ); +} //----------------------------------------------------------------------------- diff --git a/engine/source/sim/simObject.h b/engine/source/sim/simObject.h index 11c74f560..3a47b2248 100755 --- a/engine/source/sim/simObject.h +++ b/engine/source/sim/simObject.h @@ -718,6 +718,24 @@ class SimObject: public ConsoleObject, public TamlCallbacks /// @param obj Object to copy from. void assignFieldsFrom(SimObject *obj); + /// What copyFieldsFrom may be told to leave alone. + enum CopyFieldsFlags + { + CopyFields_SkipName = BIT(0), ///< Two objects answering to one name is a bug. + CopyFields_SkipParentGroup = BIT(1), ///< Writing it ADDS the object to that group. + CopyFields_SkipScriptClass = BIT(2) ///< class and superclass, which link namespaces. + }; + + /// assignFieldsFrom, with the option of leaving some fields out. + /// + /// Every persist field is a field, including the two that decide where the + /// object lives and what it is called - so a copy that wants to be a copy + /// rather than a second reference to the same place has to say so. + /// + /// @param obj Object to copy from; must be of the same class. + /// @param flags CopyFieldsFlags, or 0 for everything. + void copyFieldsFrom(SimObject *obj, const U32 flags); + /// Copy dynamic fields from another object onto this one. /// /// Everything from obj will overwrite what's in this @@ -776,6 +794,44 @@ class SimObject: public ConsoleObject, public TamlCallbacks SimObject* clone( const bool copyDynamicFields ); virtual void copyTo(SimObject* object); + /// Copy this object, everything in it, and everything below it. + /// + /// Unlike clone(), which makes a shell of the right class and leaves the + /// fields to the caller, a deep clone is finished when it returns: fields, + /// dynamic fields and the whole child tree, with each child a new object of + /// its own. The name and the parent group are deliberately not copied - the + /// clone is nameless and belongs to nothing until someone adds it. + /// + /// No script lifecycle callback fires on a deep clone. See cloneInto. + SimObject* deepClone(); + +protected: + /// The three phases of a deep clone, separated because a child has to be + /// added to its new parent BEFORE its fields are written: a container that + /// places its own children takes what it wants from a child as it arrives + /// (a GuiChainCtrl zeroes the position, a GuiGridCtrl forces the sizing off + /// center), and doing that after the values were copied would undo them. + + /// A registered but empty object of this object's class. + virtual SimObject* allocClone(); + + /// Fill in a shell from allocClone: fields, then children, then the class. + /// + /// copyTo runs LAST, and that is the whole reason a deep clone is inert. + /// It is what sets mClassName/mSuperClassName and links the namespaces, and + /// until it has run the clone's script class is invisible: registerObject + /// fires script onAdd (simObject.cc) and GuiControl::onChildAdded fires + /// script onChildAdded (guiControl.cc), and neither can find a method on a + /// namespace that is not linked yet. So a class whose onAdd builds children + /// cannot build a second set of them on top of the ones being copied. + void cloneInto(SimObject* clone); + + /// Copy this object's children into %clone. Nothing to do here; SimGroup, + /// which is where owned children live, does the work. + virtual void deepCloneChildren(SimObject* clone) {} + +public: + template bool isType(void) { return dynamic_cast(this) != NULL; } // Component Console Overrides diff --git a/engine/source/sim/simObject_ScriptBinding.h b/engine/source/sim/simObject_ScriptBinding.h index 335add6d3..ffa04f9e1 100644 --- a/engine/source/sim/simObject_ScriptBinding.h +++ b/engine/source/sim/simObject_ScriptBinding.h @@ -906,6 +906,26 @@ ConsoleMethodWithDocs(SimObject, clone, ConsoleInt, 2, 3, ([copyDynamicFields = return pClonedObject->getId(); } +/*! Copies the object, everything in it, and everything below it. + Unlike clone(), a deep clone comes back finished: every field, every dynamic + field, and a new copy of every child, recursively. The copy has no name and + belongs to no group until you add it somewhere. + + No script callback runs on the copy while it is being made - not onAdd, not + onChildAdded - so a class that builds children of its own cannot end up with + two sets of them. + @return (newObjectID) The new object's id if successful, otherwise a 0. +*/ +ConsoleMethodWithDocs(SimObject, deepClone, ConsoleInt, 2, 2, ()) +{ + SimObject* pClonedObject = object->deepClone(); + + if ( pClonedObject == NULL ) + return 0; + + return pClonedObject->getId(); +} + /*! Takes all values from one object and puts them into anther object of the same class. This includes dynamic fields. @return No return value. */ diff --git a/engine/source/sim/simSet.cc b/engine/source/sim/simSet.cc index a9028247c..5b221729c 100755 --- a/engine/source/sim/simSet.cc +++ b/engine/source/sim/simSet.cc @@ -404,6 +404,37 @@ void SimGroup::onChildRemoved(SimObject* obj) ////////////////////////////////////////////////////////////////////////// +void SimGroup::deepCloneChildren(SimObject* clone) +{ + SimGroup* pCloneGroup = dynamic_cast( clone ); + if ( pCloneGroup == NULL ) + return; + + // The child is added while it is still empty, and only then filled in. That + // order is the point: a container that places its own children takes what it + // wants from a child as the child arrives - a GuiChainCtrl zeroes its + // position, a GuiGridCtrl forces its sizing off center, a GuiFrameSetCtrl + // puts it in a frame - and every one of those would overwrite the values a + // copy is trying to carry if they were written first. + // + // Order within the list is the arrival order, so the copy holds its children + // in the same order the original does. Anything that lays out by list order + // (which is all of them) therefore lays the copy out the same way. + for (iterator itr = begin(); itr != end(); itr++) + { + SimObject* pChild = *itr; + + SimObject* pChildClone = pChild->allocClone(); + if ( pChildClone == NULL ) + continue; + + pCloneGroup->addObject( pChildClone ); + pChild->cloneInto( pChildClone ); + } +} + +////////////////////////////////////////////////////////////////////////// + void SimGroup::onRemove() { lock(); diff --git a/engine/source/sim/simSet.h b/engine/source/sim/simSet.h index 625618ab1..54e61188a 100755 --- a/engine/source/sim/simSet.h +++ b/engine/source/sim/simSet.h @@ -318,6 +318,17 @@ class SimGroup: public SimSet bool processArguments(S32 argc, const char **argv); +protected: + /// Deep-clone every child into %clone. + /// + /// A group's children are its own - it is the only container in the engine + /// that owns what it holds - so a copy of a group is a copy of the tree under + /// it. A SimSet does not override this: its members belong to whatever group + /// holds them, and duplicating those would be inventing objects nobody asked + /// for. + virtual void deepCloneChildren(SimObject* clone); + +public: DECLARE_CONOBJECT(SimGroup); }; diff --git a/engine/source/testing/tests/simObjectCloneTests.cc b/engine/source/testing/tests/simObjectCloneTests.cc new file mode 100644 index 000000000..d45b5bcd0 --- /dev/null +++ b/engine/source/testing/tests/simObjectCloneTests.cc @@ -0,0 +1,392 @@ +//----------------------------------------------------------------------------- +// 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. +//----------------------------------------------------------------------------- + +// We don't want tests in a shipping version. +#ifndef TORQUE_SHIPPING + +#ifndef _UNIT_TESTING_H_ +#include "testing/unitTesting.h" +#endif + +#ifndef _SIMBASE_H_ +#include "sim/simBase.h" +#endif + +#ifndef _SIMSET_H_ +#include "sim/simSet.h" +#endif + +#ifndef _GUICONTROL_H_ +#include "gui/guiControl.h" +#endif + +#ifndef _GUIFRAMESETCTRL_H_ +#include "gui/containers/guiFrameSetCtrl.h" +#endif + +#ifndef _CONSOLE_H_ +#include "console/console.h" +#endif + +//----------------------------------------------------------------------------- +// SimObject::deepClone and the copyFieldsFrom it is built on. +// +// A deep clone exists for copy and paste in the Gui Editor, and the two things +// it promises are the two things a clipboard needs: the copy holds everything +// the original held, and nothing of the original's script lifecycle runs while +// it is being made. Both are asserted here, because both are orderings inside +// the implementation that nothing else would catch if they were reversed. +//----------------------------------------------------------------------------- + +static StringTableEntry fieldName( const char* name ) +{ + return StringTable->insert( name ); +} + +static const char* readField( SimObject* object, const char* name ) +{ + return object->getDataField( fieldName( name ), NULL ); +} + +static void writeField( SimObject* object, const char* name, const char* value ) +{ + object->setDataField( fieldName( name ), NULL, value ); +} + +//----------------------------------------------------------------------------- +// copyFieldsFrom: the two fields a copy must not take. +//----------------------------------------------------------------------------- + +TEST( SimObjectCloneTests, CopyFieldsFromCarriesOrdinaryFields ) +{ + SimObject* source = new SimObject(); + source->registerObject(); + writeField( source, "internalName", "carried" ); + writeField( source, "aDynamicField", "also carried" ); + + SimObject* target = new SimObject(); + target->registerObject(); + target->copyFieldsFrom( source, 0 ); + + ASSERT_STREQ( readField( target, "internalName" ), "carried" ); + ASSERT_STREQ( readField( target, "aDynamicField" ), "also carried" ) + << "Dynamic fields come across too, as assignFieldsFrom always did."; + + source->deleteObject(); + target->deleteObject(); +} + +// A protected field whose get function answers from the object rather than from +// the raw data it is handed - GuiControl's "text" is one (getTextProperty returns +// obj->getText()) - has to be read through the source. Reading it through the +// destination gives back what the destination already held, so the field silently +// does not copy at all. +TEST( SimObjectCloneTests, CopyFieldsFromCarriesProtectedFieldsWithCustomGetters ) +{ + GuiControl* source = new GuiControl(); + source->registerObject(); + writeField( source, "text", "Save As..." ); + writeField( source, "Extent", "123 45" ); + + GuiControl* clone = dynamic_cast( source->deepClone() ); + ASSERT_TRUE( clone != NULL ); + + ASSERT_STREQ( readField( clone, "text" ), "Save As..." ) + << "text is protected with a getter that ignores the data pointer."; + ASSERT_STREQ( readField( clone, "Extent" ), "123 45" ) + << "Extent is protected too, but with the default getter."; + + clone->deleteObject(); + source->deleteObject(); +} + +TEST( SimObjectCloneTests, CopyFieldsFromCanSkipTheName ) +{ + SimObject* source = new SimObject(); + source->registerObject( "CloneTestNamedSource" ); + + SimObject* withName = new SimObject(); + withName->registerObject(); + withName->copyFieldsFrom( source, 0 ); + ASSERT_STREQ( withName->getName(), "CloneTestNamedSource" ) + << "name is an ordinary persist field, so it copies unless asked not to."; + + SimObject* withoutName = new SimObject(); + withoutName->registerObject(); + withoutName->copyFieldsFrom( source, SimObject::CopyFields_SkipName ); + ASSERT_TRUE( withoutName->getName() == NULL || withoutName->getName()[0] == '\0' ) + << "Two objects answering to one name is the bug this flag exists for."; + + source->deleteObject(); + withName->deleteObject(); + withoutName->deleteObject(); +} + +TEST( SimObjectCloneTests, CopyFieldsFromCanSkipTheParentGroup ) +{ + SimGroup* group = new SimGroup(); + group->registerObject(); + + SimObject* source = new SimObject(); + source->registerObject(); + group->addObject( source ); + + // Copying parentGroup does not record where the object lives, it moves the + // object there: the field's setter calls parent->addObject(). + SimObject* moved = new SimObject(); + moved->registerObject(); + moved->copyFieldsFrom( source, 0 ); + ASSERT_TRUE( moved->getGroup() == group ) + << "parentGroup is a persist field whose setter adds the object to the group."; + + SimObject* homeless = new SimObject(); + homeless->registerObject(); + homeless->copyFieldsFrom( source, SimObject::CopyFields_SkipParentGroup ); + ASSERT_TRUE( homeless->getGroup() == NULL ) + << "A copy belongs nowhere until someone puts it somewhere."; + + homeless->deleteObject(); + group->deleteObject(); // takes source and moved with it +} + +//----------------------------------------------------------------------------- +// deepClone. +//----------------------------------------------------------------------------- + +TEST( SimObjectCloneTests, DeepCloneCopiesFieldsButNotIdentity ) +{ + SimGroup* group = new SimGroup(); + group->registerObject(); + + SimObject* source = new SimObject(); + source->registerObject( "CloneTestDeepSource" ); + group->addObject( source ); + writeField( source, "internalName", "inner" ); + writeField( source, "aDynamicField", "dynamic" ); + + SimObject* clone = source->deepClone(); + ASSERT_TRUE( clone != NULL ); + ASSERT_TRUE( clone != source ); + + ASSERT_STREQ( readField( clone, "internalName" ), "inner" ); + ASSERT_STREQ( readField( clone, "aDynamicField" ), "dynamic" ); + ASSERT_TRUE( clone->getName() == NULL || clone->getName()[0] == '\0' ); + ASSERT_TRUE( clone->getGroup() == NULL ); + + clone->deleteObject(); + group->deleteObject(); +} + +TEST( SimObjectCloneTests, DeepCloneCopiesTheWholeTree ) +{ + SimGroup* source = new SimGroup(); + source->registerObject(); + + SimObject* child = new SimObject(); + child->registerObject(); + source->addObject( child ); + writeField( child, "internalName", "child" ); + + SimGroup* grandChildHolder = new SimGroup(); + grandChildHolder->registerObject(); + source->addObject( grandChildHolder ); + + SimObject* grandChild = new SimObject(); + grandChild->registerObject(); + grandChildHolder->addObject( grandChild ); + writeField( grandChild, "internalName", "grandChild" ); + + SimGroup* clone = dynamic_cast( source->deepClone() ); + ASSERT_TRUE( clone != NULL ); + ASSERT_EQ( clone->size(), 2 ); + + // Deep, not shared: every object below the clone is a new object. + ASSERT_TRUE( (*clone)[0] != child ); + ASSERT_STREQ( readField( (*clone)[0], "internalName" ), "child" ); + + SimGroup* clonedHolder = dynamic_cast( (*clone)[1] ); + ASSERT_TRUE( clonedHolder != NULL ); + ASSERT_EQ( clonedHolder->size(), 1 ); + ASSERT_TRUE( (*clonedHolder)[0] != grandChild ); + ASSERT_STREQ( readField( (*clonedHolder)[0], "internalName" ), "grandChild" ); + + clone->deleteObject(); + source->deleteObject(); +} + +TEST( SimObjectCloneTests, DeepCloneOfASimSetDoesNotDuplicateItsMembers ) +{ + // A SimSet references objects some group owns; duplicating those would be + // inventing objects nobody asked for. Only SimGroup recurses. + SimSet* set = new SimSet(); + set->registerObject(); + + SimObject* member = new SimObject(); + member->registerObject(); + set->addObject( member ); + + SimSet* clone = dynamic_cast( set->deepClone() ); + ASSERT_TRUE( clone != NULL ); + ASSERT_EQ( clone->size(), 0 ); + + clone->deleteObject(); + set->deleteObject(); + member->deleteObject(); +} + +//----------------------------------------------------------------------------- +// The promise that makes a deep clone usable as a clipboard: no script +// lifecycle callback fires on it. +// +// copyTo runs last in cloneInto, which is what makes this true - it is what +// links the namespaces, so while the clone is being filled in there is no +// script class on it for registerObject or onChildAdded to find a method on. +// A class whose onAdd builds children would otherwise build a second set on top +// of the ones being copied. +//----------------------------------------------------------------------------- + +TEST( SimObjectCloneTests, DeepCloneRunsNoScriptLifecycle ) +{ + Con::evaluate( + "function CloneTestProbe::onAdd(%this) { $CloneTestProbeAdds = $CloneTestProbeAdds + 1; }\n" + "function CloneTestProbe::onChildAdded(%this, %child) { $CloneTestProbeChildAdds = $CloneTestProbeChildAdds + 1; }\n", + false, NULL ); + + Con::setIntVariable( "$CloneTestProbeAdds", 0 ); + Con::setIntVariable( "$CloneTestProbeChildAdds", 0 ); + + GuiControl* source = new GuiControl(); + writeField( source, "class", "CloneTestProbe" ); + source->registerObject(); + + ASSERT_EQ( Con::getIntVariable( "$CloneTestProbeAdds" ), 1 ) + << "The original really does have a class whose onAdd runs."; + + GuiControl* child = new GuiControl(); + child->registerObject(); + source->addObject( child ); + + ASSERT_EQ( Con::getIntVariable( "$CloneTestProbeChildAdds" ), 1 ) + << "And a class whose onChildAdded runs."; + + Con::setIntVariable( "$CloneTestProbeAdds", 0 ); + Con::setIntVariable( "$CloneTestProbeChildAdds", 0 ); + + GuiControl* clone = dynamic_cast( source->deepClone() ); + ASSERT_TRUE( clone != NULL ); + + ASSERT_EQ( Con::getIntVariable( "$CloneTestProbeAdds" ), 0 ) + << "A deep clone is data: onAdd must not run on it."; + ASSERT_EQ( Con::getIntVariable( "$CloneTestProbeChildAdds" ), 0 ) + << "Nor onChildAdded, or a class that builds children would double them."; + + ASSERT_EQ( clone->size(), 1 ) + << "Exactly the children the original had, and no more."; + ASSERT_STREQ( readField( clone, "class" ), "CloneTestProbe" ) + << "The class itself does come across - copyTo is what copies it."; + + clone->deleteObject(); + source->deleteObject(); +} + +//----------------------------------------------------------------------------- +// GuiFrameSetCtrl, the one control whose layout is not in its field list. +//----------------------------------------------------------------------------- + +static U32 countFrames( const GuiFrameSetCtrl::Frame* frame ) +{ + if ( frame == NULL ) + return 0; + + return 1 + countFrames( frame->child1 ) + countFrames( frame->child2 ); +} + +static bool framesMatch( const GuiFrameSetCtrl::Frame* a, const GuiFrameSetCtrl::Frame* b ) +{ + if ( a == NULL || b == NULL ) + return a == b; + + if ( a->id != b->id || a->isVertical != b->isVertical || + a->isAnchored != b->isAnchored || a->extent != b->extent ) + return false; + + if ( (a->control == NULL) != (b->control == NULL) ) + return false; + + return framesMatch( a->child1, b->child1 ) && framesMatch( a->child2, b->child2 ); +} + +// Every control the tree holds is one of %owner's own children. +static bool framesHoldOwnChildren( const GuiFrameSetCtrl::Frame* frame, GuiFrameSetCtrl* owner ) +{ + if ( frame == NULL ) + return true; + + if ( frame->control != NULL && frame->control->getGroup() != owner ) + return false; + + return framesHoldOwnChildren( frame->child1, owner ) && + framesHoldOwnChildren( frame->child2, owner ); +} + +TEST( SimObjectCloneTests, DeepCloneRebuildsAFrameSetsTree ) +{ + GuiFrameSetCtrl* source = new GuiFrameSetCtrl(); + source->registerObject(); + source->resize( Point2I( 0, 0 ), Point2I( 400, 200 ) ); + + // Root frame is 1. Split it, then split the right half, for three leaves. + const Point2I halves = source->splitFrame( 1, false ); + source->splitFrame( halves.y, true ); + + for ( U32 i = 0; i < 3; i++ ) + { + GuiControl* panel = new GuiControl(); + panel->registerObject(); + source->addObject( panel ); + } + + // Settle the tree before anything is measured. A split leaves its new frames + // at their constructed extent until the control resizes, and the copy ends + // with a resize of its own (as setFrameLayout does) - so an unsettled source + // would differ from its copy over extents neither of them had been told yet. + source->resize( Point2I( 0, 0 ), Point2I( 400, 200 ) ); + + ASSERT_EQ( countFrames( &source->mRootFrame ), 5u ) + << "Two splits make five frames: a root, two halves, and two under one of them."; + ASSERT_TRUE( framesHoldOwnChildren( &source->mRootFrame, source ) ); + + GuiFrameSetCtrl* clone = dynamic_cast( source->deepClone() ); + ASSERT_TRUE( clone != NULL ); + ASSERT_EQ( clone->size(), 3 ); + + ASSERT_EQ( countFrames( &clone->mRootFrame ), countFrames( &source->mRootFrame ) ); + ASSERT_TRUE( framesMatch( &clone->mRootFrame, &source->mRootFrame ) ) + << "Same shape, same ids, same extents, same anchoring, same frames filled."; + ASSERT_TRUE( framesHoldOwnChildren( &clone->mRootFrame, clone ) ) + << "And filled with the CLONE's children, not the original's."; + + clone->deleteObject(); + source->deleteObject(); +} + +#endif // TORQUE_SHIPPING From 0c02d3168fe7b7620a40e97ba7c410197c314bfb Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Wed, 29 Jul 2026 21:36:49 -0400 Subject: [PATCH 17/55] Gui Editor: copy, cut and paste The three stubs left on the Edit menu. Everything around them was already built: the menu items and their Ctrl+X/C/V accelerators have been there all along, the canvas offers a key to the first responder before the accelerator map (so a text box in the properties pane keeps Ctrl+C for its own text and the canvas only gets it when nothing else wanted it), and undo's recorder turns an add or a trash into one step. What was missing was a faithful way to duplicate a control, which the previous commit adds. GuiEditorClipboard owns a SimGroup outside the document and deep-clones the selection into it, so the clipboard is a snapshot: editing or deleting the original afterwards does not change what will be pasted. Paste clones a second time out of that stash, which is what makes pasting repeatedly free. Cut is a copy plus the delete the Delete key already does -- the trash, and onTrashSelection -- so it was undoable the moment it was written. The legacy file clipboard in guiEditCtrl.cc (saveSelection/loadSelection) is left exactly as it is, and still unused. It cannot do this job: the .cs writer drops dynamic fields, never writes a frame set's frame tree, and duplicates names. Choices worth writing down. The selection is reduced to the controls no other selected control already contains, in document order, by walking the document rather than reading the selection: selecting a panel and a button inside it is one copy, not two, and walking settles the order so the copies keep the originals' z-order. Placement steps per container. A paste into the container the copy came from lands one grid step off so it is not hidden exactly behind the original; a paste anywhere else keeps the position it had; each further paste into a given container steps again. That last part is per container on purpose -- one running count looks right until you paste into A, then B, then A again, and the third lands exactly on the first. Names are uniquified: okButton becomes okButton2 and then okButton3, and a name already ending in digits counts on from them rather than growing another. deepClone deliberately drops names, so each copy carries its original's in a dynamic field the paste consumes and clears. Uniqueness is tested by walking the document, because the editor runs in editor mode where assignName stashes a name instead of registering it, so isObject() cannot answer. A cut and paste therefore keeps the name it had -- the original is in the trash, which is not the document, so nothing there holds it. onControlDropped is split. acceptControl is now everything about a control arriving except where it lands -- the undo record, theming on arrival, the selection, the AddControl event the Explorer tree listens for -- and a drop is that plus its two global-coordinate placements. Paste sets a local position before calling it instead, for a reason worth knowing: a control's mRenderInsetLT is its PARENT's content inset, written onto the child when the parent renders it, so a control that has not been drawn in its new container has none. Handing the drop path a global position therefore lands the paste correctly and then shifts it by the container's border thickness 40ms later, when the re-assert fires against an inset that now exists. The clipboard is cleared in detachTheme beside the undo stack, and for the same reason: the copies hold GuiControlProfile pointers by raw field, and a paste after the library freed one would read freed memory. It deliberately survives New Gui and Open Gui, so a copy taken from one Gui can be pasted into the next. Verified: tests/smoke/clipboard.cs, 99 checks -- placement, one step per paste however many controls it puts back, undo into the trash and redo back out, a panel copied with its children renamed, dynamic fields, a frame set's tree, a control whose class builds a child in onAdd holding exactly one child afterwards, the menu items following the selection and the clipboard, and the theme-deletion clear. Full suite green, all 25 as expected; undo.cs loses the assertion that Cut is still a stub. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qi2pZ9UzF5rsLCNVErpzwt --- editor/GuiEditor/GuiEditor.cs | 66 +- editor/GuiEditor/scripts/GuiEditorBrain.cs | 32 +- .../GuiEditor/scripts/GuiEditorClipboard.cs | 455 +++++++++++ tests/run.ps1 | 4 +- tests/smoke/clipboard.cs | 723 ++++++++++++++++++ tests/smoke/undo.cs | 7 +- 6 files changed, 1270 insertions(+), 17 deletions(-) create mode 100644 editor/GuiEditor/scripts/GuiEditorClipboard.cs create mode 100644 tests/smoke/clipboard.cs diff --git a/editor/GuiEditor/GuiEditor.cs b/editor/GuiEditor/GuiEditor.cs index 74f8d9fc6..2f50322d4 100644 --- a/editor/GuiEditor/GuiEditor.cs +++ b/editor/GuiEditor/GuiEditor.cs @@ -68,6 +68,9 @@ 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"); + %this.guiPage = EditorCore.RegisterEditor("Gui Editor", %this); // What the control palette can offer and what each entry looks like. Built @@ -107,6 +110,15 @@ 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() @@ -365,6 +377,13 @@ class = "SimulatedCanvas"; %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(); + } } function GuiEditor::open(%this, %content) @@ -381,12 +400,13 @@ class = "SimulatedCanvas"; EditorCore.menuBar.setMenuActive("Layout", true); EditorCore.menuBar.setMenuActive("Select", true); - // Undo and Redo are greyed from the stacks; the other three items on the Edit - // menu are still stubs and must not look live. - EditorCore.menuBar.setMenuActive("Cut", false); - EditorCore.menuBar.setMenuActive("Copy", false); - EditorCore.menuBar.setMenuActive("Paste", false); + // Undo and Redo are greyed from the stacks, Cut and Copy from the selection, + // and Paste from whether anything has been copied. All three of the last are + // cached against what the menu was last told, so they are forced here: the + // menu looks new every time the editor is opened. %this.undoRecorder.forceRefreshMenu(); + %this.clipboard.forceRefreshMenu(); + %this.brain.toggleMenuItems(); editorMode(true); } @@ -699,6 +719,11 @@ class = "GuiEditorThemeDialog"; // 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); } @@ -874,19 +899,40 @@ class = "GuiEditorThemeDialog"; return false; } -function GuiEditor::Cut(%this) +//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()); } -function GuiEditor::Copy(%this) +// Copy, then the delete the Delete key already does: the C++ moves the selection +// into the trash and announces it, which the recorder turns into one undo step +// (GuiEditorBrain::onTrashSelection). Nothing is deleted for real, so a cut is +// undoable and the controls it took 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. +function GuiEditor::Cut(%this) { - + if(!%this.clipboard.copy(%this.brain.getSelected())) + { + return; + } + + %this.brain.deleteSelection(); + %this.brain.onDelete(); } function GuiEditor::Paste(%this) { - + %this.clipboard.paste(); } //LAYOUT----------------------------------------------------------------------- diff --git a/editor/GuiEditor/scripts/GuiEditorBrain.cs b/editor/GuiEditor/scripts/GuiEditorBrain.cs index 2eba9fd38..928d2c0b7 100644 --- a/editor/GuiEditor/scripts/GuiEditorBrain.cs +++ b/editor/GuiEditor/scripts/GuiEditorBrain.cs @@ -28,6 +28,24 @@ %x = getWord(%pos, 0); %y = getWord(%pos, 1); + %this.acceptControl(%payload); + + %payload.setPositionGlobal(%x, %y); + %this.schedule(40, "finishControlDropped", %payload, %x, %y); +} + +// 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); @@ -58,11 +76,9 @@ %this.postEvent("Rethemed", %payload); } - %payload.setPositionGlobal(%x, %y); %this.setFirstResponder(); %this.postEvent("AddControl", %payload); - %this.postEvent("Inspect", %payload); - %this.schedule(40, "finishControlDropped", %payload, %x, %y); + %this.postEvent("Inspect", %payload); } function GuiEditorBrain::finishControlDropped(%this, %payload, %x, %y) @@ -218,6 +234,12 @@ } // 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 @@ -225,7 +247,7 @@ // 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::restoreSelection(%this, %list) +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 @@ -268,6 +290,8 @@ { %count = %this.getSelected().getCount(); EditorCore.menuBar.setMenuActive("Deselect", %count != 0); + EditorCore.menuBar.setMenuActive("Cut", %count != 0); + EditorCore.menuBar.setMenuActive("Copy", %count != 0); EditorCore.menuBar.setMenuActive("Nudge Up", %count != 0); EditorCore.menuBar.setMenuActive("Nudge Down", %count != 0); EditorCore.menuBar.setMenuActive("Nudge Left", %count != 0); diff --git a/editor/GuiEditor/scripts/GuiEditorClipboard.cs b/editor/GuiEditor/scripts/GuiEditorClipboard.cs new file mode 100644 index 000000000..41412ba2e --- /dev/null +++ b/editor/GuiEditor/scripts/GuiEditorClipboard.cs @@ -0,0 +1,455 @@ + +//----------------------------------------------------------------------------- +// The Gui Editor's clipboard. Owned by GuiEditor; the only thing that holds a +// copied control, and the only thing that puts one back. +// +// 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; + + // What the Edit menu was last told about Paste. setMenuActive walks the whole + // menu tree and re-applies every item's profile, so it is worth not saying + // the same thing twice. + %this.menuPaste = -1; +} + +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; + } + + // 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; +} + +//----------------------------------------------------------------------------- +// 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) +{ + %has = %this.isEmpty() ? 0 : 1; + + if(%has == %this.menuPaste) + { + return; + } + + %this.menuPaste = %has; + + if(isObject(EditorCore) && isObject(EditorCore.menuBar)) + { + EditorCore.menuBar.setMenuActive("Paste", %has); + } +} + +// The menu looks rebuilt every time the editor is opened, and the cache above +// would otherwise decide there was nothing to say. +function GuiEditorClipboard::forceRefreshMenu(%this) +{ + %this.menuPaste = -1; + %this.refreshMenu(); +} diff --git a/tests/run.ps1 b/tests/run.ps1 index 75802885c..6c8b5c199 100644 --- a/tests/run.ps1 +++ b/tests/run.ps1 @@ -73,8 +73,8 @@ $KeepProject = @('bitmapPathRead') $Order = @( 'profileEditor', 'profileForm', 'border', 'borderPane', 'standalone', 'headerPane', 'colorPopup', 'themeApply', 'font', 'assetPicker', - 'tooltipProfile', 'textClick', 'undo', 'bitmapPathWrite', 'bitmapPathRead', - 'toybox', 'planetX' + 'tooltipProfile', 'textClick', 'undo', 'clipboard', 'bitmapPathWrite', + 'bitmapPathRead', 'toybox', 'planetX' ) if (-not (Test-Path $Exe)) { diff --git a/tests/smoke/clipboard.cs b/tests/smoke/clipboard.cs new file mode 100644 index 000000000..baae37c8c --- /dev/null +++ b/tests/smoke/clipboard.cs @@ -0,0 +1,723 @@ +//----------------------------------------------------------------------------- +// Copy, cut and paste in the Gui Editor. Boots the editor, opens the PlanetX +// project so there is a real theme to work with, and puts the clipboard through +// what a person does with it: copy a control and paste it, paste it again, paste +// it somewhere else, copy a whole panel, cut something. +// +// The three things that are easy to get wrong and are checked hardest: a paste +// is ONE undo step however much it puts back, a copy is a real copy (children, +// dynamic fields, a frame set's frames, and no second helping of whatever a +// control's class builds for itself), and no two controls end up sharing a name. +// Run: tests/run.ps1 clipboard ; grep CLIP in console.log. +//----------------------------------------------------------------------------- + +setLogMode(2); +setScriptExecEcho(false); +trace(false); +$Scripts::ignoreDSOs = true; +setCompanyAndProduct("Torque Game Engines", "Torque2D"); +ModuleDatabase.EchoInfo = false; +AssetDatabase.EchoInfo = false; +AssetDatabase.IgnoreAutoUnload = true; + +testExec("editor/main.cs"); + +function cCheck(%label, %condition) +{ + echo(%condition ? ("CLIP PASS: " @ %label) : ("CLIP FAIL: " @ %label)); +} + +function cUndoCount() +{ + return GuiEditor.undoRecorder.undoCount(); +} + +function cRedoCount() +{ + return GuiEditor.undoRecorder.redoCount(); +} + +function cSelect(%ctrl) +{ + GuiEditor.brain.clearSelection(); + GuiEditor.brain.select(%ctrl); +} + +function cIndexOf(%parent, %ctrl) +{ + for(%i = 0; %i < %parent.getCount(); %i++) + { + if(%parent.getObject(%i) == %ctrl) + { + return %i; + } + } + return -1; +} + +// What a paste selected, which is what it pasted. +function cPasted(%index) +{ + %set = GuiEditor.brain.getSelected(); + return %set.getObject(%index); +} + +function cPastedCount() +{ + %set = GuiEditor.brain.getSelected(); + return %set.getCount(); +} + +// Comparing against "" proves nothing: an absent dynamic field and an empty one +// read back the same, because an empty one is exactly what the engine deletes. +function cHasDynamicField(%ctrl, %name) +{ + for(%i = 0; %i < %ctrl.getDynamicFieldCount(); %i++) + { + if(getWord(%ctrl.getDynamicField(%i), 0) $= %name) + { + return true; + } + } + return false; +} + +// Menu items are nested controls, so this walks rather than indexes. +function cMenuItem(%parent, %text) +{ + for(%i = 0; %i < %parent.getCount(); %i++) + { + %item = %parent.getObject(%i); + if(%item.Text $= %text) + { + return %item; + } + + %found = cMenuItem(%item, %text); + if(isObject(%found)) + { + return %found; + } + } + + return 0; +} + +// A frame set's tree with the control ids taken out: those are object ids, so a +// copy's are necessarily different. What has to match is the shape - every +// frame's id, split direction, extent and anchoring - and which frames hold a +// control at all. +function cFrameShape(%layout) +{ + %shape = ""; + %count = getWordCount(%layout); + + for(%i = 0; (%i + 7) < %count; %i += 8) + { + for(%j = 0; %j < 7; %j++) + { + %shape = %shape @ getWord(%layout, %i + %j) @ " "; + } + %shape = %shape @ ((getWord(%layout, %i + 7) != 0) ? "1" : "0") @ " "; + } + + return %shape; +} + +// A control class that builds a child of its own the moment it is created, which +// is the house pattern (TORQUE_SCRIPT.md) and the thing a copy must not do twice. +function ClipProbe::onAdd(%this) +{ + %kid = new GuiControl() + { + Position = "4 4"; + Extent = "20 20"; + }; + %this.add(%kid); +} + +schedule(2000, 0, "cStep1"); + +function cStep1() +{ + ProjectManager.setProjectFolder("PlanetX"); + ModuleDatabase.ScanModules("PlanetX"); + ModuleDatabase.LoadExplicit("AppCore", 1); + + $cTheme = nameToID("PlanetX"); + cCheck("PlanetX theme loaded", isObject($cTheme)); + + GuiEditor.open(); + cCheck("the editor built a clipboard", isObject(GuiEditor.clipboard)); + cCheck("which starts empty", GuiEditor.clipboard.isEmpty()); + + // Before anything has been copied, so this is the only moment it can be + // checked. + %pasteItem = cMenuItem(EditorCore.menuBar, "Paste"); + cCheck("Paste is greyed with nothing on the clipboard", !%pasteItem.Active); + + // The stage: a panel with three named children, and a second container to + // paste into. + $cPanel = new GuiControl() { Position = "10 10"; Extent = "400 300"; }; + GuiEditor.rootGui.add($cPanel); + + $cA = new GuiButtonCtrl(clipA) { Position = "10 10"; Extent = "80 30"; Text = "A"; }; + $cPanel.add($cA); + $cB = new GuiButtonCtrl(clipB) { Position = "10 50"; Extent = "80 30"; Text = "B"; }; + $cPanel.add($cB); + $cC = new GuiButtonCtrl(clipC) { Position = "10 90"; Extent = "80 30"; Text = "C"; }; + $cPanel.add($cC); + + $cOther = new GuiControl() { Position = "10 320"; Extent = "300 120"; }; + GuiEditor.rootGui.add($cOther); + + GuiEditor.setTheme($cTheme, false); + GuiEditor.undoRecorder.clear(); + + cCheck("the stage's controls are named", $cA.getName() $= "clipA"); + cCheck("the stack starts empty", cUndoCount() == 0 && cRedoCount() == 0); + + schedule(300, 0, "cStepCopyPaste"); +} + +//----------------------------------------------------------------------------- +// One control: copy, paste, and what the paste is worth on the undo stack. +//----------------------------------------------------------------------------- + +function cStepCopyPaste() +{ + GuiEditor.undoRecorder.clear(); + %grid = GuiEditor.brain.getGridSize(); + + cSelect($cA); + GuiEditor.Copy(); + + cCheck("a copy fills the clipboard", !GuiEditor.clipboard.isEmpty()); + cCheck("and records nothing on the undo stack", cUndoCount() == 0); + cCheck("and does not touch the original", $cA.getParent() == $cPanel); + + %pasteItem = cMenuItem(EditorCore.menuBar, "Paste"); + cCheck("Paste is offered once something is copied", %pasteItem.Active); + + %before = $cPanel.getCount(); + GuiEditor.Paste(); + + %copy = cPasted(0); + cCheck("a paste is one step", cUndoCount() == 1); + cCheck("it added one control", $cPanel.getCount() == (%before + 1)); + cCheck("into the container the original was in", %copy.getParent() == $cPanel); + cCheck("and it is a different object", %copy != $cA); + cCheck("of the same class", %copy.getClassName() $= $cA.getClassName()); + cCheck("with the same extent", %copy.getExtent() $= $cA.getExtent()); + cCheck("the same caption", %copy.Text $= $cA.Text); + cCheck("and the same profile (" @ %copy.getFieldValue("Profile") @ ")", + %copy.getFieldValue("Profile") $= $cA.getFieldValue("Profile")); + cCheck("the paste is selected", cPastedCount() == 1); + + // Stepped one grid line so it is not hidden exactly behind the original. + %wanted = (getWord($cA.getPosition(), 0) + %grid) SPC + (getWord($cA.getPosition(), 1) + %grid); + cCheck("stepped off the original (" @ %copy.getPosition() @ " wanted " @ %wanted @ ")", + %copy.getPosition() $= %wanted); + + // The name is the original's, made unique - not shared with it. + cCheck("the copy was renamed (" @ %copy.getName() @ ")", %copy.getName() $= "clipA2"); + cCheck("and the original kept its own name", $cA.getName() $= "clipA"); + cCheck("with no marker left behind", !cHasDynamicField(%copy, "clipName")); + + // Undo does not delete: the control lives in the trash, which is what leaves + // redo something to put back. + %trash = GuiEditor.brain.getTrash(); + GuiEditor.Undo(); + cCheck("undo took the paste out of the Gui", %copy.getGroup() == %trash); + cCheck("but did not delete it", isObject(%copy)); + cCheck("and the container is back to what it held", $cPanel.getCount() == %before); + + GuiEditor.Redo(); + cCheck("redo pasted it again", %copy.getParent() == $cPanel); + cCheck("still named for the original", %copy.getName() $= "clipA2"); + + // A second paste while the first copy is still in the Gui: it has to step past + // it and be named around it, rather than colliding with what it just made. + GuiEditor.Paste(); + %twice = cPasted(0); + + %wanted = (getWord($cA.getPosition(), 0) + (2 * %grid)) SPC + (getWord($cA.getPosition(), 1) + (2 * %grid)); + cCheck("a second paste steps past the first (" @ %twice.getPosition() @ ")", + %twice.getPosition() $= %wanted); + cCheck("and is named around it (" @ %twice.getName() @ ")", %twice.getName() $= "clipA3"); + + // Put the stage back the way the next step expects to find it. + GuiEditor.Undo(); + GuiEditor.Undo(); + cCheck("the stage is back to three children", $cPanel.getCount() == 3); + + schedule(300, 0, "cStepRepeat"); +} + +//----------------------------------------------------------------------------- +// Pasting again, and pasting elsewhere. Nothing is ever laid exactly on top of +// something already pasted. +//----------------------------------------------------------------------------- + +function cStepRepeat() +{ + GuiEditor.undoRecorder.clear(); + %grid = GuiEditor.brain.getGridSize(); + %from = $cA.getPosition(); + + // A fresh copy, which starts the stepping over. + GuiEditor.brain.setCurrentAddSet($cPanel); + cSelect($cA); + GuiEditor.Copy(); + + GuiEditor.Paste(); + %first = cPasted(0); + cCheck("the first paste steps once (" @ %first.getPosition() @ ")", + %first.getPosition() $= ((getWord(%from, 0) + %grid) SPC (getWord(%from, 1) + %grid))); + + // A different container: the position it had, unstepped, because there is + // nothing there to hide behind. + cSelect($cOther); + GuiEditor.brain.setCurrentAddSet($cOther); + GuiEditor.Paste(); + %second = cPasted(0); + + cCheck("pasting elsewhere puts it in that container", %second.getParent() == $cOther); + cCheck("at the position it had (" @ %second.getPosition() @ ")", + %second.getPosition() $= %from); + + // Back to the panel, where a paste has already been. The step carries on from + // where that container left off rather than starting again, or this paste + // would land exactly on the copy already sitting there. + GuiEditor.brain.setCurrentAddSet($cPanel); + GuiEditor.Paste(); + %third = cPasted(0); + + cCheck("coming back to a container carries on stepping (" @ %third.getPosition() @ ")", + %third.getPosition() $= ((getWord(%from, 0) + (2 * %grid)) SPC + (getWord(%from, 1) + (2 * %grid)))); + cCheck("so it does not land on the copy already there", + %third.getPosition() !$= %first.getPosition()); + cCheck("and all three are different objects", + %first != %second && %second != %third && %first != %third); + + // Tidy up: three pastes, three steps back. + GuiEditor.Undo(); + GuiEditor.Undo(); + GuiEditor.Undo(); + cCheck("all three pastes came back off the stack", cUndoCount() == 0); + cCheck("and the panel holds what it started with", $cPanel.getCount() == 3); + cCheck("as does the other container", $cOther.getCount() == 0); + + schedule(300, 0, "cStepMultiple"); +} + +//----------------------------------------------------------------------------- +// More than one control at once, and the reduction that stops a control being +// pasted twice. +//----------------------------------------------------------------------------- + +function cStepMultiple() +{ + GuiEditor.undoRecorder.clear(); + GuiEditor.brain.setCurrentAddSet($cPanel); + + GuiEditor.brain.clearSelection(); + GuiEditor.brain.select($cA); + GuiEditor.brain.addSelection($cC); + GuiEditor.Copy(); + + %before = $cPanel.getCount(); + GuiEditor.Paste(); + + cCheck("pasting two controls is still one step", cUndoCount() == 1); + cCheck("and both arrived", $cPanel.getCount() == (%before + 2)); + cCheck("both are selected", cPastedCount() == 2); + + // Document order, not selection order: the copies sit in the same z-order as + // the originals. + %firstCopy = cPasted(0); + %secondCopy = cPasted(1); + cCheck("in the order they were in the document", + %firstCopy.Text $= "A" && %secondCopy.Text $= "C"); + + GuiEditor.Undo(); + cCheck("one undo took both back", $cPanel.getCount() == %before); + cCheck("and left nothing on the undo stack", cUndoCount() == 0); + + // A control inside another selected control is already coming along. + GuiEditor.undoRecorder.clear(); + GuiEditor.brain.clearSelection(); + GuiEditor.brain.select($cPanel); + GuiEditor.brain.addSelection($cB); + GuiEditor.Copy(); + + %rootBefore = GuiEditor.rootGui.getCount(); + GuiEditor.brain.setCurrentAddSet(GuiEditor.rootGui); + GuiEditor.Paste(); + + cCheck("selecting a container and something inside it pastes one control", + GuiEditor.rootGui.getCount() == (%rootBefore + 1)); + cCheck("which is the container", cPastedCount() == 1); + + %panelCopy = cPasted(0); + cCheck("and it brought its three children (" @ %panelCopy.getCount() @ ")", + %panelCopy.getCount() == 3); + + GuiEditor.Undo(); + cCheck("and that was one step too", GuiEditor.rootGui.getCount() == %rootBefore); + + schedule(300, 0, "cStepNested"); +} + +//----------------------------------------------------------------------------- +// A copy of a whole panel: everything below it comes across, and none of it +// shares a name with the original. +//----------------------------------------------------------------------------- + +function cStepNested() +{ + GuiEditor.undoRecorder.clear(); + GuiEditor.brain.setCurrentAddSet(GuiEditor.rootGui); + + cSelect($cPanel); + GuiEditor.Copy(); + GuiEditor.Paste(); + + %copy = cPasted(0); + cCheck("the panel copy holds three children", %copy.getCount() == 3); + + %childA = %copy.getObject(0); + %childB = %copy.getObject(1); + %childC = %copy.getObject(2); + + cCheck("the children are new objects", + %childA != $cA && %childB != $cB && %childC != $cC); + cCheck("in the same order", %childA.Text $= "A" && %childC.Text $= "C"); + cCheck("with the positions they had", + %childA.getPosition() $= $cA.getPosition() && + %childC.getPosition() $= $cC.getPosition()); + cCheck("and the extents they had", %childB.getExtent() $= $cB.getExtent()); + cCheck("wearing the same profiles", + %childB.getFieldValue("Profile") $= $cB.getFieldValue("Profile")); + + cCheck("every child was renamed (" @ %childA.getName() SPC %childB.getName() SPC + %childC.getName() @ ")", + %childA.getName() !$= "clipA" && %childB.getName() !$= "clipB" && + %childC.getName() !$= "clipC"); + cCheck("and named for what it was", %childA.getName() $= "clipA2"); + cCheck("with no markers left behind", + !cHasDynamicField(%childA, "clipName") && !cHasDynamicField(%childC, "clipName")); + + GuiEditor.Undo(); + cCheck("undoing the panel paste takes the whole branch", cUndoCount() == 0); + + schedule(300, 0, "cStepDynamic"); +} + +//----------------------------------------------------------------------------- +// Dynamic fields, which the .cs writer would have dropped - the reason the +// clipboard clones rather than serialises. +//----------------------------------------------------------------------------- + +function cStepDynamic() +{ + GuiEditor.undoRecorder.clear(); + GuiEditor.brain.setCurrentAddSet($cPanel); + + GuiEditor.undoRecorder.writeDynamicField($cB, "cSmokeTag", "hello"); + cCheck("the original carries a dynamic field", cHasDynamicField($cB, "cSmokeTag")); + + cSelect($cB); + GuiEditor.Copy(); + GuiEditor.Paste(); + + %copy = cPasted(0); + cCheck("the copy carries it too", cHasDynamicField(%copy, "cSmokeTag")); + cCheck("with the value it had", %copy.cSmokeTag $= "hello"); + + GuiEditor.Undo(); + GuiEditor.undoRecorder.clear(); + $cB.cSmokeTag = ""; + + schedule(300, 0, "cStepClass"); +} + +//----------------------------------------------------------------------------- +// The one a clipboard built on serialising, or on plain construction, gets +// wrong: a control whose class builds a child in onAdd. The copy must hold the +// children the original has, and not a second set of them. +//----------------------------------------------------------------------------- + +function cStepClass() +{ + GuiEditor.undoRecorder.clear(); + GuiEditor.brain.setCurrentAddSet(GuiEditor.rootGui); + + $cProbe = new GuiControl() + { + class = "ClipProbe"; + Position = "420 10"; + Extent = "120 60"; + }; + GuiEditor.rootGui.add($cProbe); + + cCheck("the probe's class built it a child", $cProbe.getCount() == 1); + + cSelect($cProbe); + GuiEditor.Copy(); + GuiEditor.Paste(); + + %copy = cPasted(0); + cCheck("the copy has exactly one child (" @ %copy.getCount() @ ")", %copy.getCount() == 1); + cCheck("and it is not the original's child", %copy.getObject(0) != $cProbe.getObject(0)); + cCheck("the copy still has the class", %copy.class $= "ClipProbe"); + + // Two steps, not one expression: TorqueScript cannot call a method on the + // result of a call. + %copyKid = %copy.getObject(0); + %sourceKid = $cProbe.getObject(0); + cCheck("and the child kept its geometry", + %copyKid.getPosition() $= %sourceKid.getPosition()); + + GuiEditor.Undo(); + + schedule(300, 0, "cStepCut"); +} + +//----------------------------------------------------------------------------- +// Cut, which is a copy plus the delete the Delete key already does. +//----------------------------------------------------------------------------- + +function cStepCut() +{ + GuiEditor.undoRecorder.clear(); + GuiEditor.brain.setCurrentAddSet($cPanel); + %trash = GuiEditor.brain.getTrash(); + + %index = cIndexOf($cPanel, $cB); + cSelect($cB); + GuiEditor.Cut(); + + cCheck("a cut is one step", cUndoCount() == 1); + cCheck("the control went to the trash", $cB.getGroup() == %trash); + cCheck("it was not deleted", isObject($cB)); + cCheck("and the clipboard has it", !GuiEditor.clipboard.isEmpty()); + + GuiEditor.Paste(); + %copy = cPasted(0); + + cCheck("the paste after a cut is a second step", cUndoCount() == 2); + cCheck("and it arrived", %copy.getParent() == $cPanel); + + // The original is in the trash, which is not the document - so its name is + // free and the pasted control can have it back. + cCheck("a cut and paste keeps the name (" @ %copy.getName() @ ")", + %copy.getName() $= "clipB"); + + GuiEditor.Undo(); + cCheck("undoing the paste leaves the cut", cUndoCount() == 1); + GuiEditor.Undo(); + cCheck("undoing the cut puts the original back", $cB.getParent() == $cPanel); + cCheck("at the index it came from", cIndexOf($cPanel, $cB) == %index); + + schedule(300, 0, "cStepFrames"); +} + +//----------------------------------------------------------------------------- +// A frame set, whose layout is not in its field list at all: the frame tree is +// written as TAML custom nodes and nothing else, so copying one is the case that +// proves the copy is a deep clone rather than a field copy. +//----------------------------------------------------------------------------- + +function cStepFrames() +{ + GuiEditor.undoRecorder.clear(); + GuiEditor.brain.setCurrentAddSet(GuiEditor.rootGui); + + $cFrames = new GuiFrameSetCtrl() + { + Position = "420 100"; + Extent = "400 200"; + DividerThickness = 4; + }; + GuiEditor.rootGui.add($cFrames); + + %ids = $cFrames.createHorizontalSplit(1); + %left = getWord(%ids, 0); + %right = getWord(%ids, 1); + $cFrames.createVerticalSplit(%left); + $cFrames.createVerticalSplit(%right); + + for(%i = 0; %i < 4; %i++) + { + %panel = new GuiControl() { Extent = "40 20"; }; + $cFrames.add(%panel); + } + + // Settle the layout before anything is measured: a frame set places its + // children when it resizes, and until then they sit at the bounds they were + // built with. + $cFrames.childrenReordered(); + + %sourceShape = cFrameShape($cFrames.getFrameLayout()); + cCheck("the frame set has a tree", %sourceShape !$= ""); + + cSelect($cFrames); + GuiEditor.Copy(); + GuiEditor.Paste(); + + %copy = cPasted(0); + cCheck("the copy holds four children", %copy.getCount() == 4); + + %copyShape = cFrameShape(%copy.getFrameLayout()); + cCheck("and the frame tree came with it" NL + " source: " @ %sourceShape NL + " copy: " @ %copyShape, %copyShape $= %sourceShape); + + // Each frame's control must be one of the COPY's children. The layout's + // eighth value per frame is the control id, so this reads them back out. + %layout = %copy.getFrameLayout(); + %ownChildren = true; + for(%i = 0; (%i + 7) < getWordCount(%layout); %i += 8) + { + %ctrl = getWord(%layout, %i + 7); + if(%ctrl != 0 && %ctrl.getParent() != %copy) + { + %ownChildren = false; + } + } + cCheck("holding the copy's own children, not the original's", %ownChildren); + + GuiEditor.Undo(); + cCheck("and the paste was one step", cUndoCount() == 0); + + schedule(300, 0, "cStepMenu"); +} + +//----------------------------------------------------------------------------- +// The Edit menu, which is what says whether any of this is available. +//----------------------------------------------------------------------------- + +function cStepMenu() +{ + %cutItem = cMenuItem(EditorCore.menuBar, "Cut"); + %copyItem = cMenuItem(EditorCore.menuBar, "Copy"); + %pasteItem = cMenuItem(EditorCore.menuBar, "Paste"); + + cCheck("the Edit menu has all three items", + isObject(%cutItem) && isObject(%copyItem) && isObject(%pasteItem)); + + cSelect($cA); + cCheck("Cut is offered with a selection", %cutItem.Active); + cCheck("so is Copy", %copyItem.Active); + + GuiEditor.brain.clearSelection(); + cCheck("Cut is greyed with nothing selected", !%cutItem.Active); + cCheck("so is Copy", !%copyItem.Active); + + cCheck("Paste is still offered - the clipboard is not empty", %pasteItem.Active); + + schedule(300, 0, "cStepStale"); +} + +//----------------------------------------------------------------------------- +// The clipboard holds live controls wearing live profiles, so it goes stale in +// the one place the undo stack does: when the theme library frees a profile. +//----------------------------------------------------------------------------- + +function cStepStale() +{ + %library = GuiEditor.getThemeLibrary(); + $cThemeB = %library.createTheme("ClipThemeB"); + cCheck("second theme created", isObject($cThemeB)); + + GuiEditor.setTheme($cThemeB, false); + + cSelect($cA); + GuiEditor.Copy(); + cCheck("something is on the clipboard", !GuiEditor.clipboard.isEmpty()); + + // Deleting the theme detaches the document from it first, and the copies in + // the clipboard hold the same profiles by raw pointer. + %library.deleteTheme($cThemeB); + cCheck("deleting a theme empties the clipboard", GuiEditor.clipboard.isEmpty()); + + %pasteItem = cMenuItem(EditorCore.menuBar, "Paste"); + cCheck("and greys Paste again", !%pasteItem.Active); + + GuiEditor.setTheme($cTheme, false); + GuiEditor.undoRecorder.clear(); + + schedule(300, 0, "cStepChain"); +} + +//----------------------------------------------------------------------------- +// Pasting into a container that places its own children. +// +// A GuiChainCtrl lays its children out in list order and takes a child's +// position when it arrives, which it is entitled to do: a control pasted into a +// chain belongs where the chain puts it. What must still hold is that the copy +// arrived, in the right container, as one undo step. +// +// Last, and on the real editor UI, because a chain only claims its children +// while the canvas is in edit mode - which needs the editor pushed onto the +// canvas rather than merely registered. +//----------------------------------------------------------------------------- + +function cStepChain() +{ + EditorCore.open(); + EditorCore.tabBook.selectPageName("Gui Editor"); + + schedule(500, 0, "cStepChainRun"); +} + +function cStepChainRun() +{ + GuiEditor.undoRecorder.clear(); + + $cChain = new GuiChainCtrl() + { + Position = "420 320"; + Extent = "200 120"; + IsVertical = true; + ChildSpacing = 4; + }; + GuiEditor.rootGui.add($cChain); + + for(%i = 0; %i < 3; %i++) + { + %button = new GuiButtonCtrl() { Extent = "80 24"; Text = "chain" @ %i; }; + $cChain.add(%button); + } + + %probe = $cChain.getObject(1); + cCheck("the editor is in edit mode (a chain lays out what it is given)", + getWord(%probe.getPosition(), 0) == 0); + + cSelect(%probe); + GuiEditor.Copy(); + + GuiEditor.brain.setCurrentAddSet($cChain); + GuiEditor.Paste(); + + %copy = cPasted(0); + cCheck("the paste is one step", cUndoCount() == 1); + cCheck("it arrived in the chain", %copy.getParent() == $cChain); + cCheck("the chain holds four now", $cChain.getCount() == 4); + cCheck("and the copy kept its caption", %copy.Text $= %probe.Text); + + GuiEditor.Undo(); + cCheck("undo took it back out", $cChain.getCount() == 3); + + schedule(300, 0, "cDone"); +} + +function cDone() +{ + echo("CLIP DONE"); + quit(); +} diff --git a/tests/smoke/undo.cs b/tests/smoke/undo.cs index a99ed8d86..6bb3c48b3 100644 --- a/tests/smoke/undo.cs +++ b/tests/smoke/undo.cs @@ -657,7 +657,12 @@ function uStepMenu() uCheck("the Edit menu exists", isObject(%editMenu)); uCheck("and is offered at last", %editMenu.Active); - uCheck("Cut is still a stub and is not offered", !%cutItem.Active); + + // Cut belongs to the clipboard now, and it follows the selection rather than + // the undo stack - tests/smoke/clipboard.cs is where that is checked. Here it + // is only worth knowing that this suite left a selection behind, since undo + // moves one. + uCheck("Cut is offered while something is selected", %cutItem.Active); GuiEditor.undoRecorder.clear(); uCheck("Undo is greyed with an empty stack", !%undoItem.Active); From 37f4a08ba475572e05a680db95660a302129fe75 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Wed, 29 Jul 2026 23:49:19 -0400 Subject: [PATCH 18/55] Gui Editor: a control arrives where it can be seen, and a delete says so Three complaints, two causes. A drop is accepted anywhere on the screen. GuiDragAndDropCtrl hit-tests from the drag control's PARENT, which is the brain, and findHitControl answers "me" when none of its children was hit -- it never tests its own bounds. So the walk up to something carrying onControlDropped resolves to the brain for every point there is, and the brain then placed the control at the cursor. Dragging one back onto the palette to change your mind, which is what a hand does, added it behind the palette; so did letting go over the explorer, the inspector or the menu bar. Measured before the fix: a drop at 724,146, which the brain's own rect does not contain, was accepted and landed over the palette. So the brain polices its own boundary. A drop whose pointer is not over the canvas is not a drop -- nothing is added, and the payload dies with the drag control that carried it, deleteOnMouseUp freeing the control and a SimGroup freeing what it holds. onControlDragged asks the same question first, because hit-testing a point outside the Gui answers the Gui itself: 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. Neither callback can be asked where the pointer is. The position they are handed is built from the drag control's own bounds (sendDragEvent) and so is local to the brain, which is why the code has always ignored it and read the payload's global position instead. cursorFrom does the same: a drag grabs a control by the middle, so the middle of the payload is the pointer. Clicking a tile was already centred in the container being worked in, and that was the second cause. A Gui is authored at its own size -- 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 off-canvas. With PlanetX's titleGui loaded the middle of its inner container measures 808,643, behind the explorer, and the middle of the loaded root measures 808,395, directly behind the control list. Which is the report. centredPlacement therefore clips the container to the canvas and centres in what is left. A container that fits -- every container in a Gui the size of the canvas -- still gets its own exact middle, so the rule is unchanged where it was already right. It lives on the brain rather than on the tile because the brain owns both the add set and the canvas the container has to be visible on. The Explorer tree not refreshing on a delete is its own thing, and it depended on which of the three routes asked. The Delete key on the canvas and Cut both delete and then announce it, and the Explorer window is listening. The Delete key while the TREE holds first responder is the other way round: the tree announces and the brain does the deleting -- and postEvent walks the listener list only, so it never delivers to whoever posted. The window that owns the tree was never told, and its rows kept naming a control already in the trash. The receiving half now re-announces, so every route into a delete leaves the panes agreeing with the document. It has to come after endRadioSilence, which is what re-adds the listeners a post needs, and it is safe from the circular-event guard because bIsEventRaised is per object -- the brain is not the tree. Refreshing from inside the tree's own post is safe too: refreshTree's clearItems frees the item vectors directly and never calls clearSelection, so it fires no selection callback to collide with. Verified: two new suites. canvasDrop.cs, 20 checks -- a drop over the palette adds nothing and takes its payload with it and is not an undo step, the same gesture over the canvas lands under the cursor, a drag off the canvas keeps the container, a click centres in a panel exactly and on the canvas, and the 1024x768 case places into the part of the container that can be seen. explorerDelete.cs, 13 checks -- all three routes, plus undo putting the row back. Both were watched failing first, on those measured numbers. Full suite green, 27 as expected; undo.cs loses one stale expectation, having synthesised a drop with the payload left at 0 0, which is off the canvas and now correctly refuses. The drags in canvasDrop are built rather than posted. A real drag needs startDragging, which mouse-locks the canvas, and posted WM_MOUSEMOVE cannot drive the gesture at all -- a harness that presses a tile and posts moves adds nothing on unmodified code either, so it observes nothing about this change. The object graph a drag makes is assembled instead, and the drag control deleted exactly as onTouchUp deletes it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qi2pZ9UzF5rsLCNVErpzwt --- editor/GuiEditor/scripts/GuiEditorBrain.cs | 132 ++++++- .../GuiEditor/scripts/GuiEditorControlTile.cs | 25 +- tests/smoke/canvasDrop.cs | 326 ++++++++++++++++++ tests/smoke/explorerDelete.cs | 162 +++++++++ tests/smoke/undo.cs | 7 +- 5 files changed, 629 insertions(+), 23 deletions(-) create mode 100644 tests/smoke/canvasDrop.cs create mode 100644 tests/smoke/explorerDelete.cs diff --git a/editor/GuiEditor/scripts/GuiEditorBrain.cs b/editor/GuiEditor/scripts/GuiEditorBrain.cs index 928d2c0b7..2cd5178ba 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); @@ -24,6 +50,11 @@ function GuiEditorBrain::onControlDropped(%this, %payload, %position) { + if(!%this.isOverCanvas(%this.cursorFrom(%payload))) + { + return; + } + %pos = %payload.getGlobalPosition(); %x = getWord(%pos, 0); %y = getWord(%pos, 1); @@ -34,6 +65,94 @@ %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 @@ -167,12 +286,23 @@ %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(); } //----------------------------------------------------------------------------- diff --git a/editor/GuiEditor/scripts/GuiEditorControlTile.cs b/editor/GuiEditor/scripts/GuiEditorControlTile.cs index b3417223c..787385226 100644 --- a/editor/GuiEditor/scripts/GuiEditorControlTile.cs +++ b/editor/GuiEditor/scripts/GuiEditorControlTile.cs @@ -237,33 +237,16 @@ 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 = %this.dropPosition(%payload); + %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". GuiEditor.brain.onControlDropped(%payload, %payload.Position); } - -// Centred in the current add set, which is the container the editor is already -// adding to; addNewControl falls back to the root when there is none. -function GuiEditorControlTile::dropPosition(%this, %payload) -{ - %target = GuiEditor.brain.getCurrentAddSet(); - if(!isObject(%target)) - { - %target = GuiEditor.rootGui; - } - - %at = %target.getGlobalPosition(); - %room = %target.getExtent(); - %size = %payload.getExtent(); - - %x = getWord(%at, 0) + ((getWord(%room, 0) - getWord(%size, 0)) / 2); - %y = getWord(%at, 1) + ((getWord(%room, 1) - getWord(%size, 1)) / 2); - - return mFloor(%x) SPC mFloor(%y); -} diff --git a/tests/smoke/canvasDrop.cs b/tests/smoke/canvasDrop.cs new file mode 100644 index 000000000..99a999b9e --- /dev/null +++ b/tests/smoke/canvasDrop.cs @@ -0,0 +1,326 @@ +//----------------------------------------------------------------------------- +// Where a control lands when it arrives in the document. +// +// Two gestures put one there, and both used to be able to put it somewhere +// nobody can see: +// +// a drag GuiDragAndDropCtrl hit-tests from the drag control's PARENT, which +// is the brain, and GuiControl::findHitControl answers "me" when no +// child is hit -- whatever the point. So the brain heard +// onControlDropped for every point on screen, and placed the control +// at the cursor: drag one back onto the palette to change your mind +// and it was added behind the palette. +// +// a click places the control in the middle of the container being worked in. +// A real Gui is designed at 1024x768 and the canvas frame is a few +// hundred pixels wide, so the middle of a container is regularly off +// the side of the canvas -- behind the palette or the explorer. +// +// The drags here are built rather than posted. A real drag would need +// startDragging, which mouse-locks the canvas, and the point of the test is +// what onControlDropped does with the payload it is handed -- so the object +// graph a drag makes (payload inside a GuiDragAndDropCtrl inside the brain) is +// assembled directly, and the drag control is deleted afterwards exactly as +// GuiDragAndDropCtrl::onTouchUp deletes it. +//----------------------------------------------------------------------------- + +setLogMode(2); +setScriptExecEcho(false); +trace(false); +$Scripts::ignoreDSOs = true; +setCompanyAndProduct("Torque Game Engines", "Torque2D"); +ModuleDatabase.EchoInfo = false; +AssetDatabase.EchoInfo = false; +AssetDatabase.IgnoreAutoUnload = true; + +testExec("editor/main.cs"); + +$Pass = 0; +$Fail = 0; + +function cdCheck(%label, %condition) +{ + if(%condition) + { + $Pass++; + echo("CDROP PASS: " @ %label); + } + else + { + $Fail++; + echo("CDROP FAIL: " @ %label); + } +} + +schedule(2000, 0, "cdSetup"); + +// A project, so there is a theme: an unthemed drop skips a whole branch of +// acceptControl and this suite is about the placing, not the theming. +function cdSetup() +{ + ProjectManager.setProjectFolder("PlanetX"); + ModuleDatabase.ScanModules("PlanetX"); + ModuleDatabase.LoadExplicit("AppCore", 1); + + GuiEditor.open(); + schedule(500, 0, "cdDropChecks"); +} + +//----------------------------------------------------------------------------- +// Helpers. Global coordinates throughout -- the position these callbacks are +// handed is local to the brain, which is why neither the code nor the test +// measures anything with it. +//----------------------------------------------------------------------------- + +// The rect of a control, as "x y width height" in global coordinates. +function cdInside(%point, %ctrl) +{ + %x = getWord(%point, 0); + %y = getWord(%point, 1); + %at = %ctrl.getGlobalPosition(); + %ext = %ctrl.getExtent(); + + return (%x >= getWord(%at, 0) && %y >= getWord(%at, 1) && + %x < getWord(%at, 0) + getWord(%ext, 0) && + %y < getWord(%at, 1) + getWord(%ext, 1)); +} + +// The whole control, not just its corner. +function cdWhollyInside(%ctrl, %container) +{ + %at = %ctrl.getGlobalPosition(); + %ext = %ctrl.getExtent(); + %far = (getWord(%at, 0) + getWord(%ext, 0) - 1) SPC + (getWord(%at, 1) + getWord(%ext, 1) - 1); + + return cdInside(%at, %container) && cdInside(%far, %container); +} + +// The object graph a drag has when the mouse comes up, without the mouse lock. +// beginDrag grabs the payload by the middle, so the cursor is its centre. +function cdBuildDrag(%payload, %cursor) +{ + %brainAt = GuiEditor.brain.getGlobalPosition(); + %size = %payload.getExtent(); + + %x = getWord(%cursor, 0) - getWord(%brainAt, 0) - (getWord(%size, 0) / 2); + %y = getWord(%cursor, 1) - getWord(%brainAt, 1) - (getWord(%size, 1) / 2); + + %drag = new GuiDragAndDropCtrl() + { + Profile = "GuiDragAndDropProfile"; + HorizSizing = "anchorLeft"; + VertSizing = "anchorTop"; + Position = mFloor(%x) SPC mFloor(%y); + Extent = %size; + deleteOnMouseUp = true; + }; + %drag.add(%payload); + GuiEditor.brain.add(%drag); + + return %drag; +} + +// Press, drag to %cursor, release. Answers the payload, which may be dead. +function cdDrop(%class, %cursor) +{ + %payload = eval("return new " @ %class @ "();"); + %drag = cdBuildDrag(%payload, %cursor); + + GuiEditor.brain.onControlDropped(%payload, %drag.getPosition()); + %drag.delete(); + + return %payload; +} + +//----------------------------------------------------------------------------- +// A drop has to be over the canvas. +//----------------------------------------------------------------------------- + +function cdDropChecks() +{ + %root = GuiEditor.rootGui; + %palette = GuiEditor.ctrlListWindow; + + // Aimed at the palette, which is where a hand goes to change its mind. + %at = %palette.getGlobalPosition(); + %cursor = (getWord(%at, 0) + 60) SPC (getWord(%at, 1) + 140); + cdCheck("the test is aiming off the canvas", !cdInside(%cursor, %root)); + + %before = %root.getCount(); + %depth = GuiEditor.undoRecorder.undoCount(); + %payload = cdDrop("GuiButtonCtrl", %cursor); + + cdCheck("a drop over the palette adds nothing (" @ %root.getCount() @ ")", + %root.getCount() == %before); + cdCheck("the control it was carrying goes with the drag", !isObject(%payload)); + cdCheck("and it is not an undo step (" @ + (GuiEditor.undoRecorder.undoCount() - %depth) @ ")", + GuiEditor.undoRecorder.undoCount() == %depth); + + // The same gesture, ended over the canvas. + %rootAt = %root.getGlobalPosition(); + %cursor = (getWord(%rootAt, 0) + 80) SPC (getWord(%rootAt, 1) + 90); + %payload = cdDrop("GuiButtonCtrl", %cursor); + + cdCheck("a drop over the canvas is added (" @ %root.getCount() @ ")", + %root.getCount() == %before + 1); + cdCheck("it is in the add set", %payload.getGroup() == GuiEditor.brain.getCurrentAddSet()); + cdCheck("it landed under the cursor", %payload.getGlobalCenter() $= %cursor); + + // A drop lands twice -- once now, once 40ms later, because the container it + // arrived in may have moved it. Let the second one happen before going on. + schedule(200, 0, "cdAddSetChecks", %payload); +} + +//----------------------------------------------------------------------------- +// A drag that wanders off the canvas must not change the container being worked +// in: the click gesture places into it, so losing it silently moves where the +// next click puts a control. +//----------------------------------------------------------------------------- + +function cdAddSetChecks(%dropped) +{ + cdCheck("the drop stayed under the cursor after its second placing", + cdWhollyInside(%dropped, GuiEditor.rootGui)); + + // A container to be working in, sized so it is wholly on the canvas. + %panel = new GuiControl() + { + Position = "20 20"; + Extent = "200 200"; + }; + ThemeManager.setProfile(%panel, "panelProfile"); + GuiEditor.rootGui.add(%panel); + GuiEditor.brain.setCurrentAddSet(%panel); + cdCheck("working in the panel", GuiEditor.brain.getCurrentAddSet() == %panel); + + %palette = GuiEditor.ctrlListWindow; + %at = %palette.getGlobalPosition(); + %cursor = (getWord(%at, 0) + 60) SPC (getWord(%at, 1) + 140); + + %payload = new GuiButtonCtrl(); + %drag = cdBuildDrag(%payload, %cursor); + GuiEditor.brain.onControlDragged(%payload, %drag.getPosition()); + %drag.delete(); + + cdCheck("dragging off the canvas keeps the container (" @ + GuiEditor.brain.getCurrentAddSet() @ ")", + GuiEditor.brain.getCurrentAddSet() == %panel); + + schedule(100, 0, "cdClickChecks", %panel); +} + +//----------------------------------------------------------------------------- +// A click places in the middle of the container being worked in. +//----------------------------------------------------------------------------- + +function cdClickChecks(%panel) +{ + %tile = cdFindTile("GuiButtonCtrl"); + cdCheck("found the button tile", isObject(%tile)); + + // In the panel: dead centre of it. + %probe = new GuiButtonCtrl(); + %size = %probe.getExtent(); + %at = %panel.getGlobalPosition(); + %room = %panel.getExtent(); + %want = mFloor(getWord(%at, 0) + ((getWord(%room, 0) - getWord(%size, 0)) / 2)) SPC + mFloor(getWord(%at, 1) + ((getWord(%room, 1) - getWord(%size, 1)) / 2)); + %probe.delete(); + + %tile.onClick(); + %ctrl = %panel.getObject(%panel.getCount() - 1); + cdCheck("clicking placed it in the panel", %ctrl.getGroup() == %panel); + cdCheck("in the middle of it (" @ %ctrl.getGlobalPosition() @ " wanted " @ %want @ ")", + %ctrl.getGlobalPosition() $= %want); + cdCheck("which is on the canvas", cdWhollyInside(%ctrl, GuiEditor.rootGui)); + + // And with nothing selected, the middle of the canvas itself. + GuiEditor.brain.setCurrentAddSet(GuiEditor.rootGui); + %tile.onClick(); + %ctrl = GuiEditor.rootGui.getObject(GuiEditor.rootGui.getCount() - 1); + cdCheck("with no container chosen it goes on the canvas", + cdWhollyInside(%ctrl, GuiEditor.rootGui)); + + schedule(100, 0, "cdBigGuiChecks"); +} + +//----------------------------------------------------------------------------- +// The case the canvas frame cannot show: a Gui designed at 1024x768 in a canvas +// a few hundred pixels wide. The middle of a container that runs off the side +// of the canvas is behind the palette, so the click has to place into the part +// of the container that can actually be seen. +//----------------------------------------------------------------------------- + +function cdBigGuiChecks() +{ + %content = TAMLRead(testRoot("PlanetX/PlanetXGame/gui/titleGui.gui.taml")); + GuiEditor.DisplayGuiContent(%content, false); + + %root = GuiEditor.rootGui; + %inner = %content.getObject(0); + GuiEditor.brain.setCurrentAddSet(%inner); + + cdCheck("the loaded Gui is wider than the canvas (" @ + getWord(%content.getExtent(), 0) @ " in " @ getWord(%root.getExtent(), 0) @ ")", + getWord(%content.getExtent(), 0) > getWord(%root.getExtent(), 0)); + cdCheck("its container runs off the canvas", !cdWhollyInside(%inner, %root)); + + %tile = cdFindTile("GuiButtonCtrl"); + %tile.onClick(); + %ctrl = %inner.getObject(%inner.getCount() - 1); + + cdCheck("clicking placed it in the container", %ctrl.getGroup() == %inner); + cdCheck("somewhere that can be seen (" @ %ctrl.getGlobalPosition() @ ")", + cdWhollyInside(%ctrl, %root)); + + // Specifically the middle of the visible part: the container starts at the + // canvas's left edge and runs 1024 wide, so that is the middle of the + // canvas across, and the middle of the overlap down. + %overlap = cdOverlapRect(%inner, %root); + %size = %ctrl.getExtent(); + %want = mFloor(getWord(%overlap, 0) + ((getWord(%overlap, 2) - getWord(%size, 0)) / 2)) SPC + mFloor(getWord(%overlap, 1) + ((getWord(%overlap, 3) - getWord(%size, 1)) / 2)); + cdCheck("in the middle of the part on the canvas (" @ + %ctrl.getGlobalPosition() @ " wanted " @ %want @ ")", + %ctrl.getGlobalPosition() $= %want); + + echo("CDROP DONE " @ $Pass @ " passed, " @ $Fail @ " failed"); + quit(); +} + +// "x y width height" of what two controls share, in global coordinates. +function cdOverlapRect(%a, %b) +{ + %aAt = %a.getGlobalPosition(); + %aExt = %a.getExtent(); + %bAt = %b.getGlobalPosition(); + %bExt = %b.getExtent(); + + %left = mFloor(mGetMax(getWord(%aAt, 0), getWord(%bAt, 0))); + %top = mFloor(mGetMax(getWord(%aAt, 1), getWord(%bAt, 1))); + %right = mFloor(mGetMin(getWord(%aAt, 0) + getWord(%aExt, 0), + getWord(%bAt, 0) + getWord(%bExt, 0))); + %bottom = mFloor(mGetMin(getWord(%aAt, 1) + getWord(%aExt, 1), + getWord(%bAt, 1) + getWord(%bExt, 1))); + + return %left SPC %top SPC (%right - %left) SPC (%bottom - %top); +} + +function cdFindTile(%key) +{ + %window = GuiEditor.ctrlListWindow; + for(%g = 0; %g < %window.groupCount; %g++) + { + %group = %window.group[%g]; + for(%i = 0; %i < %group.tileCount; %i++) + { + if(%group.tile[%i].key $= %key) + { + return %group.tile[%i]; + } + } + } + return 0; +} diff --git a/tests/smoke/explorerDelete.cs b/tests/smoke/explorerDelete.cs new file mode 100644 index 000000000..cb943feb2 --- /dev/null +++ b/tests/smoke/explorerDelete.cs @@ -0,0 +1,162 @@ +//----------------------------------------------------------------------------- +// Deleting a control, from each of the three places a person can ask for it, +// and the one thing that has to be true afterwards: the Explorer tree shows the +// document as it now is. +// +// The routes are not symmetrical, which is how the tree came to go stale. Two of +// them (the Delete key on the canvas, and Cut) delete and then announce it, and +// the Explorer window is listening. The third is the Delete key while the tree +// itself holds first responder: the TREE announces, the brain does the deleting +// -- and postEvent does not deliver to the object that posted, so the window +// that owns the tree never heard that anything had gone. +//----------------------------------------------------------------------------- + +setLogMode(2); +setScriptExecEcho(false); +trace(false); +$Scripts::ignoreDSOs = true; +setCompanyAndProduct("Torque Game Engines", "Torque2D"); +ModuleDatabase.EchoInfo = false; +AssetDatabase.EchoInfo = false; +AssetDatabase.IgnoreAutoUnload = true; + +testExec("editor/main.cs"); + +$Pass = 0; +$Fail = 0; + +function edCheck(%label, %condition) +{ + if(%condition) + { + $Pass++; + echo("EDEL PASS: " @ %label); + } + else + { + $Fail++; + echo("EDEL FAIL: " @ %label); + } +} + +schedule(2000, 0, "edSetup"); + +function edSetup() +{ + ProjectManager.setProjectFolder("PlanetX"); + ModuleDatabase.ScanModules("PlanetX"); + ModuleDatabase.LoadExplicit("AppCore", 1); + + GuiEditor.open(); + schedule(500, 0, "edCanvasKey"); +} + +//----------------------------------------------------------------------------- +// Helpers. +//----------------------------------------------------------------------------- + +// One control on the canvas, selected, with the tree showing it. +function edPlace() +{ + %tile = edFindTile("GuiButtonCtrl"); + %tile.onClick(); + + %ctrl = GuiEditor.rootGui.getObject(GuiEditor.rootGui.getCount() - 1); + GuiEditor.brain.onInspect(%ctrl); + + return %ctrl; +} + +// What every route has to leave behind: the control gone from the document, and +// gone from the tree as well. +function edGone(%label, %ctrl, %items) +{ + %tree = GuiEditor.explorerWindow.tree; + + edCheck(%label @ ": the control left the document", + %ctrl.getGroup() != GuiEditor.rootGui); + edCheck(%label @ ": the tree no longer lists it", + %tree.findItemID(%ctrl) == -1); + edCheck(%label @ ": the tree lost a row (" @ %items @ " -> " @ + %tree.getItemCount() @ ")", %tree.getItemCount() == %items - 1); +} + +//----------------------------------------------------------------------------- +// The Delete key with the canvas in charge. The C++ deletes and then calls +// onDelete, which is the announcement. +//----------------------------------------------------------------------------- + +function edCanvasKey() +{ + %tree = GuiEditor.explorerWindow.tree; + %ctrl = edPlace(); + + edCheck("the tree lists a control that was just placed", %tree.findItemID(%ctrl) != -1); + %items = %tree.getItemCount(); + + GuiEditor.brain.deleteSelection(); + GuiEditor.brain.onDelete(); + edGone("canvas Delete", %ctrl, %items); + + schedule(100, 0, "edCut"); +} + +//----------------------------------------------------------------------------- +// Cut, which is a copy and then that same delete. +//----------------------------------------------------------------------------- + +function edCut() +{ + %tree = GuiEditor.explorerWindow.tree; + %ctrl = edPlace(); + %items = %tree.getItemCount(); + + GuiEditor.Cut(); + edGone("Cut", %ctrl, %items); + + schedule(100, 0, "edTreeKey"); +} + +//----------------------------------------------------------------------------- +// The Delete key while the tree holds first responder, which is what happens +// after clicking a row. GuiListBoxCtrl::onKeyDown calls onDeleteKey. +//----------------------------------------------------------------------------- + +function edTreeKey() +{ + %tree = GuiEditor.explorerWindow.tree; + %ctrl = edPlace(); + %items = %tree.getItemCount(); + + %index = %tree.findItemID(%ctrl); + edCheck("the tree can find the row to delete", %index != -1); + + %tree.onDeleteKey(%index, %tree.getItemText(%index), %ctrl); + edGone("tree Delete", %ctrl, %items); + + // A delete is one undo step whichever route asked for it, and undoing it has + // to put the row back -- the same refresh, the other way round. + GuiEditor.Undo(); + edCheck("undo brought the control back", %ctrl.getGroup() == GuiEditor.rootGui); + edCheck("and the tree lists it again", %tree.findItemID(%ctrl) != -1); + + echo("EDEL DONE " @ $Pass @ " passed, " @ $Fail @ " failed"); + quit(); +} + +function edFindTile(%key) +{ + %window = GuiEditor.ctrlListWindow; + for(%g = 0; %g < %window.groupCount; %g++) + { + %group = %window.group[%g]; + for(%i = 0; %i < %group.tileCount; %i++) + { + if(%group.tile[%i].key $= %key) + { + return %group.tile[%i]; + } + } + } + return 0; +} diff --git a/tests/smoke/undo.cs b/tests/smoke/undo.cs index 6bb3c48b3..679f1c40a 100644 --- a/tests/smoke/undo.cs +++ b/tests/smoke/undo.cs @@ -350,7 +350,12 @@ function uStepAdd() GuiEditor.brain.setCurrentAddSet($uPanel); - $uDropped = new GuiCheckBoxCtrl() { Position = "0 0"; Extent = "120 30"; Text = "Dropped"; }; + // Position, not the point handed to the callback, is what a drop is placed + // from: the payload is not owned yet, so its own global position is where the + // cursor let go. It has to be over the canvas -- a drop that is not is a drag + // taken back to the palette and abandoned, and adds nothing at all. The panel + // this lands in sits at 10,10 inside a canvas that starts at 366,26. + $uDropped = new GuiCheckBoxCtrl() { Position = "400 60"; Extent = "120 30"; Text = "Dropped"; }; GuiEditor.brain.onControlDropped($uDropped, "50 50"); uCheck("a drop is one step, not one per thing it did", uUndoCount() == 1); From d2845d787a414ce06993b2694c192e29706ebf16 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Thu, 30 Jul 2026 18:22:34 -0400 Subject: [PATCH 19/55] Gui Editor: a tab book makes its own pages The palette offered a Tab Page tile, and a tab page is the one control that means nothing anywhere but inside a tab book. Dropped on a panel it is a panel that draws nothing and that no tab will ever select. Dropped on a book it works, but only because the book happened to be the add set. And a book dropped from the palette arrived with no pages at all, which rendered as nothing whatsoever -- not an empty strip, nothing. Both halves of that are one short circuit: calculatePageTabs returns early on an empty mPages, leaving mTabRect the zero it was constructed with, and onRender bails on the invalid rect before it draws the strip, the background or the children. So the book makes its own pages. Tab Page leaves the palette, a dropped book arrives holding Page 1, and while the Gui is being authored the book draws a ghosted square "+" after the last real tab whose click adds another. Pages are removed with the Delete that already worked. It is GuiChainCtrl's edit-mode "+" band, except that the chain's is a drop-target hint nothing hit-tests and this one is the primary way a page is made -- so it is laid out, hit-tested, and reserved space in the strip. Choices worth writing down. The palette REFUSES the class rather than losing the row. frame IS the row index in GuiEditorControlIcons, so deleting Tab Page's row would silently repoint Window and Input onto the wrong art; and dropping it from covered[] would have addUndrawnClasses sweep the class registry and offer it straight back in an "Undrawn" group with a question mark for an icon. The row keeps its frame, its label and its place in covered[]; only groupKeys refuses it. So keysInGroup is the palette's view and keys() is the table's, and the icon sheet still carries the art. The page is made in script, not in C++. A page created while authoring has to be themed, recorded for undo and announced to the Explorer tree, and a control knows how to do none of that. GuiTabBookCtrl draws the affordance and hit-tests it; GuiEditorBrain::onAddTabPage does the rest. acceptControl is split for it -- adoptControl is now the half of arriving that is the same however a control got here, and the undo record stays with each caller because what counts as one step differs: a dropped control is a step, and the page seeded inside a book that is itself arriving is part of that book arriving. That seeded page is added with a plain add(), which announces nothing and so records nothing, and undo of the drop takes the book to the trash with its page inside it. One step, which is what the user did. The "+" is drawn whenever the book is in edit mode, selected or not, and the strip reserves its space the whole time. Drawing it only on selection -- what the chain does -- would reflow the tabs under the cursor at the moment you select the book, and it is the only way to make a page, so it has to be findable without knowing to select the book first. It wraps like a real tab when it will not fit. That grows mTabRect and shrinks mPageRect and so re-sizes every page, but only in edit mode and not durably: the book sizes each page from mPageRect whenever one is added, so a Gui saved with the "+" on a row of its own loads back unchanged. A book with pages always has an active one, and exactly one page is visible -- but NOT via selectPage. selectPage ends in an onTabSelected script callback and EditorCore's handler opens the editor a page belongs to, while EditorCore::FinishRegistration adds one page per editor as each editor's module loads. Calling it from onChildAdded would open EditorConsole during LoadExplicit, before the other three editors exist, and every smoke suite boots through that path. syncPageVisibility is the quiet half: no callback, safe to call while a child is arriving or leaving. It also fixes undo of deleting the ACTIVE page, which restored a visible page on top of the one promoted in its place -- the recorder puts back position, extent and sizing, and visibility is not among them. canBeChildOf is a new GuiControl virtual, and it is advice the editor takes rather than an invariant the object model enforces: there is no veto hook anywhere in the engine, SimGroup::addObject cannot refuse, and add() will still put a control wherever it is told. GuiTabPageCtrl answers true only for a tab book, so book to book is still a move. Four doors ask -- the Explorer drag, the canvas drag, paste, and the tree's drop indicator. The indicator matters: a drag that is quietly dropped on the floor reads as a broken tree, where a line that does not appear reads as a rule. Three bugs fell out, all pre-existing, all newly reachable because an empty book is now an ordinary state. onMouseDownEditor hit-tested tabs with raw control-local coordinates where onTouchDown has always converted with getTabLocalCoord first. Invisible for a top-aligned book, out by the whole page area for a bottom or right one. The "+" needs the same conversion, so it is fixed here rather than around it. The bottom and right cases of calculatePageTabs computed the strip's origin by measuring back from the far edge using mTabRect.extent BEFORE this pass had written it -- last pass's size, and zero on the first pass. A book with pages hid it behind a second pass; a book with none gets only the first. All four cases now assign extent before point. reorderFromDrag called bringObjectToFront unconditionally after addObject. That is reOrder(obj, front()), and front() is evaluated to build the argument before reOrder can notice the object is not a member -- so dropping a button on a page-less book, which onChildAdded re-homes away, dereferenced an empty list. Guarded with isMember. onChildAdded's re-home also resolved its destination before removing the child, rather than after, so a book with no page and no parent no longer leaves it registered with no group at all. Verified: two new suites and a shot harness. tabBook.cs, 63 checks -- the palette refusing the class while keeping its frame, a drop arriving with one themed page as one undo step, the "+" adding and numbering pages, the geometry the click depends on, a book emptied to nothing and refilled from its own "+", exactly one page visible through a whole undo walk, and the reparent rule through canBeChildOf, moveSelectionToCtrl and paste. tabBookClick.cs posts a REAL click onto the "+" and asserts a themed, active, undoable page appears -- that is the only cover the C++ hit test has, and it is why the coordinate fix above is testable at all. shots/tabBook.cs draws a populated and an empty book in all four tab positions, which is what the two stale-field cases needed a picture of. The click point is not hard-coded in the driver. A fixed coordinate that drifted beside the "+" would report a missing page, which is exactly what a broken hit test reports, and the test would be lying either way. The engine works out where the "+" actually landed -- which depends on the theme's font and the profile's borders -- and hands it over in a file the .input.ps1 polls for. getAddPageTabRect exists for that, and answers empty outside edit mode so closing the editor cannot leave a stale rectangle behind. Full suite green, all 29 as expected, and 12 shot harnesses. No existing suite lost an expectation. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qi2pZ9UzF5rsLCNVErpzwt --- editor/GuiEditor/scripts/GuiEditorBrain.cs | 131 +++++- .../GuiEditor/scripts/GuiEditorClipboard.cs | 10 + .../scripts/GuiEditorControlIcons.cs | 29 +- .../source/gui/containers/guiTabBookCtrl.cc | 279 ++++++++++-- engine/source/gui/containers/guiTabBookCtrl.h | 34 ++ .../containers/guiTabBookCtrl_ScriptBinding.h | 37 +- .../source/gui/containers/guiTabPageCtrl.cc | 15 + engine/source/gui/containers/guiTabPageCtrl.h | 4 + engine/source/gui/editor/guiEditCtrl.cc | 21 + engine/source/gui/guiControl.h | 12 + engine/source/gui/guiControl_ScriptBinding.h | 19 + engine/source/gui/guiTreeViewCtrl.cc | 70 ++- engine/source/gui/guiTreeViewCtrl.h | 9 + tests/shots/tabBook.cs | 107 +++++ tests/smoke/palette.cs | 27 +- tests/smoke/tabBook.cs | 402 ++++++++++++++++++ tests/smoke/tabBookClick.cs | 141 ++++++ tests/smoke/tabBookClick.input.ps1 | 39 ++ 18 files changed, 1319 insertions(+), 67 deletions(-) create mode 100644 tests/shots/tabBook.cs create mode 100644 tests/smoke/tabBook.cs create mode 100644 tests/smoke/tabBookClick.cs create mode 100644 tests/smoke/tabBookClick.input.ps1 diff --git a/editor/GuiEditor/scripts/GuiEditorBrain.cs b/editor/GuiEditor/scripts/GuiEditorBrain.cs index 2cd5178ba..953c3b8c0 100644 --- a/editor/GuiEditor/scripts/GuiEditorBrain.cs +++ b/editor/GuiEditor/scripts/GuiEditorBrain.cs @@ -168,15 +168,43 @@ // 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); + } + + %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 addNewCtrl, because the control's parent decides + // 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 addNewCtrl is also what + // 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. @@ -189,15 +217,106 @@ if(isObject(%theme)) { GuiEditor.undoRecorder.suspend(); - GuiEditor.themeApplier.applyToBranch(%payload, %theme, false); + GuiEditor.themeApplier.applyToBranch(%ctrl, %theme, false); GuiEditor.undoRecorder.resume(); - %this.postEvent("Rethemed", %payload); + %this.postEvent("Rethemed", %ctrl); } %this.setFirstResponder(); - %this.postEvent("AddControl", %payload); - %this.postEvent("Inspect", %payload); + %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; } function GuiEditorBrain::finishControlDropped(%this, %payload, %x, %y) diff --git a/editor/GuiEditor/scripts/GuiEditorClipboard.cs b/editor/GuiEditor/scripts/GuiEditorClipboard.cs index 41412ba2e..4c2325ed3 100644 --- a/editor/GuiEditor/scripts/GuiEditorClipboard.cs +++ b/editor/GuiEditor/scripts/GuiEditorClipboard.cs @@ -209,6 +209,16 @@ 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. diff --git a/editor/GuiEditor/scripts/GuiEditorControlIcons.cs b/editor/GuiEditor/scripts/GuiEditorControlIcons.cs index 9c9c6408d..9eb41262c 100644 --- a/editor/GuiEditor/scripts/GuiEditorControlIcons.cs +++ b/editor/GuiEditor/scripts/GuiEditorControlIcons.cs @@ -32,10 +32,12 @@ %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 two are real controls + // 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, and GuiMenuItemCtrl means nothing outside a menu bar. - %this.refusedNames = "GuiCanvas GuiDragAndDropCtrl GuiGraphCtrl GuiMenuItemCtrl GuiMessageVectorCtrl GuiParticleGraphInspector GuiSceneObjectCtrl"; + // 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++) @@ -96,14 +98,27 @@ { %this.keyList = (%this.keyList $= "") ? %key : (%this.keyList TAB %key); - %group = %this.group[%key]; - %held = %this.groupKeys[%group]; - %this.groupKeys[%group] = (%held $= "") ? %key : (%held 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); + } } } } diff --git a/engine/source/gui/containers/guiTabBookCtrl.cc b/engine/source/gui/containers/guiTabBookCtrl.cc index 1971a22d7..ee0cf508d 100755 --- a/engine/source/gui/containers/guiTabBookCtrl.cc +++ b/engine/source/gui/containers/guiTabBookCtrl.cc @@ -60,6 +60,11 @@ GuiTabBookCtrl::GuiTabBookCtrl() mBounds.extent.set( 400, 300 ); mPageRect = RectI(0,0,0,0); mTabRect = RectI(0,0,0,0); + + // Empty until a layout pass in edit mode fills it in, and read by + // getAddTabGlobalRect - which script can ask about a book that has never laid + // itself out at all. + mAddTabRect = RectI(0,0,0,0); mTabDownPosition = Point2I(); mDepressed = false; @@ -82,17 +87,6 @@ void GuiTabBookCtrl::initPersistFields() addField("TabProfile", TypeGuiProfile, Offset(mTabProfile, GuiTabBookCtrl)); } -// Empty for now, will implement for handling design time context menu for manipulating pages -ConsoleMethod( GuiTabBookCtrl, addPage, void, 2, 2, "() Empty") -{ - object->addNewPage(); -} - -//ConsoleMethod( GuiTabBookCtrl, removePage, void, 2, 2, "()") -//{ -//} - - bool GuiTabBookCtrl::onAdd() { Parent::onAdd(); @@ -125,6 +119,26 @@ void GuiTabBookCtrl::onChildRemoved( GuiControl* child ) else if (mActivePage == NULL ) mActivePage = static_cast(mPages[0].Page); + // The strip has one fewer tab in it, and nothing else re-runs the layout on a + // removal: solveDirty watches the tab position, the font height and the first + // tab's width, and a delete changes none of them. Without this the surviving + // tabs keep the rectangles they were given and leave a hole where the deleted + // one used to be. + calculatePageTabs(); + + // Whichever page was promoted above was hidden the moment some other tab was + // chosen, and nothing has told it otherwise. + syncPageVisibility(); +} + +void GuiTabBookCtrl::syncPageVisibility() +{ + for( S32 i = 0; i < mPages.size(); i++ ) + { + GuiTabPageCtrl* page = mPages[i].Page; + if( page != NULL ) + page->setVisible( page == mActivePage ); + } } // The tab strip is drawn from mPages, which is filled in the order pages are @@ -164,6 +178,13 @@ void GuiTabBookCtrl::childrenReordered() mPages.push_back(ordered[p]); calculatePageTabs(); + + // Undo puts a deleted page back by moving it, and the recorder's layout fix + // restores position, extent and sizing - not visibility. A page that was + // showing when it was deleted comes back still showing, on top of whichever + // page took over from it. + syncPageVisibility(); + Parent::childrenReordered(); } @@ -173,19 +194,26 @@ void GuiTabBookCtrl::onChildAdded( GuiControl *child ) if( !page ) { Con::warnf("GuiTabBookCtrl::onChildAdded - attempting to add NON GuiTabPageCtrl as child page"); - SimObject *simObj = reinterpret_cast(child); - removeObject( simObj ); - if( mActivePage ) + + // Work out where it is going BEFORE taking it out of the book. A book with + // no active page and no parent has nowhere to send it, and removing it + // first left it registered with no group at all - which a book emptied of + // its pages in the editor makes an ordinary thing to run into. + GuiControl *destination = mActivePage; + if( destination == NULL ) { - mActivePage->addObject( simObj ); + Con::warnf("GuiTabBookCtrl::onChildAdded - unable to find active page to reassign ownership of new child control to, placing on parent"); + destination = getParent(); } - else + + if( destination == NULL ) { - Con::warnf("GuiTabBookCtrl::onChildAdded - unable to find active page to reassign ownership of new child control to, placing on parent"); - GuiControl *rent = getParent(); - if( rent ) - rent->addObject( simObj ); + Con::warnf("GuiTabBookCtrl::onChildAdded - no parent to place it on either; leaving it where it is"); + return; } + + removeObject( child ); + destination->addObject( child ); return; } @@ -198,9 +226,24 @@ void GuiTabBookCtrl::onChildAdded( GuiControl *child ) mPages.push_back( newPage ); + // A book with pages always has an active one. Without this a book holding a + // single page draws that page's tab unselected and shows nothing inside it, + // and onMouseDownEditor's "select the page behind the tab" has nothing to + // select. + // + // Deliberately not selectPage(), which ends in an onTabSelected script + // callback: this runs from inside addObject, and EditorCore adds one page per + // editor as each editor's module loads. Its handler opens the editor that + // page belongs to, so during load the first one would open before the rest of + // them exist. + if( mActivePage == NULL ) + mActivePage = page; + // Calculate Page Information calculatePageTabs(); + syncPageVisibility(); + child->resize( Point2I(0, 0), mPageRect.extent ); } @@ -258,6 +301,19 @@ void GuiTabBookCtrl::addNewPage() this->addObject( page ); } +void GuiTabBookCtrl::requestNewPage() +{ + // The GuiEditCtrl wears the GuiEditorBrain namespace, so this arrives at + // GuiEditorBrain::onAddTabPage. A book being edited by anything that has no + // handler for it simply gets no page, which is the right way for this to + // fail. + GuiEditCtrl* edit = GuiControl::smEditorHandle; + if( edit != NULL && edit->isMethod("onAddTabPage") ) + { + Con::executef( edit, 2, "onAddTabPage", getIdString() ); + } +} + void GuiTabBookCtrl::resize(const Point2I &newPosition, const Point2I &newExtent) { Parent::resize( newPosition, newExtent ); @@ -297,6 +353,27 @@ Point2I GuiTabBookCtrl::getTabLocalCoord(const Point2I &src) return ret; } +RectI GuiTabBookCtrl::getAddTabGlobalRect() +{ + // isEditMode as well as the rectangle, because closing the editor does not + // re-run the layout: the book would go on reporting the "+" it had until + // something else happened to resize it. Nothing DRAWS one - renderAddTab has + // no editor to ask for a colour - so this is the only place it could show. + if (!mAddTabRect.isValidRect() || !isEditMode()) + { + return RectI(0, 0, 0, 0); + } + + // The same walk onRender makes to reach the point it hands renderTabs, which + // is the origin mAddTabRect is measured from. + Point2I totalOffset = localToGlobalCoord(Point2I(0, 0)) + mTabRect.point; + RectI ctrlRect = applyMargins(totalOffset, mTabRect.extent, NormalState, mProfile); + RectI fillRect = applyBorders(ctrlRect.point, ctrlRect.extent, NormalState, mProfile); + RectI contentRect = applyPadding(fillRect.point, fillRect.extent, NormalState, mProfile); + + return RectI(contentRect.point + mAddTabRect.point, mAddTabRect.extent); +} + void GuiTabBookCtrl::onTouchDown(const GuiEvent &event) { Point2I localMouse = globalToLocalCoord( event.mousePoint ); @@ -382,7 +459,25 @@ bool GuiTabBookCtrl::onMouseDownEditor(const GuiEvent &event, const Point2I& off if( mTabRect.pointInRect( localMouse ) ) { - GuiTabPageCtrl *tab = findHitTab( localMouse ); + // Tab rectangles are measured from the strip's CONTENT, not from the + // control. onTouchDown has always converted before asking and this has + // always not, which put every editor tab hit out by the book's margin, + // border and padding - and, for a bottom or right strip, by the whole + // width of the page area as well. + Point2I tabLocalMouse = getTabLocalCoord( localMouse ); + + // Before the real tabs: the "+" sits inside the strip, so a stale tab + // rectangle must not get first refusal on it. + if( mAddTabRect.isValidRect() && mAddTabRect.pointInRect( tabLocalMouse ) ) + { + requestNewPage(); + + // Nothing else happens on this click. Selection follows the page the + // editor is about to make, not the page that happened to be showing. + return true; + } + + GuiTabPageCtrl *tab = findHitTab( tabLocalMouse ); if( tab != NULL ) { selectPage( tab ); @@ -442,11 +537,6 @@ void GuiTabBookCtrl::onRender(Point2I offset, const RectI &updateRect) void GuiTabBookCtrl::renderTabs( const Point2I &offset ) { - // If the tab size is zero, don't render tabs, - // and assume it's a tab-less tab-book - JDD - if( mPages.empty()) - return; - for( S32 i = 0; i < mPages.size(); i++ ) { RectI tabBounds = mPages[i].TabRect; @@ -455,6 +545,42 @@ void GuiTabBookCtrl::renderTabs( const Point2I &offset ) if( tab != NULL ) renderTab( tabBounds, tab ); } + + // After the real tabs, so it always reads as the end of the strip. Empty + // unless calculatePageTabs found itself in edit mode, which is the whole test + // - a book with no pages still gets one, and that is the only thing standing + // between an emptied book and being unrecoverable. + if( mAddTabRect.isValidRect() ) + { + RectI addBounds = mAddTabRect; + addBounds.point += offset; + renderAddTab( addBounds ); + } +} + +void GuiTabBookCtrl::renderAddTab( RectI tabRect ) +{ + GuiEditCtrl* edit = GuiControl::smEditorHandle; + if( edit == NULL ) + return; + + // Ghosted, so it never passes for a page. Brighter under the cursor, so it + // reads as something to click rather than a gap the tabs did not fill - the + // same idea as the frame set's split handles. + ColorI fill = edit->getEditorColor(); + fill.alpha = 100; + + GuiCanvas* root = getRoot(); + if( root != NULL && tabRect.pointInRect( root->getCursorPos() ) ) + fill.alpha = 200; + + dglDrawRectFill( tabRect, fill ); + + dglSetBitmapModulation( getFontColor( edit->mProfile, NormalState ) ); + F32 tempAdjust = mFontSizeAdjust; + mFontSizeAdjust = 1.5f; + renderText( tabRect.point, tabRect.extent, "+", edit->mProfile ); + mFontSizeAdjust = tempAdjust; } void GuiTabBookCtrl::renderTab( RectI tabRect, GuiTabPageCtrl *tab ) @@ -566,11 +692,26 @@ S32 GuiTabBookCtrl::calculatePageTabWidth( GuiTabPageCtrl *page ) void GuiTabBookCtrl::calculatePageTabs() { + // Ahead of every return below: a book that leaves edit mode must not be left + // holding a "+" tab that is no longer drawn, or getAddPageTabRect reports a + // rectangle nothing will answer a click in. + mAddTabRect.set(Point2I(0, 0), Point2I(0, 0)); + + // The "+" tab is the only reason to lay out a book with no pages. Without it + // an empty book short-circuits here exactly as it always has: mTabRect keeps + // the zero it was constructed with, and onRender returns on the invalid rect + // before drawing anything at all. + // + // mTabProfile is the other half of what that short circuit has been quietly + // protecting - the font lookup below dereferences it, and it is only set from + // a profile the constructor names, which a bare engine need not have. + const bool wantAddTab = isEditMode() && mTabProfile != NULL; + // Short Circuit. // // If the tab size is zero, don't render tabs, // and assume it's a tab-less tab-book - JDD - if( mPages.empty()) + if( mPages.empty() && !wantAddTab ) return; S32 currRow = 0; @@ -662,20 +803,74 @@ void GuiTabBookCtrl::calculatePageTabs() }; } + // The "+" tab goes after the last real one, laid out by the same rules but + // square, so it reads as an affordance rather than a page with no name. + // + // It wraps like a tab too. That grows mTabRect and shrinks mPageRect, which + // re-sizes every page - but only while the Gui is being authored, and not + // durably: the book sizes each page from mPageRect whenever one is added, so + // a Gui saved with the "+" on a row of its own loads back unchanged. + // + // Only the counter that survives the loop needs bumping. currRow feeds the + // strip's height for a top or bottom book, currColumn its width for a left or + // right one; the other is written and never read. + if( wantAddTab ) + { + const S32 addSize = tabHeight; + + switch( mTabPosition ) + { + case AlignTop: + case AlignBottom: + // currX > 0 so a strip too narrow for even one square does not push the + // "+" onto an empty row it still cannot fit on. + if( currX + addSize > innerRect.extent.x && currX > 0 ) + { + balanceRow( currRow, currX ); + currRow++; + currX = 0; + } + + mAddTabRect.point.x = currX; + mAddTabRect.point.y = currRow * tabHeight; + mAddTabRect.extent.x = addSize; + mAddTabRect.extent.y = tabHeight; + break; + case AlignLeft: + case AlignRight: + if( currY + addSize > innerRect.extent.y && currY > 0 ) + { + balanceColumn( currColumn, currY ); + currColumn++; + currY = 0; + } + + mAddTabRect.point.x = currColumn * tabHeight; + mAddTabRect.point.y = currY; + mAddTabRect.extent.x = tabHeight; + mAddTabRect.extent.y = addSize; + break; + }; + } + currRow++; currColumn++; Point2I colExtent = Point2I(currColumn * tabHeight, currRow * tabHeight); Point2I outerExtent = getOuterExtent(colExtent, NormalState, mProfile); - // Calculate + // Extent before point, in every case. A bottom or right strip places itself + // by measuring back from the far edge, so reading mTabRect.extent before this + // pass has written it takes the size the strip was LAST time - zero on the + // first pass after construction, which is the only pass a book gets when it + // has no pages to add and so nothing to trigger a second one. switch( mTabPosition ) { case AlignTop: - mTabRect.point.x = 0; - mTabRect.point.y = 0; mTabRect.extent.x = mBounds.extent.x; mTabRect.extent.y = outerExtent.y; + mTabRect.point.x = 0; + mTabRect.point.y = 0; mPageRect.point.x = 0; mPageRect.point.y = mTabRect.extent.y; @@ -684,10 +879,10 @@ void GuiTabBookCtrl::calculatePageTabs() break; case AlignBottom: - mTabRect.point.x = 0; - mTabRect.point.y = mBounds.extent.y - mTabRect.extent.y; mTabRect.extent.x = mBounds.extent.x; mTabRect.extent.y = outerExtent.y; + mTabRect.point.x = 0; + mTabRect.point.y = mBounds.extent.y - mTabRect.extent.y; mPageRect.point.x = 0; mPageRect.point.y = 0; @@ -696,26 +891,26 @@ void GuiTabBookCtrl::calculatePageTabs() break; case AlignLeft: - mTabRect.point.x = 0; - mTabRect.point.y = 0; - mTabRect.extent.x = outerExtent.x; + mTabRect.extent.x = outerExtent.x; mTabRect.extent.y = mBounds.extent.y; + mTabRect.point.x = 0; + mTabRect.point.y = 0; - mPageRect.point.x = mTabRect.extent.x; + mPageRect.point.x = mTabRect.extent.x; mPageRect.point.y = 0; - mPageRect.extent.x = mBounds.extent.x - mTabRect.extent.x; + mPageRect.extent.x = mBounds.extent.x - mTabRect.extent.x; mPageRect.extent.y = mBounds.extent.y; break; case AlignRight: - mTabRect.point.x = mBounds.extent.x - mTabRect.extent.x; - mTabRect.point.y = 0; - mTabRect.extent.x = outerExtent.x; + mTabRect.extent.x = outerExtent.x; mTabRect.extent.y = mBounds.extent.y; + mTabRect.point.x = mBounds.extent.x - mTabRect.extent.x; + mTabRect.point.y = 0; - mPageRect.point.x = 0; + mPageRect.point.x = 0; mPageRect.point.y = 0; - mPageRect.extent.x = mBounds.extent.x - mTabRect.extent.x; + mPageRect.extent.x = mBounds.extent.x - mTabRect.extent.x; mPageRect.extent.y = mTabRect.extent.y; break; diff --git a/engine/source/gui/containers/guiTabBookCtrl.h b/engine/source/gui/containers/guiTabBookCtrl.h index 8e3c5f0d1..7a2c1a3ed 100755 --- a/engine/source/gui/containers/guiTabBookCtrl.h +++ b/engine/source/gui/containers/guiTabBookCtrl.h @@ -85,6 +85,12 @@ class GuiTabBookCtrl : public GuiControl RectI mPageRect; ///< Rectangle of the tab page portion of the control RectI mTabRect; ///< Rectangle of the tab portion of the control + + /// The editor-only "+" tab that follows the last real one, in the same + /// coordinates as TabHeaderInfo::TabRect - local to the tab strip's content, + /// which is what getTabLocalCoord converts a mouse point into. Empty whenever + /// the book is not being authored, which is how everything tests for it. + RectI mAddTabRect; Vector mPages; ///< Vector of pages contained by the control GuiTabPageCtrl* mActivePage; ///< Pointer to the active (selected) tab page child control GuiTabPageCtrl* mHoverTab; ///< Pointer to the tab page that currently has the mouse positioned ontop of its tab @@ -148,6 +154,12 @@ class GuiTabBookCtrl : public GuiControl /// @param tabRect the rectangle to render the tab into /// @param tab pointer to the tab page control for which to render the tab void renderTab( RectI tabRect, GuiTabPageCtrl* tab ); + + /// Draw the editor-only "+" tab. Ghosted rather than drawn as a real tab, + /// because it is an affordance and not a page - the same treatment + /// GuiChainCtrl gives the space it keeps at the end of itself. + /// @param tabRect the rectangle to render into, in global coordinates + void renderAddTab( RectI tabRect ); /// @} /// @name Page Management @@ -159,6 +171,13 @@ class GuiTabBookCtrl : public GuiControl /// This may change in the future. void addNewPage(); + /// Ask the Gui Editor for a page, because the "+" tab was clicked. + /// + /// The book draws the affordance; it does not make the page. A page created + /// while authoring has to be themed, recorded for undo and announced to the + /// Explorer tree, none of which a control knows how to do. + void requestNewPage(); + U32 getSelectedPage(); /// Select a tab page based on an index @@ -184,6 +203,14 @@ class GuiTabBookCtrl : public GuiControl /// @name Internal Utility Functions /// @{ + /// Show the active page and hide the rest. + /// + /// The quiet half of selectPage: no script callback, so it is safe to call + /// while a child is arriving or leaving. selectPage ends in onTabSelected, + /// and EditorCore's handler opens the editor a page belongs to - which is not + /// something a book can afford to trigger from inside addObject. + void syncPageVisibility(); + /// Update ourselves by hooking common GuiControl functionality. void setUpdate(); @@ -213,6 +240,13 @@ class GuiTabBookCtrl : public GuiControl /// Changes a local point to a point in the inner rect of the tab section. Point2I getTabLocalCoord(const Point2I &src); + /// The "+" tab in global coordinates, or an empty rect when there is none. + /// + /// The explicit inverse of getTabLocalCoord, worked out on demand rather than + /// remembered from the last render: script asks about a book the moment it is + /// dropped, which is before the book has drawn a frame. + RectI getAddTabGlobalRect(); + /// @} /// @name Sizing diff --git a/engine/source/gui/containers/guiTabBookCtrl_ScriptBinding.h b/engine/source/gui/containers/guiTabBookCtrl_ScriptBinding.h index c82b681c1..28b941e83 100644 --- a/engine/source/gui/containers/guiTabBookCtrl_ScriptBinding.h +++ b/engine/source/gui/containers/guiTabBookCtrl_ScriptBinding.h @@ -59,4 +59,39 @@ ConsoleMethodWithDocs(GuiTabBookCtrl, setTabProfile, ConsoleVoid, 3, 3, (GuiCont if (Sim::findObject(argv[2], profile)) object->setControlTabProfile(profile); -} \ No newline at end of file +} + +/*! Appends an untitled page to the book. + + The raw form: it names the page itself, puts it on GuiTabPageProfile and + tells nobody. The Gui Editor does not use this - a page made there has to be + themed, recorded for undo and announced to the Explorer tree, so the editor + builds its own from GuiEditorBrain::newTabPage. This is for building a book + from script or from C++, which is what GuiFrameSetCtrl does when it docks a + window. + @return No return value +*/ +ConsoleMethodWithDocs(GuiTabBookCtrl, addPage, ConsoleVoid, 2, 2, ()) +{ + object->addNewPage(); +} + +/*! Returns the bounds of the editor-only "+" tab as "x y width height", in + global coordinates. + + Empty - "0 0 0 0" - unless the book is inside the Gui being authored, which + is the only time the "+" is drawn. A book with no pages at all still reports + one; that is what stops an emptied book from being unrecoverable. + @return The rectangle the "+" occupies on screen. +*/ +ConsoleMethodWithDocs(GuiTabBookCtrl, getAddPageTabRect, ConsoleString, 2, 2, ()) +{ + RectI rect = object->getAddTabGlobalRect(); + + char* buffer = Con::getReturnBuffer(64); + dSprintf(buffer, 64, "%d %d %d %d", rect.point.x, rect.point.y, rect.extent.x, rect.extent.y); + + return buffer; +} + +ConsoleMethodGroupEndWithDocs(GuiTabBookCtrl) \ No newline at end of file diff --git a/engine/source/gui/containers/guiTabPageCtrl.cc b/engine/source/gui/containers/guiTabPageCtrl.cc index 188810583..248bb5d10 100644 --- a/engine/source/gui/containers/guiTabPageCtrl.cc +++ b/engine/source/gui/containers/guiTabPageCtrl.cc @@ -28,6 +28,11 @@ #include "gui/guiDefaultControlRender.h" #include "gui/editor/guiEditCtrl.h" +// Only for canBeChildOf below. The include belongs here rather than in the +// header: guiTabBookCtrl.h includes guiTabPageCtrl.h, so the two headers cannot +// include each other. +#include "gui/containers/guiTabBookCtrl.h" + IMPLEMENT_CONOBJECT(GuiTabPageCtrl); GuiTabPageCtrl::GuiTabPageCtrl(void) @@ -105,6 +110,16 @@ GuiControl *GuiTabPageCtrl::findPrevTabable(GuiControl *curResponder, bool first return tabCtrl; } +// A page is a tab's worth of a book and nothing else: its geometry is dictated +// by the book, its caption is what the book draws on the tab, and outside one it +// renders as a bare panel that nothing can select a tab for. So it refuses every +// other parent, and the Gui Editor leaves it where it is. Book to book is still +// allowed - that is a page moving between two things that can both hold it. +bool GuiTabPageCtrl::canBeChildOf(GuiControl* parent) +{ + return dynamic_cast(parent) != NULL; +} + void GuiTabPageCtrl::setText(const char *txt) { Parent::setText( txt ); diff --git a/engine/source/gui/containers/guiTabPageCtrl.h b/engine/source/gui/containers/guiTabPageCtrl.h index 24936fa7b..fc0c361c8 100644 --- a/engine/source/gui/containers/guiTabPageCtrl.h +++ b/engine/source/gui/containers/guiTabPageCtrl.h @@ -42,6 +42,10 @@ class GuiTabPageCtrl : public GuiControl void selectWindow(void); ///< Select this window + /// A page only means anything inside a GuiTabBookCtrl, so it refuses every + /// other parent. See GuiControl::canBeChildOf. + bool canBeChildOf(GuiControl* parent); + virtual void setText(const char *txt = NULL); ///< Override setText function to signal parent we need to update. void onRender(Point2I offset, const RectI &updateRect); void parentResized(const Point2I& oldParentExtent, const Point2I& newParentExtent); diff --git a/engine/source/gui/editor/guiEditCtrl.cc b/engine/source/gui/editor/guiEditCtrl.cc index bb95a9640..efd5093b9 100755 --- a/engine/source/gui/editor/guiEditCtrl.cc +++ b/engine/source/gui/editor/guiEditCtrl.cc @@ -143,6 +143,22 @@ ConsoleMethod(GuiEditCtrl, setCurrentAddSet, void, 3, 3, "(GuiControl ctrl) Set object->setCurrentAddSet(addSet); } +ConsoleMethod(GuiEditCtrl, moveSelectionToCtrl, void, 3, 3, "(GuiControl parent) Reparent every selected control into the given container.\n" + "What a drag across the canvas onto a container does. Controls that refuse the\n" + "parent - see GuiControl::canBeChildOf - and locked ones are left where they are.\n" + "@param parent The container to move the selection into.\n" + "@return No return value.") +{ + GuiControl* newParent; + + if (!Sim::findObject(argv[2], newParent)) + { + Con::printf("%s(): Invalid control: %s", argv[0], argv[2]); + return; + } + object->moveSelectionToCtrl(newParent); +} + ConsoleMethod(GuiEditCtrl, getCurrentAddSet, S32, 2, 2, "()\n @return Returns the set to which new controls will be added") { const GuiControl* add = object->getCurrentAddSet(); @@ -1198,6 +1214,11 @@ void GuiEditCtrl::moveSelectionToCtrl(GuiControl* newParent) if (ctrl->isLocked()) continue; + // skip controls that will not live there - a tab page outside a tab book + // is the case this exists for + if (!ctrl->canBeChildOf(newParent)) + continue; + Point2I globalpos = ctrl->localToGlobalCoord(Point2I(0, 0)); newParent->addObject(ctrl); Point2I newpos = ctrl->globalToLocalCoord(globalpos) + ctrl->mBounds.point; diff --git a/engine/source/gui/guiControl.h b/engine/source/gui/guiControl.h index 5b1f96a33..7948e975c 100755 --- a/engine/source/gui/guiControl.h +++ b/engine/source/gui/guiControl.h @@ -684,6 +684,18 @@ class GuiControl : public SimGroup, public virtual Tickable /// @param offset the offset which is representative of the units x and y that the editor takes up on screen virtual bool onMouseDraggedEditor(const GuiEvent &event, const Point2I& offset) { return false; }; + /// Whether this control is allowed to become a child of the given container. + /// + /// The Gui Editor asks before it reparents anything - a drag in the Explorer + /// tree, a drag across the canvas, a paste - and leaves the control where it + /// is when the answer is no. Nothing else in the engine asks: SimGroup::addObject + /// cannot refuse, so this is advice the editor takes, not an invariant the + /// object model enforces. + /// + /// A control that only means something inside one particular parent overrides + /// this. GuiTabPageCtrl is the one that does. + virtual bool canBeChildOf(GuiControl* parent) { return true; }; + /// @} /// @name Tabs diff --git a/engine/source/gui/guiControl_ScriptBinding.h b/engine/source/gui/guiControl_ScriptBinding.h index cea1940f1..bea4a719c 100644 --- a/engine/source/gui/guiControl_ScriptBinding.h +++ b/engine/source/gui/guiControl_ScriptBinding.h @@ -90,6 +90,25 @@ ConsoleMethodWithDocs(GuiControl, childrenReordered, ConsoleVoid, 2, 2, ()) object->childrenReordered(); } +/*! Whether this control is allowed to become a child of the given container. + + The Gui Editor asks before every reparent it performs - a drag in the Explorer + tree, a drag across the canvas, a paste - and leaves the control where it is + when the answer is false. Almost everything answers true; a GuiTabPageCtrl + answers true only for a GuiTabBookCtrl. + + Nothing enforces this below the editor: add() will still put a control + anywhere. This is the question, not the gate. + @param parent The container being proposed. + @return True when the control would be at home there. +*/ +ConsoleMethodWithDocs(GuiControl, canBeChildOf, ConsoleBool, 3, 3, (GuiControl parent)) +{ + GuiControl* pParent = dynamic_cast(Sim::findObject(argv[2])); + + return object->canBeChildOf(pParent); +} + /*! @return Returns the Id of the parent control */ ConsoleMethodWithDocs( GuiControl, getParent, ConsoleInt, 2, 2, ()) diff --git a/engine/source/gui/guiTreeViewCtrl.cc b/engine/source/gui/guiTreeViewCtrl.cc index c88754b2f..b4167b1d9 100755 --- a/engine/source/gui/guiTreeViewCtrl.cc +++ b/engine/source/gui/guiTreeViewCtrl.cc @@ -257,11 +257,10 @@ SimObject* GuiTreeViewCtrl::getItemObject(TreeItem* item) return item ? static_cast(item->itemData) : nullptr; } -void GuiTreeViewCtrl::reorderFromDrag() +SimGroup* GuiTreeViewCtrl::resolveDropTarget(TreeItem* dragItem) { - TreeItem* dragItem = grabItemPtr(mDragIndex); if (!dragItem) - return; + return NULL; if (mReorderMethod == ReorderMethod::Below && dragItem->isOpen) { @@ -270,20 +269,56 @@ void GuiTreeViewCtrl::reorderFromDrag() // The container the dragged items land in: the drag item itself when // inserting into it, otherwise the drag item's parent branch. For a - // non-Insert drop at the root there is no parent branch, so bail. + // non-Insert drop at the root there is no parent branch. TreeItem* targetItem = (mReorderMethod == ReorderMethod::Insert) ? dragItem : dragItem->trunk; if (!targetItem) - return; + return NULL; // The drop target must be a real SimGroup (a GuiControl hierarchy). If the // item's object isn't one - or was deleted - there is nothing to reorder. - SimGroup* target = dynamic_cast(getItemObject(targetItem)); + return dynamic_cast(getItemObject(targetItem)); +} + +bool GuiTreeViewCtrl::selectionAcceptsTarget(SimGroup* target) +{ + // A non-GuiControl target answers NULL here, which is the right question to + // ask: a control that insists on a particular kind of parent is not at home + // in a bare SimGroup either. + GuiControl* parent = dynamic_cast(target); + + for (S32 i = 0; i < mItems.size(); i++) + { + TreeItem* treeItem = dynamic_cast(mItems[i]); + if (treeItem && treeItem->isSelected) + { + GuiControl* ctrl = dynamic_cast(getItemObject(treeItem)); + if (ctrl && !ctrl->canBeChildOf(parent)) + return false; + } + } + + return true; +} + +void GuiTreeViewCtrl::reorderFromDrag() +{ + TreeItem* dragItem = grabItemPtr(mDragIndex); + if (!dragItem) + return; + + SimGroup* target = resolveDropTarget(dragItem); if (!target) { Con::warnf("GuiTreeViewCtrl::reorderFromDrag - drop target is not a SimGroup; ignoring reorder"); return; } + // The drag indicator has already refused this, so reaching it means the + // hover and the drop disagreed. Bail rather than move a control somewhere it + // said it does not belong. + if (!selectionAcceptsTarget(target)) + return; + // Past every bail, so the rearrangement below is certain to happen. A drop // can move any number of selected items into any number of containers, and // the only record of where they came from is the hierarchy itself - hence a @@ -323,7 +358,15 @@ void GuiTreeViewCtrl::reorderFromDrag() if (obj) { target->addObject(obj); - target->bringObjectToFront(obj); + + // A container is allowed to turn a child away inside addObject - + // a tab book re-homes anything that is not a page - so the object + // may not be in the target at all. bringObjectToFront reads + // front() to build its argument, before reOrder gets the chance + // to notice the object is not a member, and front() on an empty + // list dereferences nothing at all. + if (target->isMember(obj)) + target->bringObjectToFront(obj); } } } @@ -335,8 +378,8 @@ void GuiTreeViewCtrl::reorderFromDrag() group->bringObjectToFront(obj); } - // target is the same object as the container above; reorder its children. - GuiControl* control = dynamic_cast(getItemObject(targetItem)); + // The container the items landed in; tell it its children moved. + GuiControl* control = dynamic_cast(target); if (control) { control->childrenReordered(); @@ -591,6 +634,15 @@ S32 GuiTreeViewCtrl::getHitIndex(const GuiEvent& event) } } + // And the controls get a say in where they are put. A drop the + // tree would refuse must not draw an indicator promising it - + // nothing happening is a great deal harder to read than no line + // appearing in the first place. + if (mIsDragLegal && !selectionAcceptsTarget(resolveDropTarget(treeItem))) + { + mIsDragLegal = false; + } + mDragIndex = j; return i; } diff --git a/engine/source/gui/guiTreeViewCtrl.h b/engine/source/gui/guiTreeViewCtrl.h index 8a0d64923..c95666145 100755 --- a/engine/source/gui/guiTreeViewCtrl.h +++ b/engine/source/gui/guiTreeViewCtrl.h @@ -76,6 +76,15 @@ class GuiTreeViewCtrl : public GuiListBoxCtrl // wrong-typed item rather than trusting itemData is a GuiControl/SimGroup. void reorderFromDrag(); + // The container a drop on the given row would land in: the row itself when + // inserting into it, otherwise its parent branch. Both the hover, which draws + // the drop indicator, and the drop itself resolve it through here, so the + // indicator cannot promise a target the drop then refuses. + SimGroup* resolveDropTarget(TreeItem* dragItem); + // Whether every selected control would accept that container as a parent. + // GuiControl::canBeChildOf is the question; a tab page is what says no. + bool selectionAcceptsTarget(SimGroup* target); + // Keyboard navigation has to work in visible-row space. Collapsed branches // stay in mItems with isVisible false and never render, so stepping raw // indices - which is what the list box base class does - walks the selection diff --git a/tests/shots/tabBook.cs b/tests/shots/tabBook.cs new file mode 100644 index 000000000..1f83c48eb --- /dev/null +++ b/tests/shots/tabBook.cs @@ -0,0 +1,107 @@ +// Visual harness for the tab book's editor-only "+" tab. +// +// Two books in every shot, because the two cases fail differently: +// populated three pages, so the "+" has to land after the last real tab +// empty no pages at all -- which before this drew NOTHING, tab strip +// included, because calculatePageTabs short-circuited and left +// mTabRect zero for onRender to bail on +// +// All four tab positions, not just the two obvious ones: a bottom or right strip +// places itself by measuring back from the far edge, and used to read its own +// extent before this pass had written it. A book with pages hid that behind a +// second layout pass; a book with none gets only the first. +// +// Run: tests/run.ps1 -Shots tabBook ; look in shots/. + +setLogMode(1); +$Scripts::ignoreDSOs = true; +setScriptExecEcho(false); +trace(false); + +testExec("editor/main.cs"); +schedule(2500, 0, "tbOpenProject"); + +// A real project, loaded the way the project selector does it, so the books are +// drawn wearing a project's theme rather than the engine's fallbacks. +function tbOpenProject() +{ + ProjectManager.setProjectFolder("PlanetX"); + EditorCore.projectSelector.onProjectSelected(pathConcat(getMainDotCsDir(), "PlanetX")); + schedule(2500, 0, "tbOpenEditor"); +} + +// Loading AppCore starts the project's game over the canvas; the editor comes +// back the way Ctrl+~ does it. Pages register in load order: EditorConsole, +// ProjectManager, AssetAdmin, GuiEditor. +function tbOpenEditor() +{ + EditorCore.toggleEditor(); + EditorCore.tabBook.selectPage(3); + schedule(1500, 0, "tbStep1"); +} + +function tbStep1() +{ + $tbTheme = GuiEditor.themeLibrary.createTheme("TabBookShotTheme"); + GuiEditor.themeName = $tbTheme.getName(); + + // Where the canvas can actually show something. A Gui is authored at its own + // size and the editor's frame is a few hundred pixels wide, so the middle of + // the root is off screen more often than not; the brain already works out + // which part of a container is visible for click placement. + %room = GuiEditor.brain.visiblePartOf(GuiEditor.rootGui); + %left = getWord(%room, 0) + 20; + %top = getWord(%room, 1) + 20; + + $tbBook = tbPlace(%left, %top); + for(%i = 1; %i <= 3; %i++) + { + %page = new GuiTabPageCtrl() { Text = "Page " @ %i; }; + $tbBook.add(%page); + GuiEditor.themeApplier.applyToBranch(%page, $tbTheme, true); + } + + $tbEmpty = tbPlace(%left, %top + 160); + + createPath(testRoot("shots/")); + + $tbCases = "Top" TAB "Bottom" TAB "Left" TAB "Right"; + $tbIndex = 0; + schedule(800, 0, "tbShoot"); +} + +function tbPlace(%x, %y) +{ + %book = new GuiTabBookCtrl() { Extent = "300 130"; }; + GuiEditor.rootGui.add(%book); + %book.setPositionGlobal(%x, %y); + GuiEditor.themeApplier.applyToBranch(%book, $tbTheme, true); + return %book; +} + +function tbShoot() +{ + %pos = getField($tbCases, $tbIndex); + $tbBook.TabPosition = %pos; + $tbEmpty.TabPosition = %pos; + + // solveDirty notices the changed field on the next onPreRender and resizes, + // which is what re-runs calculatePageTabs. Nothing to call by hand. + schedule(600, 0, "tbGrab", %pos); +} + +function tbGrab(%pos) +{ + screenShot(testRoot("shots/tabbook-" @ %pos @ ".png"), "PNG"); + echo("SHOT: wrote tabbook-" @ %pos @ ".png"); + + $tbIndex++; + if($tbIndex < getFieldCount($tbCases)) + { + schedule(300, 0, "tbShoot"); + return; + } + + echo("SHOT DONE"); + schedule(300, 0, "quit"); +} diff --git a/tests/smoke/palette.cs b/tests/smoke/palette.cs index c94e6cd29..9c06c3fe5 100644 --- a/tests/smoke/palette.cs +++ b/tests/smoke/palette.cs @@ -100,7 +100,27 @@ function palTableChecks() %icons.groupFor(%key) $= %group); } } - palCheck("the groups account for every entry (" @ %sum @ " of " @ %total @ ")", %sum == %total); + + // Every entry is either in a group or refused -- none silently dropped. A + // refused entry keeps its row, and so its frame and its label, because the + // frame IS the row index; it just isn't offered. GuiTabPageCtrl is the one + // that is: only a tab book makes a page, from the + tab it draws in the + // editor. + %refused = 0; + for(%i = 0; %i < %total; %i++) + { + %key = getField(%all, %i); + if(!%icons.isPlaceableClass(%icons.classFor(%key))) + { + %refused++; + } + } + palCheck("the table refuses one entry (" @ %refused @ ")", %refused == 1); + palCheck("Tab Page is the refused one", !%icons.isPlaceableClass("GuiTabPageCtrl")); + palCheck("a refused entry keeps its icon", %icons.isKnown("GuiTabPageCtrl")); + palCheck("a refused entry still counts as covered", %icons.coversClass("GuiTabPageCtrl")); + palCheck("every entry is either grouped or refused (" @ %sum @ " + " @ %refused @ + " of " @ %total @ ")", (%sum + %refused) == %total); // The fallback is reachable but never offered. palCheck("the fallback is not in any group", %icons.groupFor("unknown") $= ""); @@ -176,7 +196,10 @@ function palPaletteChecks() getWord(%group.getExtent(), 1) > $GuiEditorControlGroup::headerHeight); %tiles += %group.tileCount; } - palCheck("30 tiles across the groups (" @ %tiles @ ")", %tiles == 30); + // One fewer than the 30 table entries: Tab Page is refused, so it has a row + // and an icon but no tile. + palCheck("29 tiles across the groups (" @ %tiles @ ")", %tiles == 29); + palCheck("no Tab Page tile", palFindTile("GuiTabPageCtrl") == 0); palModeChecks(); } diff --git a/tests/smoke/tabBook.cs b/tests/smoke/tabBook.cs new file mode 100644 index 000000000..9fac7b271 --- /dev/null +++ b/tests/smoke/tabBook.cs @@ -0,0 +1,402 @@ +//----------------------------------------------------------------------------- +// Authoring a GuiTabBookCtrl in the Gui Editor. +// +// A tab page is the one control the palette will not offer, because it is the +// one control that means nothing outside its container. So the book makes its +// own: one when it is dropped, and one for every click on the "+" tab it draws +// at the end of its strip while the Gui is being authored. This checks that the +// palette really has stopped offering it, that both routes to a page produce the +// same object, that a book can be emptied and still be recoverable, and that a +// page cannot be dragged, dropped or pasted anywhere but into a book. +// +// Runs on the real editor UI throughout, because the "+" only exists where +// isEditMode() is true - which needs the editor pushed onto the canvas rather +// than merely registered. +// +// Run: tests/run.ps1 tabBook ; grep TABBOOK in console.log. +//----------------------------------------------------------------------------- + +setLogMode(2); +setScriptExecEcho(false); +trace(false); +$Scripts::ignoreDSOs = true; +setCompanyAndProduct("Torque Game Engines", "Torque2D"); +ModuleDatabase.EchoInfo = false; +AssetDatabase.EchoInfo = false; +AssetDatabase.IgnoreAutoUnload = true; + +testExec("editor/main.cs"); + +function tbCheck(%label, %condition) +{ + echo(%condition ? ("TABBOOK PASS: " @ %label) : ("TABBOOK FAIL: " @ %label)); +} + +function tbUndoCount() +{ + return GuiEditor.undoRecorder.undoCount(); +} + +function tbSelect(%ctrl) +{ + GuiEditor.brain.clearSelection(); + GuiEditor.brain.select(%ctrl); +} + +// Is the inner rect wholly inside the outer one? Both are "x y width height". +function tbInside(%inner, %outer) +{ + return getWord(%inner, 0) >= getWord(%outer, 0) && + getWord(%inner, 1) >= getWord(%outer, 1) && + (getWord(%inner, 0) + getWord(%inner, 2)) <= (getWord(%outer, 0) + getWord(%outer, 2)) && + (getWord(%inner, 1) + getWord(%inner, 3)) <= (getWord(%outer, 1) + getWord(%outer, 3)); +} + +function tbGlobalRect(%ctrl) +{ + return %ctrl.getGlobalPosition() SPC %ctrl.getExtent(); +} + +schedule(2000, 0, "tbStep1"); + +function tbStep1() +{ + ProjectManager.setProjectFolder("PlanetX"); + ModuleDatabase.ScanModules("PlanetX"); + ModuleDatabase.LoadExplicit("AppCore", 1); + + // By id: a bare identifier in TorqueScript is a string, and everything here + // compares object handles. + $tbTheme = nameToID("PlanetX"); + tbCheck("PlanetX theme loaded", isObject($tbTheme)); + + GuiEditor.open(); + GuiEditor.setTheme($tbTheme, false); + + // The real editor UI, on the canvas, from the very start. Everything about + // the "+" is gated on isEditMode(), which answers false until the Gui being + // edited is actually on screen under the editor. + EditorCore.open(); + EditorCore.tabBook.selectPageName("Gui Editor"); + + schedule(800, 0, "tbStepPalette"); +} + +//----------------------------------------------------------------------------- +// The palette, which has to refuse the class without losing it. +// +// The row stays in the icon table: the frame number IS the row index, so +// dropping the row would repoint every icon below it onto the wrong art, and +// dropping the class from covered[] would have the sweep over the class registry +// offer it straight back with a question mark for an icon. +//----------------------------------------------------------------------------- + +function tbStepPalette() +{ + %icons = GuiEditor.controlIcons; + + tbCheck("the palette will not place a tab page", !%icons.isPlaceableClass("GuiTabPageCtrl")); + tbCheck("no Tab Page tile in Layout", + strstr(%icons.keysInGroup("Layout"), "GuiTabPageCtrl") == -1); + tbCheck("but Tab Book is still there", + strstr(%icons.keysInGroup("Layout"), "GuiTabBookCtrl") != -1); + + tbCheck("the entry survives in the table", %icons.isKnown("GuiTabPageCtrl")); + tbCheck("and still counts as covered", %icons.coversClass("GuiTabPageCtrl")); + tbCheck("so it keeps its label (" @ %icons.labelFor("GuiTabPageCtrl") @ ")", + %icons.labelFor("GuiTabPageCtrl") $= "Tab Page"); + + // The two frames either side of the seam. If the row had been deleted rather + // than refused, Window would have slid down onto Tab Page's art. + tbCheck("Tab Page keeps frame 28 (" @ %icons.frameFor("GuiTabPageCtrl") @ ")", + %icons.frameFor("GuiTabPageCtrl") == 28); + tbCheck("Window keeps frame 29 (" @ %icons.frameFor("GuiWindowCtrl") @ ")", + %icons.frameFor("GuiWindowCtrl") == 29); + + schedule(300, 0, "tbStepDrop"); +} + +//----------------------------------------------------------------------------- +// Dropping a book, which has to arrive with a page in it. +//----------------------------------------------------------------------------- + +function tbStepDrop() +{ + GuiEditor.undoRecorder.clear(); + GuiEditor.brain.setCurrentAddSet(GuiEditor.rootGui); + + // Where the canvas can actually show something: a drop is only a drop when + // the middle of the payload is over the Gui being edited. + %room = GuiEditor.brain.visiblePartOf(GuiEditor.rootGui); + $tbAt = (getWord(%room, 0) + 30) SPC (getWord(%room, 1) + 30); + + $tbBook = new GuiTabBookCtrl() { Extent = "300 130"; Position = $tbAt; }; + GuiEditor.brain.onControlDropped($tbBook, "50 50"); + + tbCheck("the book arrived", $tbBook.getParent() == GuiEditor.rootGui); + tbCheck("carrying exactly one page (" @ $tbBook.getCount() @ ")", $tbBook.getCount() == 1); + + $tbPage = $tbBook.getObject(0); + tbCheck("which is a tab page", $tbPage.getClassName() $= "GuiTabPageCtrl"); + tbCheck("captioned \"" @ $tbPage.getText() @ "\"", $tbPage.getText() $= "Page 1"); + tbCheck("the only page is the active one", $tbBook.getSelectedPage() == 0); + tbCheck("and it is showing", $tbPage.isVisible()); + + // Themed by category rather than by profile name, which is what the theme + // applier actually decides. + tbCheck("the book was themed on arrival (" @ $tbBook.Profile.category @ ")", + $tbBook.Profile.category $= "TabBook"); + tbCheck("and the page it brought with it (" @ $tbPage.Profile.category @ ")", + $tbPage.Profile.category $= "TabPage"); + + // The page came with the book, so it is part of the book arriving rather + // than a second thing the user did. + tbCheck("the whole thing is one undo step (" @ tbUndoCount() @ ")", tbUndoCount() == 1); + + // After the drop's own schedule(40), so the undo is not racing it. + schedule(200, 0, "tbStepDropUndo"); +} + +function tbStepDropUndo() +{ + %trash = GuiEditor.brain.getTrash(); + + GuiEditor.Undo(); + + // getGroup, not getParent: a control sitting in the trash - a plain SimGroup + // - reads as having no parent at all. + tbCheck("undo took the book out of the Gui", $tbBook.getGroup() == %trash); + tbCheck("with its page still inside it", $tbPage.getParent() == $tbBook); + + GuiEditor.Redo(); + tbCheck("redo put the book back", $tbBook.getParent() == GuiEditor.rootGui); + tbCheck("still holding its page (" @ $tbBook.getCount() @ ")", $tbBook.getCount() == 1); + tbCheck("which is showing again", $tbPage.isVisible()); + + schedule(300, 0, "tbStepPlus"); +} + +//----------------------------------------------------------------------------- +// The "+" tab, which is what GuiTabBookCtrl::requestNewPage asks for. Called +// directly here: the click that reaches it is C++, and what this suite is about +// is what the editor does once asked. +//----------------------------------------------------------------------------- + +function tbStepPlus() +{ + GuiEditor.undoRecorder.clear(); + + GuiEditor.brain.onAddTabPage($tbBook); + + tbCheck("the book grew a page (" @ $tbBook.getCount() @ ")", $tbBook.getCount() == 2); + + $tbPage2 = $tbBook.getObject(1); + tbCheck("numbered on from the last (" @ $tbPage2.getText() @ ")", $tbPage2.getText() $= "Page 2"); + tbCheck("themed like any arrival (" @ $tbPage2.Profile.category @ ")", + $tbPage2.Profile.category $= "TabPage"); + + tbCheck("the new page is the active tab", $tbBook.getSelectedPage() == 1); + tbCheck("so it is the one showing", $tbPage2.isVisible()); + tbCheck("and the first one is not", !$tbPage.isVisible()); + + tbCheck("the new page is selected", GuiEditor.brain.selectionList() $= $tbPage2); + tbCheck("and is where the next control would land", + GuiEditor.brain.getCurrentAddSet() == $tbPage2); + + tbCheck("adding a page is one step (" @ tbUndoCount() @ ")", tbUndoCount() == 1); + + GuiEditor.Undo(); + tbCheck("undo took the page back off (" @ $tbBook.getCount() @ ")", $tbBook.getCount() == 1); + tbCheck("leaving the book alone", $tbBook.getParent() == GuiEditor.rootGui); + tbCheck("and the first page showing again", $tbPage.isVisible()); + + GuiEditor.Redo(); + tbCheck("redo put it back (" @ $tbBook.getCount() @ ")", $tbBook.getCount() == 2); + + // A third, to prove the numbering keeps counting rather than restarting. + GuiEditor.brain.onAddTabPage($tbBook); + tbCheck("a third page carries on the numbering (" @ $tbBook.getObject(2).getText() @ ")", + $tbBook.getObject(2).getText() $= "Page 3"); + + schedule(300, 0, "tbStepGeometry"); +} + +//----------------------------------------------------------------------------- +// Where the "+" is, which is the half of it a click depends on. +//----------------------------------------------------------------------------- + +function tbStepGeometry() +{ + %rect = $tbBook.getAddPageTabRect(); + %book = tbGlobalRect($tbBook); + + tbCheck("the book reports a \"+\" tab (" @ %rect @ ")", getWord(%rect, 2) > 0); + tbCheck("square, as an affordance rather than a tab (" @ + getWord(%rect, 2) @ "x" @ getWord(%rect, 3) @ ")", + getWord(%rect, 2) == getWord(%rect, 3)); + tbCheck("inside the book it belongs to", tbInside(%rect, %book)); + + // Three tabs at the default minimum width sit to its left, so a "+" at the + // very start of the strip would mean it had been laid out before them. + tbCheck("after the tabs rather than before them", + getWord(%rect, 0) > getWord(%book, 0)); + + // A book with no pages is an ordinary state now that the palette cannot + // supply one, and the "+" is the only way back out of it. + $tbEmpty = new GuiTabBookCtrl() { Extent = "300 130"; Position = $tbAt; }; + GuiEditor.rootGui.add($tbEmpty); + tbCheck("a book with no pages still offers a \"+\" (" @ $tbEmpty.getAddPageTabRect() @ ")", + getWord($tbEmpty.getAddPageTabRect(), 2) > 0); + + // Outside the Gui being authored there is no "+" at all - which is what + // keeps it off the editor's own chrome, every window of which is a real + // GuiControl on the same canvas. + %loose = new GuiTabBookCtrl() { Extent = "300 130"; }; + tbCheck("a book outside the edited Gui has none (" @ %loose.getAddPageTabRect() @ ")", + getWord(%loose.getAddPageTabRect(), 2) == 0); + %loose.delete(); + + schedule(300, 0, "tbStepDelete"); +} + +//----------------------------------------------------------------------------- +// Removing pages, which is the ordinary Delete and nothing new. +//----------------------------------------------------------------------------- + +function tbStepDelete() +{ + GuiEditor.undoRecorder.clear(); + + // The ACTIVE page first, which is the case with something to get wrong: the + // book has to promote another page AND show it, where before it promoted one + // and left it hidden from whenever some other tab was last chosen. + %active = $tbBook.getObject($tbBook.getSelectedPage()); + tbSelect(%active); + GuiEditor.brain.onObjectRemoved(%active); + + tbCheck("the page went (" @ $tbBook.getCount() @ ")", $tbBook.getCount() == 2); + tbCheck("and the page that took over is showing", + $tbBook.getObject($tbBook.getSelectedPage()).isVisible()); + + // Then down to nothing, because emptying a book is what used to leave it + // drawing nothing at all. + + while($tbBook.getCount() > 0) + { + %page = $tbBook.getObject(0); + tbSelect(%page); + GuiEditor.brain.onObjectRemoved(%page); + } + + tbCheck("the book can be emptied (" @ $tbBook.getCount() @ ")", $tbBook.getCount() == 0); + tbCheck("and is still recoverable (" @ $tbBook.getAddPageTabRect() @ ")", + getWord($tbBook.getAddPageTabRect(), 2) > 0); + + // Straight back out of it, through the same door the "+" uses. + GuiEditor.brain.onAddTabPage($tbBook); + tbCheck("the \"+\" refills an emptied book (" @ $tbBook.getCount() @ ")", + $tbBook.getCount() == 1); + tbCheck("numbering from the start again (" @ $tbBook.getObject(0).getText() @ ")", + $tbBook.getObject(0).getText() $= "Page 1"); + tbCheck("with the new page showing", $tbBook.getObject(0).isVisible()); + + schedule(300, 0, "tbStepUndoDelete"); +} + +function tbStepUndoDelete() +{ + // Right back to three pages, and exactly one of them visible at every point + // along the way. Restoring the page that was active when it was deleted is + // the case that used to draw two pages on top of each other: the recorder + // puts back position, extent and sizing, and visibility is not among them. + while(tbUndoCount() > 0) + { + GuiEditor.Undo(); + } + + tbCheck("undo walked back to three pages (" @ $tbBook.getCount() @ ")", + $tbBook.getCount() == 3); + + %showing = 0; + for(%i = 0; %i < $tbBook.getCount(); %i++) + { + if($tbBook.getObject(%i).isVisible()) + { + %showing++; + } + } + tbCheck("with exactly one of them showing (" @ %showing @ ")", %showing == 1); + + schedule(300, 0, "tbStepRules"); +} + +//----------------------------------------------------------------------------- +// Where a page is allowed to live. GuiControl::canBeChildOf is the rule; the +// Explorer tree drag, the canvas drag and paste are the three doors that ask it. +//----------------------------------------------------------------------------- + +function tbStepRules() +{ + $tbPanel = new GuiControl() { Position = "10 400"; Extent = "300 200"; }; + GuiEditor.rootGui.add($tbPanel); + + $tbButton = new GuiButtonCtrl() { Position = "10 10"; Extent = "80 30"; Text = "B"; }; + $tbPanel.add($tbButton); + + %page = $tbBook.getObject(0); + + tbCheck("a page belongs in its book", %page.canBeChildOf($tbBook)); + tbCheck("and in any other book", %page.canBeChildOf($tbEmpty)); + tbCheck("but not in a panel", !%page.canBeChildOf($tbPanel)); + tbCheck("nor on the root", !%page.canBeChildOf(GuiEditor.rootGui)); + tbCheck("an ordinary control still goes anywhere", $tbButton.canBeChildOf($tbPanel)); + tbCheck("including at a book, which re-homes it itself", + $tbButton.canBeChildOf($tbBook)); + + // The canvas drag. Both halves, because a rule that refuses everything would + // pass the first check on its own. + tbSelect(%page); + GuiEditor.brain.moveSelectionToCtrl($tbPanel); + tbCheck("dragging a page onto a panel leaves it alone", %page.getParent() == $tbBook); + + tbSelect(%page); + GuiEditor.brain.moveSelectionToCtrl($tbEmpty); + tbCheck("dragging it onto another book moves it", %page.getParent() == $tbEmpty); + + tbSelect($tbButton); + GuiEditor.brain.moveSelectionToCtrl($tbPanel); + tbCheck("an ordinary control is not caught by the rule", + $tbButton.getParent() == $tbPanel); + + schedule(300, 0, "tbStepPaste"); +} + +function tbStepPaste() +{ + %page = $tbEmpty.getObject(0); + + tbSelect(%page); + GuiEditor.Copy(); + + // Into a panel: refused, and the clipboard is left holding it so that + // selecting a book and pasting again does what was meant. + %before = $tbPanel.getCount(); + GuiEditor.brain.setCurrentAddSet($tbPanel); + GuiEditor.Paste(); + tbCheck("pasting a page into a panel puts nothing there (" @ $tbPanel.getCount() @ ")", + $tbPanel.getCount() == %before); + + %before = $tbBook.getCount(); + GuiEditor.brain.setCurrentAddSet($tbBook); + GuiEditor.Paste(); + tbCheck("pasting it into a book does (" @ $tbBook.getCount() @ ")", + $tbBook.getCount() == (%before + 1)); + + schedule(300, 0, "tbDone"); +} + +function tbDone() +{ + echo("TABBOOK DONE"); + quit(); +} diff --git a/tests/smoke/tabBookClick.cs b/tests/smoke/tabBookClick.cs new file mode 100644 index 000000000..a80717621 --- /dev/null +++ b/tests/smoke/tabBookClick.cs @@ -0,0 +1,141 @@ +//----------------------------------------------------------------------------- +// Clicking the "+" tab, for real. +// +// tabBook.cs calls GuiEditorBrain::onAddTabPage directly, which is the right +// test for what the editor does once asked - but it says nothing about the half +// that asks. That half is C++: GuiTabBookCtrl::onMouseDownEditor converts the +// mouse point into the tab strip's own coordinates, tests it against the "+", +// and calls back into script. Nothing about it can be reached from script, and +// its predecessor got the conversion wrong for years (onTouchDown converted and +// this did not), so a click that is a whole border out still looks like a hit. +// +// Driven by tabBookClick.input.ps1, which posts a real WM_LBUTTONDOWN/UP. The +// point is NOT hard-coded there: the engine works out where the "+" actually is +// and hands it over in a file. A hard-coded click landing an inch to the left of +// the "+" would report exactly what a broken hit test reports. +//----------------------------------------------------------------------------- + +setLogMode(2); +setScriptExecEcho(false); +trace(false); +$Scripts::ignoreDSOs = true; +setCompanyAndProduct("Torque Game Engines", "Torque2D"); +ModuleDatabase.EchoInfo = false; +AssetDatabase.EchoInfo = false; +AssetDatabase.IgnoreAutoUnload = true; + +testExec("editor/main.cs"); + +function tcCheck(%label, %condition) +{ + echo(%condition ? ("TABCLICK PASS: " @ %label) : ("TABCLICK FAIL: " @ %label)); +} + +// Where the engine leaves the point for the driver to click. +function tcTargetFile() +{ + return testRoot("shots/tabBookClickTarget.txt"); +} + +schedule(2500, 0, "tcStep1"); + +// Through the project selector, the way a person opens a project. The suites +// that call GuiEditor.open() directly get an editor that is awake - enough for +// isEditMode(), and so enough to call the brain's methods - but leaves the +// project selector sitting on top of it. A posted click lands on whatever is +// actually in front, so this one has to dismiss it properly. +function tcStep1() +{ + ProjectManager.setProjectFolder("PlanetX"); + EditorCore.projectSelector.onProjectSelected(pathConcat(getMainDotCsDir(), "PlanetX")); + + schedule(2500, 0, "tcStep2"); +} + +// Loading AppCore starts the project's game over the canvas; the editor comes +// back the way Ctrl+~ does it. Pages register in load order: EditorConsole, +// ProjectManager, AssetAdmin, GuiEditor. +function tcStep2() +{ + EditorCore.toggleEditor(); + EditorCore.tabBook.selectPage(3); + + createPath(testRoot("shots/")); + + schedule(1500, 0, "tcStep3"); +} + +function tcStep3() +{ + GuiEditor.brain.setCurrentAddSet(GuiEditor.rootGui); + + // Dropped rather than added, so it arrives themed and with the page the drop + // seeds it with - the state a book is actually in when someone reaches for + // the "+". + %room = GuiEditor.brain.visiblePartOf(GuiEditor.rootGui); + $tcBook = new GuiTabBookCtrl() + { + Extent = "260 120"; + Position = (getWord(%room, 0) + 40) SPC (getWord(%room, 1) + 40); + }; + GuiEditor.brain.onControlDropped($tcBook, "50 50"); + + // After the drop's own schedule(40) has finished placing it. + schedule(300, 0, "tcStep4"); +} + +function tcStep4() +{ + // A drop selects what it dropped, and the edit control tests its sizing + // knobs before it hands the click to the control. Nothing selected means + // nothing between the cursor and the book. + GuiEditor.brain.clearSelection(); + + tcCheck("the book arrived with a page (" @ $tcBook.getCount() @ ")", $tcBook.getCount() == 1); + + %rect = $tcBook.getAddPageTabRect(); + tcCheck("and reports a \"+\" to aim at (" @ %rect @ ")", getWord(%rect, 2) > 0); + + %x = getWord(%rect, 0) + mFloor(getWord(%rect, 2) / 2); + %y = getWord(%rect, 1) + mFloor(getWord(%rect, 3) / 2); + + %file = new FileObject(); + %file.openForWrite(tcTargetFile()); + %file.writeLine(%x SPC %y); + %file.close(); + %file.delete(); + + echo("TABCLICK: + tab centre at " @ %x SPC %y); + + GuiEditor.undoRecorder.clear(); + + // The driver posts its click during this window. + schedule(8000, 0, "tcStep5"); +} + +function tcStep5() +{ + tcCheck("clicking the \"+\" added a page (" @ $tcBook.getCount() @ ")", $tcBook.getCount() == 2); + + if($tcBook.getCount() == 2) + { + %page = $tcBook.getObject(1); + tcCheck("made by the editor, not the raw C++ (" @ %page.getText() @ ")", + %page.getText() $= "Page 2"); + tcCheck("themed like any arrival (" @ %page.Profile.category @ ")", + %page.Profile.category $= "TabPage"); + tcCheck("and showing, as the active tab", %page.isVisible()); + } + + tcCheck("one undo step for the click (" @ GuiEditor.undoRecorder.undoCount() @ ")", + GuiEditor.undoRecorder.undoCount() == 1); + + // The click landed on the "+" and not on the book behind it, which would + // have selected the book instead of adding anything. + tcCheck("the click was not taken as a selection", + GuiEditor.brain.selectionList() !$= $tcBook); + + screenShot(testRoot("shots/tabBookClick.png"), "PNG"); + echo("TABCLICK DONE"); + schedule(400, 0, "quit"); +} diff --git a/tests/smoke/tabBookClick.input.ps1 b/tests/smoke/tabBookClick.input.ps1 new file mode 100644 index 000000000..84e782e70 --- /dev/null +++ b/tests/smoke/tabBookClick.input.ps1 @@ -0,0 +1,39 @@ +# Input for tabBookClick.cs. Posts a real click onto the tab book's "+" tab, so +# the hit test in GuiTabBookCtrl::onMouseDownEditor is exercised rather than +# stepped over. +# +# The point is not written here. The engine works out where the "+" actually +# landed - which depends on the theme's font, the profile's borders and the tab +# position - and leaves it in a file for this script to pick up. A hard-coded +# point that drifted off the "+" would report a missing page, which is precisely +# what a broken hit test reports, and the test would be lying either way. +param([IntPtr]$Hwnd) + +. "$PSScriptRoot\..\lib\input.ps1" + +$target = Join-Path $PSScriptRoot "..\..\shots\tabBookClickTarget.txt" + +# Anything left by an earlier run would be clicked before this run has even +# placed its book. +if (Test-Path $target) { Remove-Item $target -Force } + +# The book is dropped at about t=3.5s and read again at t=11.5s. +$deadline = (Get-Date).AddSeconds(20) +$point = $null +while ((Get-Date) -lt $deadline) { + if (Test-Path $target) { + $line = (Get-Content $target -TotalCount 1) + if ($line -and $line.Trim()) { $point = $line.Trim().Split(' '); break } + } + Start-Sleep -Milliseconds 250 +} + +if (-not $point) { + Write-Host " the engine never reported a + tab position" + return +} + +Remove-Item $target -Force + +Send-EngineClick -Hwnd $Hwnd -X ([int]$point[0]) -Y ([int]$point[1]) +Write-Host " clicked the + tab at ($($point[0]),$($point[1]))" From dc6138233d719c7b7ca64a8d2d59ae4492d491e9 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Fri, 31 Jul 2026 15:58:27 -0400 Subject: [PATCH 20/55] Gui Editor: a menu bar makes its own items GuiMenuItemCtrl is refused by the palette, and rightly -- a menu item means nothing outside a menu bar. But nothing else made one either, so a bar dropped into a Gui was a permanently empty strip: there was no way, anywhere in the editor, to put a single item in it. So the bar makes its own. A dropped bar arrives holding "Menu 1", draws a ghosted "+" after the last menu whose click adds another, and while a menu is selected drops a box under it with a row per command and a "+" at its foot. Both halves are here because of the nesting: a "+" only on the bar would let you make menus you could never fill. The dropdown the editor draws is NOT the one the game opens. The runtime dropdown is a full-canvas GuiMenuBGCtrl pushed at layer 99, and a dialog at that layer takes 100% of the mouse -- handleTouchDown breaks on the first non-pass-through consumer and mAllowEventPassThru defaults false, so GuiEditCtrl::onTouchDown is never reached while one is up. Reusing it would mean reimplementing selection, the "+" and close-on-outside-click on the dialog side, and fixing GuiMenuBGCtrl::positionMenu, which reads the bar's parent-relative mBounds.point as canvas-global and so already opens an authored bar's menu in the wrong place. The bar draws the edit-mode box itself instead, and the runtime path is not touched at all. That works because of renderChild: the clip in force during a control's onRender is its PARENT's content rect, not its own bounds, so a bar can already draw below itself and be clipped to the container it sits in -- which is exactly as far as an authored dropdown should reach. No dialog, no clip escape, and the editor keeps the mouse. Hit testing is the other half and does not come free. findHitControl reaches a control through pointInControl, which walks mBounds, so every click on the box fell through to whatever was behind the bar. The bar answers for the box's rectangle as well as its own. The box is drawn from scratch rather than delegated to GuiMenuListCtrl. That list is built lazily when the first child arrives, so a menu with nothing in it has none -- and a menu the "+" just made, needing its first command, is the case that matters most. Which menu is open is derived, never stored. onPreRender walks the editor's selection and up each control's parents; a selection that reaches a direct child of this bar opens that menu. So it follows the Explorer tree as readily as the canvas, a command stays visible while it is being edited, and the menu the "+" just made is open the moment the brain selects it. The item itself is made in script. One created while authoring has to be themed, recorded for undo and announced to the Explorer tree, and a control knows how to do none of that -- so C++ draws the affordance and hit-tests it and GuiEditorBrain::onAddMenuItem does the rest, as onAddTabPage does. Numbering is per parent, so each menu's commands count from 1. canBeChildOf answers true for a bar OR another item, unlike a tab page's one legal parent: moving a command between menus, or a menu between bars, are both ordinary things to want. The properties pane. A menu item calls SimObject::initPersistFields rather than GuiControl's, so it has no profile, no geometry, no tooltip and no sizing -- a header with a caption in it and nothing underneath. Its own fields sat in a "Menu Item" section below that emptiness, which never opened, so there was no way to set any of them. They are in the header now, in a GuiEditorMenuItemBlock, because there is nothing to scroll past to reach them. The block owns its caption box rather than borrowing the shared text block. That one is multi-line with wrap, extend, alignment and font rows attached, all of which a menu item hides, and a box three lines tall for a word like "File" says the wrong thing about what belongs there. It is also where the per-keystroke write lives: a top-level menu is exactly as wide as its caption -- one of the few controls whose content area is dictated by its text -- so setUpdate recalculates the strip and the bar reflows under the cursor as the caption is typed. The undo step is still written once, on commit, from the value stashed on the first keystroke. Toggle and Radio are two bool fields but one decision, so they are one chooser, and a separator is its fourth choice. 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. So picking Spacer writes the dash and typing a dash picks Spacer. The kinds are drawn now, too -- the square bullet, the round one, the submenu arrow, the same marks GuiMenuListCtrl::onRenderItem draws -- so what a kind looks like in the editor is what it will look like in the game. Nuts. A menu item's position and extent are ignored by everything below the top level, and a top-level item's position is ignored as well. Selecting one still put eight sizing knobs on screen, around the 64x64 default rectangle, with no visible relation to the row that was selected -- and dragging them resized something nobody would ever see. isGeometryEditable is a new GuiControl virtual, false for menu items and tab pages, and the editor gives them the plain outline it already draws for a locked control: no nuts, no resize, no move. The outline is the point. It circles the row you actually picked. The theme. A generated theme's MenuItem wore Padded on all four sides, which left a separator nothing to be. A separator is drawn as the menu item profile in its SELECTED state, and nothing else in a menu ever uses that state -- hover is Highlight, greyed is Disabled -- so the SL fields of a menu item's borders belong to separators alone, and Padded's were the same 10px inset as every other state. SelectedInset is a new named border: a 10px inset in three states, and in the selected state a 4px margin, a 1px rim in the theme's surface colour and no padding at all. A separator's height is nothing but that border's margin, rim and padding -- GuiMenuListCtrl::updateSize measures one with a zero-height interior -- so those numbers are a 2px rule with 4px of the menu's own fill above and below it. MenuItem wears it top and bottom with Padded on the sides, which keeps the label's inset and lets the rule run the width of the menu rather than being capped at each end by the same margin and rim. Alone among the recipes it ignores the theme's borderSize. This rim is a rule between two groups of commands, not the edge of a control: at borderSize 0 it would vanish and the menu would silently lose its grouping, and at 3 it would thicken into a band. Bugs that fell out. All pre-existing; all newly reachable because an empty or edited menu bar is now an ordinary thing rather than a curiosity. findHitControl was declared with two parameters where the base takes four, so it hid rather than overrode and its return this was dead code. The base recursed into the items and their sub-items, whose mBounds are either the GuiControl 64x64 default or a stale canvas-global rect stamped by GuiMenuListCtrl::onRenderItem -- so clicking a menu on an authored bar handed the editor the last sub-item of that menu. Radio was declared TypeS32 over a bool mRadio. The default getter read four bytes off a one-byte member and answered with whatever was beside it, so a plain command could read back as a radio item. That is what made the new Kind chooser show the wrong kind, and it has been wrong since the field was added. onChildAdded dropped a rejected child on the floor -- removeObject and return, leaving it registered with no group at all. Both overrides now resolve the destination before removing, and leave the child where it is if there is nowhere to send it. onChildRemoved repaired nothing: mPrevItem/mNextItem still pointed at the departed item and mHoverTarget/mOpenMenu still named it, and all four are dereferenced later. GuiMenuItemCtrl::mMenuBar was never initialised and onChildAdded dereferences it -- one add() before the item was in a bar away. childrenReordered was missing, so reordering items in the Explorer tree left the strip with stale rectangles. onKeyDown fell off the end returning junk, on the delegate-to-submenu branch and on any unmatched key. A warning on every build. Emptying a menu of its last child tried to deleteObject the scroller and the list, which are built with a bare new and never registered -- an assert, and a modal one, which in a headless run reads as a 90-second hang. Registering them instead moves them into the canvas's ownership and hangs the engine on the way down if a menu is open when it quits; that was measured against a stashed tree rather than reasoned about. They are kept and reused, which changes no lifetimes at all. And click-to-place did nothing for a menu bar. onControlDropped asks whether the cursor is over the canvas, which is a drag's question; a bar pins itself to its parent's origin, so a clicked payload sat at 0,0 and the test measured a point in the editor's chrome. placeControl is that path without the cursor test, and the tile's click uses it. Verified: two new suites, a shot harness and two unit tests. menuBar.cs, 78 checks -- the palette still refusing the class, a drop arriving with one themed "Menu 1" as a single undo step, onAddMenuItem at both levels with numbering per parent, the geometry the clicks depend on (including a menu with no children at all, which still offers its "+"), a bar emptied to nothing and refilled from its own "+", the four kinds and the dash in both directions, and the reparent rule through canBeChildOf, moveSelectionToCtrl and paste. menuBarClick.cs posts four REAL clicks: the bar's "+", the dropdown's "+", and then the editor's own File menu opened and closed again -- the last two because they are what prove the runtime path is untouched. The engine works out where each "+" actually landed, which depends on the theme's font and the profile's borders, and hands the coordinates to the driver through a file. A hard-coded point that drifted beside the "+" would report a missing item, which is exactly what a broken hit test reports. shots/menuBar.cs draws the bar in five states, two of which the runtime machinery cannot draw at all: a bar with no menus, and an open menu with nothing in it. inspectorPane.cs, 150 checks, gains the new block -- single-line caption, Visible and Active kept in the header's own toggle row -- and loses the expectation that the dead section exists. guiProfileThemeTests gains SelectedInset's values and MenuItem's wiring. Full suite green: 31 smoke suites, 13 shot harnesses, and the C++ unit tests. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qi2pZ9UzF5rsLCNVErpzwt --- editor/GuiEditor/GuiEditor.cs | 1 + editor/GuiEditor/scripts/GuiEditorBrain.cs | 112 +++ .../GuiEditor/scripts/GuiEditorChoiceRow.cs | 16 + .../GuiEditor/scripts/GuiEditorControlSpec.cs | 30 +- .../GuiEditor/scripts/GuiEditorControlTile.cs | 7 +- .../GuiEditor/scripts/GuiEditorHeaderBlock.cs | 45 +- .../scripts/GuiEditorInspectorPane.cs | 21 +- .../scripts/GuiEditorMenuItemBlock.cs | 315 ++++++++ engine/source/gui/containers/guiTabPageCtrl.h | 5 + engine/source/gui/editor/guiEditCtrl.cc | 45 +- engine/source/gui/editor/guiMenuBarCtrl.cc | 707 +++++++++++++++++- engine/source/gui/editor/guiMenuBarCtrl.h | 98 ++- .../gui/editor/guiMenuBarCtrl_ScriptBinding.h | 37 + engine/source/gui/guiControl.h | 12 + engine/source/gui/guiControl_ScriptBinding.h | 16 + engine/source/gui/guiProfileTheme.cc | 39 +- .../testing/tests/guiProfileThemeTests.cc | 78 +- tests/shots/menuBar.cs | 141 ++++ tests/smoke/inspectorPane.cs | 29 +- tests/smoke/menuBar.cs | 516 +++++++++++++ tests/smoke/menuBarClick.cs | 239 ++++++ tests/smoke/menuBarClick.input.ps1 | 50 ++ 22 files changed, 2516 insertions(+), 43 deletions(-) create mode 100644 editor/GuiEditor/scripts/GuiEditorMenuItemBlock.cs create mode 100644 tests/shots/menuBar.cs create mode 100644 tests/smoke/menuBar.cs create mode 100644 tests/smoke/menuBarClick.cs create mode 100644 tests/smoke/menuBarClick.input.ps1 diff --git a/editor/GuiEditor/GuiEditor.cs b/editor/GuiEditor/GuiEditor.cs index 2f50322d4..9a58c7415 100644 --- a/editor/GuiEditor/GuiEditor.cs +++ b/editor/GuiEditor/GuiEditor.cs @@ -58,6 +58,7 @@ exec("./scripts/GuiEditorChoiceRow.cs"); exec("./scripts/GuiEditorAnchorPicker.cs"); exec("./scripts/GuiEditorTextBlock.cs"); + exec("./scripts/GuiEditorMenuItemBlock.cs"); exec("./scripts/GuiEditorHeaderBlock.cs"); exec("./scripts/GuiEditorDynamicFields.cs"); exec("./scripts/GuiEditorInspectorPane.cs"); diff --git a/editor/GuiEditor/scripts/GuiEditorBrain.cs b/editor/GuiEditor/scripts/GuiEditorBrain.cs index 953c3b8c0..ea1f846b1 100644 --- a/editor/GuiEditor/scripts/GuiEditorBrain.cs +++ b/editor/GuiEditor/scripts/GuiEditorBrain.cs @@ -55,6 +55,22 @@ 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); @@ -181,6 +197,14 @@ %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); } @@ -319,6 +343,94 @@ 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); diff --git a/editor/GuiEditor/scripts/GuiEditorChoiceRow.cs b/editor/GuiEditor/scripts/GuiEditorChoiceRow.cs index 31ae18ce3..2f2c45d80 100644 --- a/editor/GuiEditor/scripts/GuiEditorChoiceRow.cs +++ b/editor/GuiEditor/scripts/GuiEditorChoiceRow.cs @@ -83,6 +83,22 @@ class = "GuiEditorToggleIcon"; } } +// 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 GuiEditorChoiceRow::setChoiceVisible(%this, %value, %visible) +{ + for(%i = 0; %i < %this.choiceCount; %i++) + { + if(%this.choiceValue[%i] $= %value) + { + %this.choiceButton[%i].setVisible(%visible); + return; + } + } +} + //----------------------------------------------------------------------------- // Value. //----------------------------------------------------------------------------- diff --git a/editor/GuiEditor/scripts/GuiEditorControlSpec.cs b/editor/GuiEditor/scripts/GuiEditorControlSpec.cs index 9d7464330..96ec3fd0a 100644 --- a/editor/GuiEditor/scripts/GuiEditorControlSpec.cs +++ b/editor/GuiEditor/scripts/GuiEditorControlSpec.cs @@ -198,9 +198,11 @@ %this.addSection("GuiMenuBarCtrl", "Scrollbar", "Scroll Bar", "constantThumbHeight showArrowButtons scrollBarThickness"); - // IsOn is the header's principal value, so the section carries only the two - // fields that decide how the item behaves when it is picked. - %this.addSection("GuiMenuItemCtrl", "Item", "Menu Item", "Toggle Radio"); + // 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"); @@ -274,7 +276,10 @@ %this.headerValues["GuiFrameSetCtrl"] = "DividerThickness"; %this.headerValues["GuiTabBookCtrl"] = "TabPosition"; %this.headerValues["GuiWindowCtrl"] = "titleHeight"; - %this.headerValues["GuiMenuItemCtrl"] = "IsOn"; + + // 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 @@ -395,6 +400,23 @@ 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) { diff --git a/editor/GuiEditor/scripts/GuiEditorControlTile.cs b/editor/GuiEditor/scripts/GuiEditorControlTile.cs index 787385226..8bbcd7610 100644 --- a/editor/GuiEditor/scripts/GuiEditorControlTile.cs +++ b/editor/GuiEditor/scripts/GuiEditorControlTile.cs @@ -248,5 +248,10 @@ // 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". - GuiEditor.brain.onControlDropped(%payload, %payload.Position); + // + // 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/GuiEditorHeaderBlock.cs b/editor/GuiEditor/scripts/GuiEditorHeaderBlock.cs index 71cd8ea5d..ac4c34dca 100644 --- a/editor/GuiEditor/scripts/GuiEditorHeaderBlock.cs +++ b/editor/GuiEditor/scripts/GuiEditorHeaderBlock.cs @@ -44,6 +44,7 @@ %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: @@ -355,9 +356,14 @@ class = "GuiEditorAnchorPicker"; // class with no profile at all cannot use it either. %this.categoryRow.setVisible(!%bare && %spec.categoryChoices(%class) !$= ""); %this.geometryGrid.setVisible(!%bare); - %this.visibleButton.setVisible(!%bare); - %this.activeButton.setVisible(!%bare); - %this.inputButton.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 @@ -370,10 +376,43 @@ class = "GuiEditorAnchorPicker"; // 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. diff --git a/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs b/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs index 4671ef85d..e6329130e 100644 --- a/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs +++ b/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs @@ -805,10 +805,17 @@ class = "GuiProfileEditorFieldRow"; } // A section with nothing left to show gets out of the way entirely. - %panels = getWordCount(%this.panelList); - for(%i = 0; %i < %panels; %i++) + // + // 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(%this.panelList, %i); + %key = getWord(%panels, %i); %this.panel[%key].setVisible(%this.anyRowVisible(%this.panelFields[%key])); } @@ -917,6 +924,14 @@ class = "GuiProfileEditorFieldRow"; %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); + } + %this.refreshToggles(); %this.header.anchorPicker.readEnums( %this.target.getFieldValue("HorizSizing"), diff --git a/editor/GuiEditor/scripts/GuiEditorMenuItemBlock.cs b/editor/GuiEditor/scripts/GuiEditorMenuItemBlock.cs new file mode 100644 index 000000000..6ed54aae6 --- /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 = "GuiEditorChoiceRow"; + 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::onProfileRowCommit(%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::onProfileRowReset(%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/engine/source/gui/containers/guiTabPageCtrl.h b/engine/source/gui/containers/guiTabPageCtrl.h index fc0c361c8..f4565108e 100644 --- a/engine/source/gui/containers/guiTabPageCtrl.h +++ b/engine/source/gui/containers/guiTabPageCtrl.h @@ -46,6 +46,11 @@ class GuiTabPageCtrl : public GuiControl /// other parent. See GuiControl::canBeChildOf. bool canBeChildOf(GuiControl* parent); + /// Its book forces it to the page rect on every layout pass, so there is + /// nothing for the editor's sizing handles to change. See + /// GuiControl::isGeometryEditable. + bool isGeometryEditable() { return false; }; + virtual void setText(const char *txt = NULL); ///< Override setText function to signal parent we need to update. void onRender(Point2I offset, const RectI &updateRect); void parentResized(const Point2I& oldParentExtent, const Point2I& newParentExtent); diff --git a/engine/source/gui/editor/guiEditCtrl.cc b/engine/source/gui/editor/guiEditCtrl.cc index efd5093b9..328464c22 100755 --- a/engine/source/gui/editor/guiEditCtrl.cc +++ b/engine/source/gui/editor/guiEditCtrl.cc @@ -438,6 +438,17 @@ S32 GuiEditCtrl::getSizingHitKnobs(const Point2I& pt, const RectI& box) return sizingNone; } +// Whether the editor must leave this control's position and extent alone. +// +// Two different reasons that come to the same thing: isLocked is the padlock the +// user turned on, and isGeometryEditable is the control saying its parent +// dictates its geometry - a tab page, a menu item - so that dragging it would +// change a number something else overwrites on the next layout pass. +static inline bool editGeometryFrozen(GuiControl* ctrl) +{ + return ctrl == NULL || ctrl->isLocked() || !ctrl->isGeometryEditable(); +} + void GuiEditCtrl::drawControlDecoration(GuiControl* ctrl, RectI& box, ColorI& outlineColor, ColorI& nutColor) { S32 lx = box.point.x, rx = box.point.x + box.extent.x - 1; @@ -464,8 +475,10 @@ void GuiEditCtrl::drawControlDecoration(GuiControl* ctrl, RectI& box, ColorI& ou dglDrawRect(box, outlineColor); } } - else if (ctrl->isLocked()) + else if (editGeometryFrozen(ctrl)) { + // An outline rather than eight handles, because there is nothing here to + // take hold of - see editGeometryFrozen. box.inset(-1, -1); dglDrawRect(box, strongestColor); box.inset(-1,-1); @@ -726,7 +739,10 @@ void GuiEditCtrl::getCursor(GuiCursor*& cursor, bool& showCursor, const GuiEvent Point2I mousePos = globalToLocalCoord(lastGuiEvent.mousePoint); // first see if we hit a sizing knob on the currently selected control... - if (mSelectedControls.size() == 1 && initCursors() == true) + // (a control whose geometry is not the editor's to change draws no knobs, so + // it must not promise a resize cursor over one either) + if (mSelectedControls.size() == 1 && initCursors() == true && + !editGeometryFrozen(mSelectedControls.first())) { ctrl = mSelectedControls.first(); cext = ctrl->getExtent(); @@ -791,7 +807,10 @@ void GuiEditCtrl::onTouchDown(const GuiEvent& event) mLastMousePos = globalToLocalCoord(event.mousePoint); // first see if we hit a sizing knob on the currently selected control... - if (mSelectedControls.size() == 1) + // (none are drawn for a control whose geometry is not the editor's to change, + // so none can be hit either - otherwise the gesture starts, fires onPreEdit + // and records an undo step for a resize that never happens) + if (mSelectedControls.size() == 1 && !editGeometryFrozen(mSelectedControls.first())) { ctrl = mSelectedControls.first(); cext = ctrl->getExtent(); @@ -1041,8 +1060,8 @@ void GuiEditCtrl::onTouchDragged(const GuiEvent& event) GuiControl* ctrl = mSelectedControls.first(); - // can't resize a locked control - if (ctrl && ctrl->isLocked()) + // can't resize a locked control, nor one whose parent owns its geometry + if (editGeometryFrozen(ctrl)) return; Point2I ctrlPoint = mCurrentAddSet->globalToLocalCoord(event.mousePoint); @@ -1114,8 +1133,8 @@ void GuiEditCtrl::onTouchDragged(const GuiEvent& event) for (; i != mSelectedControls.end(); i++) { - // skip locked controls - if ((*i)->isLocked()) + // skip controls the editor may not move + if (editGeometryFrozen(*i)) continue; if ((*i)->mBounds.point.x < minPos.x) @@ -1141,8 +1160,8 @@ void GuiEditCtrl::onTouchDragged(const GuiEvent& event) { for (S32 i = 0; i < mSelectedControls.size(); i++) { - // skip locked controls - if (mSelectedControls[i]->isLocked()) + // skip controls the editor may not move + if (editGeometryFrozen(mSelectedControls[i])) continue; Point2I snapBackPoint(mSelectedControls[i]->mBounds.point.x, mDragBeginPoints[i].y); @@ -1156,8 +1175,8 @@ void GuiEditCtrl::onTouchDragged(const GuiEvent& event) { for (S32 i = 0; i < mSelectedControls.size(); i++) { - // skip locked controls - if (mSelectedControls[i]->isLocked()) + // skip controls the editor may not move + if (editGeometryFrozen(mSelectedControls[i])) continue; Point2I snapBackPoint(mDragBeginPoints[i].x, mSelectedControls[i]->mBounds.point.y); @@ -1279,8 +1298,8 @@ void GuiEditCtrl::moveSelection(const Point2I& delta) Vector::iterator i; for (i = mSelectedControls.begin(); i != mSelectedControls.end(); i++) { - // skip locked controls - if ((*i)->isLocked()) + // skip controls the editor may not move + if (editGeometryFrozen(*i)) continue; (*i)->resize((*i)->mBounds.point + delta, (*i)->mBounds.extent); diff --git a/engine/source/gui/editor/guiMenuBarCtrl.cc b/engine/source/gui/editor/guiMenuBarCtrl.cc index 689dda487..792910907 100644 --- a/engine/source/gui/editor/guiMenuBarCtrl.cc +++ b/engine/source/gui/editor/guiMenuBarCtrl.cc @@ -29,6 +29,9 @@ #include "gui/editor/guiMenuBarCtrl.h" +// For the editor-only "+": the colour it borrows and the handle it calls back on. +#include "gui/editor/guiEditCtrl.h" + #include "gui/editor/guiMenuBarCtrl_ScriptBinding.h" #pragma region GuiMenuBarCtrl @@ -64,6 +67,16 @@ GuiMenuBarCtrl::GuiMenuBarCtrl() mHoverTarget = NULL; mOpenMenu = NULL; + // Empty until a layout pass in edit mode fills it in, and read by + // getAddItemGlobalRect - which script can ask about a bar that has never laid + // itself out at all. + mAddItemRect = RectI(0, 0, 0, 0); + + VECTOR_SET_ASSOCIATION(mEditRowRects); + mEditOpenMenu = NULL; + mEditBoxRect = RectI(0, 0, 0, 0); + mEditAddRowRect = RectI(0, 0, 0, 0); + mUseConstantHeightThumb = false; mScrollBarThickness = 12; mShowArrowButtons = false; @@ -113,8 +126,20 @@ void GuiMenuBarCtrl::onChildAdded(GuiControl *child) if (!menu) { Con::warnf("GuiMenuBarCtrl::onChildAdded - Only a GuiMenuItemCtrl can be added to a GuiMenuBarCtrl! Child will not be added."); - SimObject *simObj = reinterpret_cast(child); - removeObject(simObj); + + // Somewhere to put it, decided BEFORE taking it out of the bar. Removing + // it first and then finding nowhere for it - which is what this did - left + // the control registered with no group at all, so it was neither in the + // document nor deleted. A bar with no parent is the case that reaches it. + GuiControl *destination = getParent(); + if (destination == NULL) + { + Con::warnf("GuiMenuBarCtrl::onChildAdded - no parent to place it on; leaving it where it is"); + return; + } + + removeObject(child); + destination->addObject(child); return; } menu->mMenuBar = this; @@ -132,11 +157,53 @@ void GuiMenuBarCtrl::onChildAdded(GuiControl *child) void GuiMenuBarCtrl::onChildRemoved(SimObject *child) { + // Everything the bar still remembers about the departing item. None of this + // mattered while a menu bar was something you built once in script and never + // edited; deleting an item is an ordinary action now, and each of these is a + // pointer something dereferences later - onKeyDown reads mOpenMenu, + // setHoverTarget reads mHoverTarget, and the key walk follows the sibling + // links. + GuiMenuItemCtrl *item = dynamic_cast(child); + if (item != NULL) + { + if (item->mPrevItem != NULL) + { + item->mPrevItem->mNextItem = item->mNextItem; + } + if (item->mNextItem != NULL) + { + item->mNextItem->mPrevItem = item->mPrevItem; + } + item->mPrevItem = NULL; + item->mNextItem = NULL; + + if (mHoverTarget == item) + { + mHoverTarget = NULL; + } + if (mOpenMenu == item) + { + mOpenMenu = NULL; + } + if (mEditOpenMenu == item) + { + mEditOpenMenu = NULL; + mEditBoxRect.set(Point2I(0, 0), Point2I(0, 0)); + mEditRowRects.clear(); + mEditAddRowRect.set(Point2I(0, 0), Point2I(0, 0)); + } + } + calculateMenus(); } void GuiMenuBarCtrl::calculateMenus() { + // Ahead of everything else: a bar that leaves edit mode must not be left + // holding a "+" that is no longer drawn, or getAddItemRect reports a + // rectangle nothing will answer a click in. + mAddItemRect.set(Point2I(0, 0), Point2I(0, 0)); + RectI innerRect = getInnerRect(); iterator i; S32 length = 0; @@ -152,13 +219,488 @@ void GuiMenuBarCtrl::calculateMenus() length += ctrl->getExtent().x; } } + + // The "+" follows the last menu, square on the strip's height so it reads as + // an affordance rather than a menu with a one-character name. Nothing to wrap + // and nothing to run out of: the bar is a single row, and onRender keeps it + // as wide as the clip rect it is drawn into. + if (isEditMode()) + { + mAddItemRect.set(Point2I(length, 0), Point2I(innerRect.extent.y, innerRect.extent.y)); + } +} + +Point2I GuiMenuBarCtrl::getMenuLocalCoord(const Point2I &src) +{ + Point2I offset = Point2I(0, 0); + RectI contentRect = getInnerRect(offset); + + return src - contentRect.point; +} + +RectI GuiMenuBarCtrl::getAddItemGlobalRect() +{ + // isEditMode as well as the rectangle, because closing the editor does not + // re-run the layout: the bar would go on reporting the "+" it had until + // something else happened to resize it. + if (!mAddItemRect.isValidRect() || !isEditMode()) + { + return RectI(0, 0, 0, 0); + } + + Point2I offset = Point2I(0, 0); + RectI contentRect = getInnerRect(offset); + + return RectI(localToGlobalCoord(contentRect.point) + mAddItemRect.point, mAddItemRect.extent); +} + +GuiMenuItemCtrl* GuiMenuBarCtrl::findSelectedMenu() +{ + GuiEditCtrl* edit = GuiControl::smEditorHandle; + if (edit == NULL || !isEditMode()) + return NULL; + + // Walk up from each selected control. Whichever one reaches a direct child of + // this bar names the menu being worked in - so selecting a command keeps its + // menu open, and selecting the bar or anything else closes it. + SimSet& selected = edit->getSelectedSet(); + for (SimSet::iterator i = selected.begin(); i != selected.end(); i++) + { + GuiControl* ctrl = dynamic_cast(*i); + while (ctrl != NULL) + { + if (ctrl->getParent() == this) + { + return dynamic_cast(ctrl); + } + ctrl = ctrl->getParent(); + } + } + + return NULL; +} + +void GuiMenuBarCtrl::refreshEditMenu() +{ + GuiMenuItemCtrl* open = findSelectedMenu(); + if (open != mEditOpenMenu) + { + mEditOpenMenu = open; + setUpdate(); + } + + // Re-laid every time rather than only on a change, so renaming a command in + // the properties pane widens the box straight away. + layoutEditMenu(); +} + +void GuiMenuBarCtrl::onPreRender() +{ + refreshEditMenu(); + + Parent::onPreRender(); +} + +void GuiMenuBarCtrl::layoutEditMenu() +{ + mEditBoxRect.set(Point2I(0, 0), Point2I(0, 0)); + mEditRowRects.clear(); + mEditAddRowRect.set(Point2I(0, 0), Point2I(0, 0)); + + if (mEditOpenMenu == NULL || mMenuItemProfile == NULL || mMenuContentProfile == NULL) + return; + + GFont* font = mMenuItemProfile->getFont(mFontSizeAdjust); + if (font == NULL) + return; + + Point2I rowInner = Point2I(0, font->getHeight()); + const S32 rowHeight = getOuterExtent(rowInner, NormalState, mMenuItemProfile).y; + if (rowHeight <= 0) + return; + + // Wide enough for the longest label, plus the two gutters the marks live in - + // a bullet on the left, a submenu arrow on the right, each a row tall. Adding + // them rather than taking them out of the middle is what the runtime list + // does, and without it every label is truncated to make room for marks most + // of the rows do not even have. + S32 widest = 0; + for (iterator i = mEditOpenMenu->begin(); i != mEditOpenMenu->end(); i++) + { + GuiMenuItemCtrl* child = dynamic_cast(*i); + if (child == NULL) + continue; + + Point2I textInner = Point2I(font->getStrWidth((const UTF8*)child->getText()), 0); + widest = getMax(widest, getOuterExtent(textInner, NormalState, mMenuItemProfile).x); + } + widest += 2 * rowHeight; + + // And never narrower than the menu it hangs from, so it reads as belonging to + // that menu rather than floating under it. + widest = getMax(widest, mEditOpenMenu->getExtent().x); + + // A separator is the profile's chrome with no line of text in it, which is + // what makes it read as a rule between two groups rather than a blank row. + // + // In the SELECTED state, which is not a detail. Nothing else in a menu uses + // that state - hover is Highlight, greyed is Disabled - so a theme shapes its + // separators entirely through the SL fields of the three border profiles, and + // measuring in any other state prices the row from padding the separator does + // not use. GuiMenuListCtrl::updateSize measures the real thing the same way. + Point2I emptyInner = Point2I(0, 0); + const S32 spacerHeight = getOuterExtent(emptyInner, SelectedState, mMenuItemProfile).y; + + // One row per command plus the "+" row - which is why an empty menu still + // gets a box. A menu with no children has no GuiMenuListCtrl at all (it is + // built lazily when the first child arrives), so the runtime machinery could + // not draw this case even if it were open, and it is the case that matters: + // a menu the "+" just made has nothing in it yet. + S32 rowsHeight = rowHeight; + for (iterator i = mEditOpenMenu->begin(); i != mEditOpenMenu->end(); i++) + { + rowsHeight += editRowHeight(dynamic_cast(*i), rowHeight, spacerHeight); + } + + Point2I boxInner = Point2I(widest, rowsHeight); + Point2I boxOuter = getOuterExtent(boxInner, NormalState, mMenuContentProfile); + + Point2I boxPoint = Point2I(mEditOpenMenu->mBounds.point.x, + mEditOpenMenu->mBounds.point.y + mEditOpenMenu->mBounds.extent.y); + mEditBoxRect.set(boxPoint, boxOuter); + + // Rows start at the box's content, not its corner. + Point2I boxOrigin = boxPoint; + RectI boxContent = getInnerRect(boxOrigin, boxOuter, NormalState, mMenuContentProfile); + + // What localToGlobalCoord will add on the way up from a command: the strip's + // content offset, and the open menu's own contribution. Measured rather than + // assumed - the inset renderChild stamps on a menu is not reliably there when + // script asks, and being one border out is exactly the kind of wrong that + // looks like nothing at all until you see the outline beside the row. + Point2I barOrigin = Point2I(0, 0); + Point2I barContentInset = getInnerRect(barOrigin).point; + Point2I fromMenu = mEditOpenMenu->mBounds.point + mEditOpenMenu->mRenderInsetLT; + + S32 y = boxContent.point.y; + for (iterator i = mEditOpenMenu->begin(); i != mEditOpenMenu->end(); i++) + { + S32 h = editRowHeight(dynamic_cast(*i), rowHeight, spacerHeight); + RectI rowRect = RectI(boxContent.point.x, y, widest, h); + mEditRowRects.push_back(rowRect); + + // Give the command the rectangle it is being drawn in, so that everything + // which asks a control where it is gets the answer the user can see. + // + // It is never rendered as a child, so nothing else maintains its bounds: + // they are either the 64x64 a GuiControl is constructed with, or a + // screen-space rectangle the RUNTIME dropdown stamped on it the last time + // one was opened. The Gui Editor draws its selection from + // localToGlobalCoord, so either of those puts the outline somewhere with + // no visible relationship to the row that was clicked. + // + // localToGlobalCoord walks mBounds.point + mRenderInsetLT and then every + // ancestor's, so the bounds it needs are the row less everything the walk + // is going to add for it. This control contributes none of its own. + GuiMenuItemCtrl* command = dynamic_cast(*i); + if (command != NULL) + { + command->mRenderInsetLT = Point2I(0, 0); + command->mRenderInsetRB = Point2I(0, 0); + command->mBounds.set(rowRect.point + barContentInset - fromMenu, rowRect.extent); + } + + y += h; + } + + mEditAddRowRect.set(Point2I(boxContent.point.x, y), Point2I(widest, rowHeight)); +} + +S32 GuiMenuBarCtrl::editRowHeight(GuiMenuItemCtrl* item, S32 rowHeight, S32 spacerHeight) +{ + if (item != NULL && item->mDisplayType == GuiMenuItemCtrl::DisplayType::Spacer) + { + return spacerHeight; + } + return rowHeight; +} + +void GuiMenuBarCtrl::renderEditMenu(const Point2I &contentOffset) +{ + if (!mEditBoxRect.isValidRect()) + return; + + // The clip in force here is this control's PARENT's content rect, not the + // bar's own bounds (GuiControl::renderChild), so the box is free to hang + // below the strip and is clipped to the container the bar sits in - which is + // exactly as far as it should reach. + RectI box = mEditBoxRect; + box.point += contentOffset; + renderUniversalRect(box, mMenuContentProfile, NormalState); + + GuiCanvas* root = getRoot(); + Point2I cursor = (root != NULL) ? root->getCursorPos() : Point2I(-1, -1); + + S32 row = 0; + for (iterator i = mEditOpenMenu->begin(); i != mEditOpenMenu->end() && row < mEditRowRects.size(); i++, row++) + { + GuiMenuItemCtrl* child = dynamic_cast(*i); + if (child == NULL) + continue; + + RectI rowRect = mEditRowRects[row]; + rowRect.point += contentOffset; + + // A separator draws as the runtime draws one - the profile in its SELECTED + // state, no text and no marks - so a rule looks here like it will look in + // the game. + if (child->mDisplayType == GuiMenuItemCtrl::DisplayType::Spacer) + { + RectI spacerRect = applyMargins(rowRect.point, rowRect.extent, SelectedState, mMenuItemProfile); + if (spacerRect.isValidRect()) + { + renderUniversalRect(spacerRect, mMenuItemProfile, SelectedState); + } + continue; + } + + GuiControlState state = child->isActive() ? NormalState : DisabledState; + if (rowRect.pointInRect(cursor)) + state = HighlightState; + + // Margins first, as GuiMenuListCtrl::onRenderItem does. A theme that puts + // a margin on a menu item - which is how a separator is given the room + // that makes its two border lines read as a groove rather than as the + // edges of a band - would otherwise draw differently here than in the game. + RectI ctrlRect = applyMargins(rowRect.point, rowRect.extent, state, mMenuItemProfile); + if (!ctrlRect.isValidRect()) + continue; + + renderUniversalRect(ctrlRect, mMenuItemProfile, state); + + // The same marks GuiMenuListCtrl::onRenderItem draws, so what a kind + // looks like is what it will look like: a square bullet for a toggle, a + // round one for a radio item, lit when it starts on, and an arrow on the + // right for an item that opens a submenu of its own. + RectI leftIcon = RectI(rowRect.point, Point2I(rowRect.extent.y, rowRect.extent.y)); + if (child->mDisplayType == GuiMenuItemCtrl::DisplayType::Toggle || + child->mDisplayType == GuiMenuItemCtrl::DisplayType::Radio) + { + ColorI markColor = child->mIsOn ? mMenuItemProfile->getFillColor(HighlightState) + : mMenuItemProfile->getFillColor(NormalState); + S32 fontHeight = mMenuItemProfile->getFont(mFontSizeAdjust)->getHeight(); + renderColorBullet(leftIcon, markColor, getMin(fontHeight, 16), + child->mDisplayType == GuiMenuItemCtrl::DisplayType::Radio); + } + + if (child->mDisplayType == GuiMenuItemCtrl::DisplayType::Menu) + { + RectI rightIcon = RectI( + Point2I(rowRect.point.x + rowRect.extent.x - rowRect.extent.y, rowRect.point.y), + leftIcon.extent); + rightIcon.inset(2, 0); + ColorI arrowColor = ColorI(getFontColor(mMenuItemProfile, state)); + renderTriangleIcon(rightIcon, arrowColor, GuiDirection::Right, + mMenuItemProfile->getFont(mFontSizeAdjust)->getHeight() / 2); + } + + // Indented past the mark column, the way the runtime rows are, so a menu + // of mixed kinds still reads as one column of labels. + dglSetBitmapModulation(getFontColor(mMenuItemProfile, state)); + RectI textRect = RectI(rowRect.point.x + rowRect.extent.y, rowRect.point.y, + rowRect.extent.x - (2 * rowRect.extent.y), rowRect.extent.y); + renderText(textRect.point, textRect.extent, child->getText(), mMenuItemProfile); + } + + if (mEditAddRowRect.isValidRect()) + { + RectI addRow = mEditAddRowRect; + addRow.point += contentOffset; + renderAddItem(addRow); + } +} + +RectI GuiMenuBarCtrl::getAddSubItemGlobalRect() +{ + // Worked out on demand rather than read back from the last frame. Which menu + // is open follows the selection, and the selection changes without anything + // being drawn - script selects a menu and asks about it in the same breath. + refreshEditMenu(); + + if (!mEditAddRowRect.isValidRect() || !isEditMode()) + { + return RectI(0, 0, 0, 0); + } + + Point2I offset = Point2I(0, 0); + RectI contentRect = getInnerRect(offset); + + return RectI(localToGlobalCoord(contentRect.point) + mEditAddRowRect.point, mEditAddRowRect.extent); +} + +GuiMenuItemCtrl* GuiMenuBarCtrl::findMenuAt(const Point2I &menuLocalPt) +{ + // Deliberately not findHitMenu: that one refuses an inactive menu, and a + // greyed-out menu is exactly the kind you open the editor to fix. Only + // visibility is honoured, because calculateMenus does not place a hidden item + // and its rectangle is therefore whatever it was last time. + iterator i; + for (i = begin(); i != end(); i++) + { + GuiMenuItemCtrl *ctrl = dynamic_cast(*i); + if (ctrl != NULL && ctrl->isVisible() && ctrl->mBounds.pointInRect(menuLocalPt)) + { + return ctrl; + } + } + return NULL; +} + +void GuiMenuBarCtrl::renderAddItem(RectI itemRect) +{ + GuiEditCtrl* edit = GuiControl::smEditorHandle; + if (edit == NULL) + return; + + // Ghosted, so it never passes for a menu. Brighter under the cursor, so it + // reads as something to click rather than the empty tail of the bar - the + // same treatment the tab book's "+" tab gets. + ColorI fill = edit->getEditorColor(); + fill.alpha = 100; + + GuiCanvas* root = getRoot(); + if (root != NULL && itemRect.pointInRect(root->getCursorPos())) + fill.alpha = 200; + + dglDrawRectFill(itemRect, fill); + + dglSetBitmapModulation(getFontColor(edit->mProfile, NormalState)); + F32 tempAdjust = mFontSizeAdjust; + mFontSizeAdjust = 1.5f; + renderText(itemRect.point, itemRect.extent, "+", edit->mProfile); + mFontSizeAdjust = tempAdjust; +} + +void GuiMenuBarCtrl::requestNewMenuItem(GuiMenuItemCtrl *parent) +{ + // The GuiEditCtrl wears the GuiEditorBrain namespace, so this arrives at + // GuiEditorBrain::onAddMenuItem. A bar being edited by anything with no + // handler for it simply gets no item, which is the right way for this to + // fail. + GuiEditCtrl* edit = GuiControl::smEditorHandle; + if (edit != NULL && edit->isMethod("onAddMenuItem")) + { + Con::executef(edit, 3, "onAddMenuItem", getIdString(), + (parent != NULL) ? parent->getIdString() : ""); + } +} + +bool GuiMenuBarCtrl::pointInControl(const Point2I& parentCoordPoint) +{ + if (Parent::pointInControl(parentCoordPoint)) + return true; + + // The dropdown drawn while authoring hangs BELOW the strip, outside the bar's + // own bounds - and both findHitControl and the canvas walk bounds, so without + // this nobody ever offers the bar a click on its own "+" row. Empty outside + // edit mode, so this says nothing about a bar in a running game. + if (mEditBoxRect.isValidRect()) + { + Point2I origin = Point2I(0, 0); + RectI contentRect = getInnerRect(origin); + + RectI box = mEditBoxRect; + box.point += mBounds.point + contentRect.point; + + if (box.pointInRect(parentCoordPoint)) + return true; + } + + return false; +} + +bool GuiMenuBarCtrl::onMouseDownEditor(const GuiEvent &event, const Point2I& offset) +{ + Point2I menuLocalMouse = getMenuLocalCoord(globalToLocalCoord(event.mousePoint)); + GuiEditCtrl* edit = GuiControl::smEditorHandle; + + if (mAddItemRect.isValidRect() && mAddItemRect.pointInRect(menuLocalMouse)) + { + requestNewMenuItem(NULL); + + // Nothing else happens on this click. Selection follows the item the + // editor is about to make. + return true; + } + + // The open dropdown, which is drawn over whatever is beneath it and so gets + // asked before it. + if (mEditAddRowRect.isValidRect() && mEditAddRowRect.pointInRect(menuLocalMouse)) + { + requestNewMenuItem(mEditOpenMenu); + return true; + } + + if (mEditOpenMenu != NULL && edit != NULL) + { + for (S32 row = 0; row < mEditRowRects.size(); row++) + { + if (mEditRowRects[row].pointInRect(menuLocalMouse) && row < mEditOpenMenu->size()) + { + edit->select(dynamic_cast((*mEditOpenMenu)[row])); + return true; + } + } + } + + // The bar is opaque to findHitControl, so nothing else is going to offer the + // menu itself as the thing that was clicked. + GuiMenuItemCtrl *item = findMenuAt(menuLocalMouse); + if (item != NULL && edit != NULL) + { + edit->select(item); + return true; + } + + return Parent::onMouseDownEditor(event, offset); } -GuiControl* GuiMenuBarCtrl::findHitControl(const Point2I &pt, S32 initialLayer) +// The bar is opaque to hit testing: it places its items itself and answers for +// them by hand, through findHitMenu. +// +// This used to take two parameters where GuiControl's takes four, so it hid the +// base rather than overriding it and never ran - every caller reaches this +// through a GuiControl*. The base then descended into the items, and on into +// THEIR children, whose mBounds are either the default 64x64 or a canvas-global +// rectangle GuiMenuListCtrl stamped on them the last time a dropdown was drawn. +// Clicking a menu on an authored bar therefore handed the Gui Editor the last +// command inside it. +GuiControl* GuiMenuBarCtrl::findHitControl(const Point2I &pt, S32 initialLayer, const bool ignoreUseInput, const bool ignoreEditSelected) { return this; } +void GuiMenuBarCtrl::setUpdate() +{ + Parent::setUpdate(); + + // A top-level menu is exactly as wide as its caption, so the strip's layout + // is a function of the text and has to be redone whenever the text might + // have moved. The properties pane writes a caption on every keystroke, and + // this is what makes the bar reflow under the cursor as it is typed. + calculateMenus(); +} + +void GuiMenuBarCtrl::childrenReordered() +{ + // The strip is packed left to right in child order, and nothing else re-runs + // it on a reorder - so dragging an item in the Gui Editor's tree would leave + // every item wearing the rectangle of whichever item used to be there. + calculateMenus(); + + Parent::childrenReordered(); +} + GuiMenuItemCtrl* GuiMenuBarCtrl::findHitMenu(const Point2I &pt) { iterator i; @@ -259,6 +801,20 @@ void GuiMenuBarCtrl::onRender(Point2I offset, const RectI &updateRect) { //Render the childen renderChildControls(offset, contentRect, updateRect); + + // After the menus, so it always reads as the end of the strip. Empty + // unless calculateMenus found itself in edit mode, which is the whole + // test - and on a bar with no menus at all it is the only way to get one. + if (mAddItemRect.isValidRect()) + { + RectI addBounds = mAddItemRect; + addBounds.point += contentRect.point; + renderAddItem(addBounds); + } + + // Last, so the open menu's box covers the strip's own "+" if the two ever + // overlap. + renderEditMenu(contentRect.point); } } @@ -617,6 +1173,10 @@ GuiMenuItemCtrl::GuiMenuItemCtrl() mNextItem = NULL; mOpenSubMenu = NULL; + // Only ever assigned once this item is inside a bar, or inside an item that + // is - and onChildAdded reads it before either has necessarily happened. + mMenuBar = NULL; + mScroll = NULL; mList = NULL; } @@ -634,7 +1194,11 @@ void GuiMenuItemCtrl::initPersistFields() addField("Visible", TypeBool, Offset(mVisible, GuiMenuItemCtrl)); addProtectedField("IsOn", TypeBool, Offset(mIsOn, GuiMenuItemCtrl), &defaultProtectedSetFn, &defaultProtectedGetFn, &writeIsOn, ""); addProtectedField("Toggle", TypeBool, Offset(mToggle, GuiMenuItemCtrl), &setToggle, &defaultProtectedGetFn, &writeToggle, ""); - addProtectedField("Radio", TypeS32, Offset(mRadio, GuiMenuItemCtrl), &setRadio, &defaultProtectedGetFn, &writeRadio, ""); + // TypeBool, not TypeS32: mRadio is a bool, and setRadio has always written it + // with dAtob. Declared as an S32 the default getter read four bytes off a + // one-byte member and answered with whatever was next to it, so a plain + // command could read back as a radio item. + addProtectedField("Radio", TypeBool, Offset(mRadio, GuiMenuItemCtrl), &setRadio, &defaultProtectedGetFn, &writeRadio, ""); endGroup("MenuItem"); addGroup("Localization"); @@ -684,16 +1248,36 @@ void GuiMenuItemCtrl::onChildAdded(GuiControl *child) if (!subMenu) { Con::warnf("GuiMenuItemCtrl::onChildAdded - Only a GuiMenuItemCtrl can be added to a GuiMenuItemCtrl! Child will not be added."); - SimObject *simObj = reinterpret_cast(child); - removeObject(simObj); + + // As in GuiMenuBarCtrl::onChildAdded above: find the destination first, so + // a refusal cannot orphan the control it refused. + GuiControl *destination = getParent(); + if (destination == NULL) + { + Con::warnf("GuiMenuItemCtrl::onChildAdded - no parent to place it on; leaving it where it is"); + return; + } + + removeObject(child); + destination->addObject(child); return; } subMenu->mMenuBar = this->mMenuBar; - subMenu->setControlProfile(mMenuBar->mMenuItemProfile); + + // mMenuBar is NULL until this item is itself inside a bar, and an item built + // in script can be given children before it is added to one. + if (mMenuBar != NULL) + { + subMenu->setControlProfile(mMenuBar->mMenuItemProfile); + } if (dStrcmp(subMenu->getText(), "-") == 0) { + // The text is left as "-" rather than cleared. It is the only record that + // this is a separator - there is no field for it - so clearing it meant a + // separator did not survive being saved and read back: the reloaded item + // had no dash left to recognise it by. Keeping it also lets the properties + // pane show what it is, and lets someone type their way back out of it. subMenu->mDisplayType = Spacer; - subMenu->setText(StringTable->EmptyString); } else if (subMenu->mToggle) { @@ -740,21 +1324,112 @@ void GuiMenuItemCtrl::onChildAdded(GuiControl *child) void GuiMenuItemCtrl::onChildRemoved(SimObject *child) { + // The same repair GuiMenuBarCtrl::onChildRemoved makes, for a submenu's own + // children: the sibling links the keyboard walk follows, and the two pointers + // that would otherwise name a control this item no longer holds. + GuiMenuItemCtrl *item = dynamic_cast(child); + if (item != NULL) + { + if (item->mPrevItem != NULL) + { + item->mPrevItem->mNextItem = item->mNextItem; + } + if (item->mNextItem != NULL) + { + item->mNextItem->mPrevItem = item->mPrevItem; + } + item->mPrevItem = NULL; + item->mNextItem = NULL; + + if (mOpenSubMenu == item) + { + mOpenSubMenu = NULL; + } + if (mList != NULL && mList->mHoveredItem == item) + { + mList->mHoveredItem = NULL; + } + } + if (size() <= 0) { if (mDisplayType == Menu) { + // Back to a plain command, which is what an item that never had + // children is - so an emptied menu draws the same as a fresh one. mDisplayType = TextCommand; - mList->deleteObject(); - mList = NULL; - mScroll->deleteObject(); - mScroll = NULL; + + // The scroller and the list are KEPT, not freed. They are built with a + // bare new and never registered - as is every hidden helper control in + // this file - so deleteObject asserts on them ("Object not + // registered"), and registering them instead moves them into the + // canvas's ownership and hangs the engine on the way down if a menu is + // open when it quits. onChildAdded above reuses them if a child ever + // comes back, which in the Gui Editor it routinely does. + // + // Nothing reached any of this until a menu could be emptied one + // command at a time while authoring. + if (mIsOpen && mMenuBar != NULL) + { + closeMenu(); + } } } checkForGoodChildren(); } +// A menu item is a row in a bar or a row in a menu and nothing else: it exposes +// none of GuiControl's geometry (initPersistFields calls SimObject's), its +// rectangle is dictated by whatever is laying it out, and anywhere else it is a +// control nothing will ever draw a row for. So it refuses every other parent and +// the Gui Editor leaves it where it is. +// +// Two legal kinds of parent rather than the one a tab page has - moving a +// command from one menu to another, or a menu from one bar to another, are both +// ordinary things to want. +bool GuiMenuItemCtrl::canBeChildOf(GuiControl* parent) +{ + return dynamic_cast(parent) != NULL || + dynamic_cast(parent) != NULL; +} + +void GuiMenuItemCtrl::setText(const char *txt) +{ + Parent::setText(txt); + + // A single dash is how the documentation says you write a separator, and the + // text is the only record of one. Derived here rather than only when the item + // is added, so typing a dash into the properties pane turns the row into a + // separator as it is typed - and typing over the dash turns it back. + // + // Only inside a menu: a separator on the bar itself would be a gap in the + // strip with nothing either side of it to separate. And never over a Menu, + // which is what having children makes an item. + if (mDisplayType != Menu && dynamic_cast(getParent()) != NULL) + { + if (dStrcmp(getText(), "-") == 0) + { + mDisplayType = Spacer; + } + else if (mDisplayType == Spacer) + { + mDisplayType = mToggle ? Toggle : (mRadio ? Radio : TextCommand); + } + } + + // Whoever is laying this item out sized it from the text that just changed. + // For a top-level menu that is the bar, whose setUpdate re-runs + // calculateMenus; for a command inside a menu the parent is another item, + // whose base setUpdate does nothing - and it does not need to, because the + // authored dropdown is re-measured every frame in refreshEditMenu. + GuiControl *parent = getParent(); + if (parent != NULL) + { + parent->setUpdate(); + } +} + void GuiMenuItemCtrl::checkForGoodChildren() { if (mDisplayType != Menu) @@ -909,7 +1584,7 @@ bool GuiMenuItemCtrl::onKeyDown(const GuiEvent &event) { if (mOpenSubMenu != NULL && (mOpenSubMenu->mList->mHoveredItem != NULL || mOpenSubMenu->mOpenSubMenu != NULL)) { - mOpenSubMenu->onKeyDown(event); + return mOpenSubMenu->onKeyDown(event); } else if (event.keyCode == KEY_DOWN && event.modifier == 0) { @@ -1019,6 +1694,12 @@ bool GuiMenuItemCtrl::onKeyDown(const GuiEvent &event) } return true; } + + // Any other key belongs to whoever asked next - matching GuiMenuBarCtrl's own + // onKeyDown. Falling off the end here returned whatever happened to be in the + // return register, for every ordinary letter and for any of the five keys + // above arriving with a modifier. + return false; } void GuiMenuItemCtrl::setMenuActive(StringTableEntry name, bool isActive) diff --git a/engine/source/gui/editor/guiMenuBarCtrl.h b/engine/source/gui/editor/guiMenuBarCtrl.h index d4aec4ec5..a88d99bf1 100644 --- a/engine/source/gui/editor/guiMenuBarCtrl.h +++ b/engine/source/gui/editor/guiMenuBarCtrl.h @@ -56,11 +56,94 @@ class GuiMenuBarCtrl : public GuiControl virtual void inspectPostApply(); virtual void onChildAdded(GuiControl *child); virtual void onChildRemoved(SimObject *child); + virtual void childrenReordered(); + /// Re-runs calculateMenus, because a top-level menu is as wide as its text + /// and so anything that changes the text changes the layout. + virtual void setUpdate(); virtual void calculateMenus(); - virtual GuiControl* findHitControl(const Point2I &pt, S32 initialLayer); + /// GuiControl's signature, so this overrides rather than hides it. The bar + /// answers for its own items; nothing outside may descend into them. + virtual GuiControl* findHitControl(const Point2I &pt, S32 initialLayer = -1, const bool ignoreUseInput = false, const bool ignoreEditSelected = true); virtual GuiMenuItemCtrl* findHitMenu(const Point2I &pt); virtual void onRender(Point2I offset, const RectI &updateRect); + /// @name Authoring + /// + /// A GuiMenuItemCtrl is not something the control palette offers - it means + /// nothing outside a bar - so the bar makes its own, from a "+" it draws after + /// the last menu while the Gui is being authored. + /// @{ + + /// The editor-only "+", in the same coordinates as an item's mBounds: local to + /// the bar's CONTENT, which is the space calculateMenus lays items out in. + /// Empty whenever the bar is not being authored, which is how everything + /// tests for it. + RectI mAddItemRect; + + /// The menu whose dropdown is showing while authoring, and the rectangles of + /// what is in it - one row per command, then the "+" row, inside a box. All + /// in the same content-local space as mAddItemRect. + /// + /// Drawn by the bar rather than opened for real: the runtime dropdown is a + /// full-canvas dialog pushed at layer 99, and a dialog at that layer takes + /// every click, so the Gui Editor would never see one. Drawing it here costs + /// nothing at runtime and leaves the editor holding the mouse. + GuiMenuItemCtrl *mEditOpenMenu; + RectI mEditBoxRect; + Vector mEditRowRects; + RectI mEditAddRowRect; + + /// The menu the selection is in - that item, or anything inside it. Derived + /// rather than toggled, so the dropdown follows the Explorer tree as readily + /// as the canvas and cannot disagree with what is selected. + GuiMenuItemCtrl* findSelectedMenu(); + /// Re-derive which menu is open and re-lay its rows. Called every frame from + /// onPreRender, and on demand by the geometry accessors - the selection can + /// change without anything drawing. + void refreshEditMenu(); + void layoutEditMenu(); + /// A separator is a rule between two groups, so it gets the profile's chrome + /// and none of the height a line of text would need. + S32 editRowHeight(GuiMenuItemCtrl* item, S32 rowHeight, S32 spacerHeight); + void renderEditMenu(const Point2I &contentOffset); + + /// The dropdown's "+" row in global coordinates, or an empty rect. + RectI getAddSubItemGlobalRect(); + + virtual void onPreRender(); + + /// Control-local to content-local. findHitMenu reaches the same space through + /// a child's mRenderInsetLT, which only says anything once the bar has drawn + /// and only if there is a child to ask. + Point2I getMenuLocalCoord(const Point2I &src); + + /// The "+" in global coordinates, or an empty rect when there is none. + RectI getAddItemGlobalRect(); + + /// The menu under a content-local point, drawn or not, active or not - which + /// is what authoring needs and findHitMenu deliberately does not give. + GuiMenuItemCtrl* findMenuAt(const Point2I &menuLocalPt); + + /// Draw the "+". Ghosted rather than drawn as a menu, because it is an + /// affordance and not an item. + void renderAddItem(RectI itemRect); + + /// Ask the Gui Editor for an item. The bar draws the affordance; it does not + /// make the item, because an item made while authoring has to be themed, + /// recorded for undo and announced to the Explorer tree. + /// @param parent The menu to put it in, or NULL for a top-level one. + void requestNewMenuItem(GuiMenuItemCtrl *parent); + + virtual bool onMouseDownEditor(const GuiEvent &event, const Point2I& offset); + + /// The bar's own bounds, plus the dropdown it draws below them while + /// authoring. Hit testing walks bounds, and the dropdown hangs outside the + /// bar's - so without this the click never reaches the bar at all and the + /// "+" row is unreachable. + virtual bool pointInControl(const Point2I& parentCoordPoint); + + /// @} + virtual void processHover(const GuiEvent &event); virtual void setHoverTarget(GuiMenuItemCtrl *ctrl); virtual void onTouchMove(const GuiEvent &event); @@ -163,6 +246,19 @@ class GuiMenuItemCtrl : public GuiControl virtual void inspectPostApply(); virtual void onChildAdded(GuiControl *child); virtual void onChildRemoved(SimObject *child); + + /// A menu item only means anything inside a bar or inside another item, so it + /// refuses every other parent. See GuiControl::canBeChildOf. + bool canBeChildOf(GuiControl* parent); + + /// Its bar decides where it sits and how wide it is, from the text. See + /// GuiControl::isGeometryEditable. + bool isGeometryEditable() { return false; }; + + /// Tell whoever is laying this out that its text - and so its width - + /// changed. The properties pane writes the text on every keystroke, so this + /// is what makes the bar reflow as you type. + virtual void setText(const char *txt = NULL); void checkForGoodChildren(); virtual void closeMenu(); void ApplyMenuSettings(); diff --git a/engine/source/gui/editor/guiMenuBarCtrl_ScriptBinding.h b/engine/source/gui/editor/guiMenuBarCtrl_ScriptBinding.h index d5cc77392..5fe70d41e 100644 --- a/engine/source/gui/editor/guiMenuBarCtrl_ScriptBinding.h +++ b/engine/source/gui/editor/guiMenuBarCtrl_ScriptBinding.h @@ -122,4 +122,41 @@ ConsoleMethodWithDocs(GuiMenuBarCtrl, setMenuActive, ConsoleVoid, 4, 4, ("string object->setMenuActive(argv[2], dAtob(argv[3])); } +/*! Returns the bounds of the editor-only "+" as "x y width height", in global + coordinates. + + Empty - "0 0 0 0" - unless the bar is inside the Gui being authored, which is + the only time the "+" is drawn. A bar with no menus at all still reports one; + that is what stops an emptied bar from being unrecoverable, now that the + control palette does not offer a GuiMenuItemCtrl. + @return The rectangle the "+" occupies on screen. +*/ +ConsoleMethodWithDocs(GuiMenuBarCtrl, getAddItemRect, ConsoleString, 2, 2, ()) +{ + RectI rect = object->getAddItemGlobalRect(); + + char* buffer = Con::getReturnBuffer(64); + dSprintf(buffer, 64, "%d %d %d %d", rect.point.x, rect.point.y, rect.extent.x, rect.extent.y); + + return buffer; +} + +/*! Returns the bounds of the "+" row at the foot of the open dropdown, as + "x y width height" in global coordinates. + + Empty unless the bar is being authored AND one of its menus is selected - + that is what decides which dropdown is showing. A menu with no commands in it + at all still reports one; that row is the only way a command ever gets made. + @return The rectangle the dropdown's "+" occupies on screen. +*/ +ConsoleMethodWithDocs(GuiMenuBarCtrl, getAddSubItemRect, ConsoleString, 2, 2, ()) +{ + RectI rect = object->getAddSubItemGlobalRect(); + + char* buffer = Con::getReturnBuffer(64); + dSprintf(buffer, 64, "%d %d %d %d", rect.point.x, rect.point.y, rect.extent.x, rect.extent.y); + + return buffer; +} + ConsoleMethodGroupEndWithDocs(GuiMenuBarCtrl) \ No newline at end of file diff --git a/engine/source/gui/guiControl.h b/engine/source/gui/guiControl.h index 7948e975c..bc54d857e 100755 --- a/engine/source/gui/guiControl.h +++ b/engine/source/gui/guiControl.h @@ -696,6 +696,18 @@ class GuiControl : public SimGroup, public virtual Tickable /// this. GuiTabPageCtrl is the one that does. virtual bool canBeChildOf(GuiControl* parent) { return true; }; + /// Whether the Gui Editor may move or resize this control. + /// + /// False where the PARENT dictates the geometry and the control's own + /// Position and Extent are written over the moment anything re-lays it out - + /// a tab page, a menu item. The editor draws such a control the way it draws + /// a locked one: an outline rather than eight sizing handles, which would + /// otherwise be handles you can drag to no effect at all. + /// + /// This is the control's own nature, not the user's padlock; isLocked is + /// that, and the editor honours both. + virtual bool isGeometryEditable() { return true; }; + /// @} /// @name Tabs diff --git a/engine/source/gui/guiControl_ScriptBinding.h b/engine/source/gui/guiControl_ScriptBinding.h index bea4a719c..e4f31690b 100644 --- a/engine/source/gui/guiControl_ScriptBinding.h +++ b/engine/source/gui/guiControl_ScriptBinding.h @@ -109,6 +109,22 @@ ConsoleMethodWithDocs(GuiControl, canBeChildOf, ConsoleBool, 3, 3, (GuiControl p return object->canBeChildOf(pParent); } +/*! Whether the Gui Editor may move or resize this control. + + False where the PARENT dictates the geometry and the control's own Position + and Extent are written over the moment anything re-lays it out - a tab page, + a menu item. The editor draws such a control the way it draws a locked one: + an outline rather than eight sizing handles. + + This is the control's own nature, not the user's padlock; isLocked is that, + and the editor honours both. + @return True when the editor may change this control's geometry. +*/ +ConsoleMethodWithDocs(GuiControl, isGeometryEditable, ConsoleBool, 2, 2, ()) +{ + return object->isGeometryEditable(); +} + /*! @return Returns the Id of the parent control */ ConsoleMethodWithDocs( GuiControl, getParent, ConsoleInt, 2, 2, ()) diff --git a/engine/source/gui/guiProfileTheme.cc b/engine/source/gui/guiProfileTheme.cc index 0eb241a26..3de53b52c 100644 --- a/engine/source/gui/guiProfileTheme.cc +++ b/engine/source/gui/guiProfileTheme.cc @@ -325,6 +325,37 @@ static void stampCondenserDarkBorder(GuiProfileTheme* theme, GuiBorderProfile* b applyBorderRecipe(border, recipe); } +static void stampSelectedInsetBorder(GuiProfileTheme* theme, GuiBorderProfile* border) +{ + // A padded inset in three states and a rule in the fourth. The odd one out is + // SELECTED, and it is deliberate: a menu separator is drawn as the menu item + // profile in its selected state and nothing else in a menu ever uses that + // state (hover is Highlight, greyed is Disabled), so the SL fields of a menu + // item's borders belong to separators alone. One border therefore gives a + // theme both the room around a label and the groove between two groups of + // them, which is why this is what a generated MenuItem wears. + // + // The margin is what makes it a groove rather than a band. A separator's + // height is nothing but this border's margin, rim and padding + // (GuiMenuListCtrl::updateSize measures it with a zero-height interior), so + // dropping the padding to 0 and giving it 4 of margin buys 4px of the menu's + // own fill above and below a 2px rule. + BorderRecipe recipe = baseBorderRecipe(theme); + set4(recipe.margin, 0, 0, 4, 0); + set4(recipe.border, 0, 0, 1, 0); + set4(recipe.borderColor, theme->getColorSurface()); + set4(recipe.padding, 10, 10, 0, 10); + recipe.underfill = true; + + // Alone among the recipes, not scaled by the theme's border size. This rim is + // a rule between two groups of commands rather than the edge of a control: at + // borderSize 0 it would vanish and the menu would lose its grouping, and at 2 + // or 3 it would thicken into a band. + recipe.borderScale = 1; + + applyBorderRecipe(border, recipe); +} + //----------------------------------------------------------------------------- // Profile recipes. Every profile starts from the Default recipe and overrides // what its category needs; borders are wired to the theme's border members. @@ -793,7 +824,12 @@ static void stampMenuItemProfile(GuiProfileTheme* theme, GuiControlProfile* prof STAMP_FIELD(profile, "fontColorHL", mFontColorHL, adj(text, 10)); STAMP_FIELD(profile, "fontColorNA", mFontColorNA, alphaOf(text, 150)); STAMP_FIELD(profile, "align", mAlignment, AlignmentType::LeftAlign); - stampProfileBorders(theme, profile, "Padded", NULL, NULL, NULL, NULL); + // Top and bottom take SelectedInset, so a new theme's menus arrive with + // separators that read as separators. The sides are held back from it and + // given the same inset with no state to it: were they to fall through, a + // separator would pick up the 4px margin and the rim at each end too, capping + // the rule with a tick rather than running it the width of the menu. + stampProfileBorders(theme, profile, "SelectedInset", "Padded", "Padded", NULL, NULL); } static void stampMenuContentProfile(GuiProfileTheme* theme, GuiControlProfile* profile) @@ -963,6 +999,7 @@ const GuiProfileTheme::BorderCategory GuiProfileTheme::smBorderCategories[] = { "RimmedExpander", "RimmedExpanderBorder", stampRimmedExpanderBorder }, { "CondenserLight", "CondenserLightBorder", stampCondenserLightBorder }, { "CondenserDark", "CondenserDarkBorder", stampCondenserDarkBorder }, + { "SelectedInset", "SelectedInsetBorder", stampSelectedInsetBorder }, }; const S32 GuiProfileTheme::smBorderCategoryCount = sizeof(smBorderCategories) / sizeof(smBorderCategories[0]); diff --git a/engine/source/testing/tests/guiProfileThemeTests.cc b/engine/source/testing/tests/guiProfileThemeTests.cc index 288c83600..091a84ff5 100644 --- a/engine/source/testing/tests/guiProfileThemeTests.cc +++ b/engine/source/testing/tests/guiProfileThemeTests.cc @@ -913,7 +913,8 @@ TEST( GuiProfileThemeTests, ThemeGeneratesTheNamedBorderPalette ) const char* names[] = { "Empty", "Rimmed", "Thick", "Light", "Dark", "Padded", "Highlight", "PaddedRim", "BevelLight", "BevelDark", "PaddedLight", - "PaddedDark", "RimmedExpander", "CondenserLight", "CondenserDark" }; + "PaddedDark", "RimmedExpander", "CondenserLight", "CondenserDark", + "SelectedInset" }; const S32 count = sizeof( names ) / sizeof( names[0] ); ASSERT_EQ( GuiProfileTheme::getBorderCategoryCount(), count ); for ( S32 i = 0; i < count; ++i ) @@ -998,6 +999,81 @@ TEST( GuiProfileThemeTests, SixBorderRecipesUseExpectedValues ) SUCCEED(); } +//----------------------------------------------------------------------------- +// SelectedInset: the border that gives a generated theme its menu separators. +// Three states pad a label; the selected state - the only state a menu ever +// draws a separator in - is a rule with room around it. +//----------------------------------------------------------------------------- + +TEST( GuiProfileThemeTests, SelectedInsetBorderIsARuleInTheSelectedState ) +{ + GuiProfileTheme* theme = new GuiProfileTheme(); + theme->registerObject( "UnitTestTheme" ); // constructor borderSize == 1 + + GuiBorderProfile* inset = theme->getBorder( StringTable->insert( "SelectedInset" ) ); + ASSERT_TRUE( inset != NULL ); + + // Normal, highlight and disabled: a plain 10px inset, no rim. + const S32 padStates[] = { 0, 1, 3 }; + for ( S32 s = 0; s < 3; ++s ) + { + const S32 i = padStates[s]; + ASSERT_EQ( inset->mMargin[i], 0 ); + ASSERT_EQ( inset->mBorder[i], 0 ); + ASSERT_EQ( inset->mPadding[i], 10 ); + } + + // Selected: margin, rim, no padding - so a separator is exactly as tall as + // its own chrome, which is how GuiMenuListCtrl::updateSize measures one. + ASSERT_EQ( inset->mMargin[2], 4 ); + ASSERT_EQ( inset->mBorder[2], 1 ); + ASSERT_EQ( inset->mPadding[2], 0 ); + ASSERT_TRUE( inset->mUnderfill ); + + for ( S32 i = 0; i < 4; ++i ) + ASSERT_TRUE( inset->mBorderColor[i] == theme->getColorSurface() ); + + // A rule, not an edge: unlike every other recipe this one ignores borderSize, + // so a theme with no borders at all still separates its menus, and a heavy + // one does not turn the rule into a band. + theme->setDataField( StringTable->insert( "borderSize" ), NULL, "0" ); + ASSERT_EQ( inset->mBorder[2], 1 ); + theme->setDataField( StringTable->insert( "borderSize" ), NULL, "3" ); + ASSERT_EQ( inset->mBorder[2], 1 ); + + theme->deleteObject(); + + SUCCEED(); +} + +TEST( GuiProfileThemeTests, MenuItemProfileWearsSelectedInsetWithPaddedSides ) +{ + GuiProfileTheme* theme = new GuiProfileTheme(); + theme->registerObject( "UnitTestTheme" ); + + GuiControlProfile* item = theme->getProfile( StringTable->insert( "MenuItem" ) ); + ASSERT_TRUE( item != NULL ); + ASSERT_EQ( item->mBorderDefault, theme->getBorder( StringTable->insert( "SelectedInset" ) ) ); + + // Top and bottom fall back to the default - which is what shapes a separator - + // while the sides carry a plain inset instead, so a rule runs the width of the + // menu rather than being capped at both ends. + GuiBorderProfile* padded = theme->getBorder( StringTable->insert( "Padded" ) ); + ASSERT_EQ( item->getLeftProfile(), padded ); + ASSERT_EQ( item->getRightProfile(), padded ); + ASSERT_EQ( item->getTopProfile(), item->mBorderDefault ); + ASSERT_EQ( item->getBottomProfile(), item->mBorderDefault ); + + // Resolved eagerly: render reads these cached pointers, never the lazy + // resolver, so a freshly stamped profile must already know its sides. + ASSERT_TRUE( item->getLeftBorder() != NULL ); + ASSERT_TRUE( item->getTopBorder() != NULL ); + + theme->deleteObject(); + + SUCCEED(); +} + //----------------------------------------------------------------------------- // Custom borders: single-use, user-authored borders owned by the theme as // extras. They are not category members, keep their own field values, are diff --git a/tests/shots/menuBar.cs b/tests/shots/menuBar.cs new file mode 100644 index 000000000..baf591590 --- /dev/null +++ b/tests/shots/menuBar.cs @@ -0,0 +1,141 @@ +// Visual harness for the menu bar's editor-only affordances. +// +// ONE bar, shot in three states, because a Gui can only really show one: a menu +// bar owns nothing but its height. 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 - so two bars in a Gui sit exactly +// on top of each other. +// +// empty no menus at all -- which the control palette cannot fix, since it +// does not offer a GuiMenuItemCtrl, so the "+" is the only way +// anything ever gets into a bar +// menus three of them, so the "+" has to land after the last +// nested a menu with commands inside it +// +// The editor's own menu bar is in every shot, along the top, and must never grow +// a "+": it is not inside the Gui being authored, so isEditMode() is false for +// it. +// +// Run: tests/run.ps1 -Shots menuBar ; look in shots/. + +setLogMode(1); +$Scripts::ignoreDSOs = true; +setScriptExecEcho(false); +trace(false); + +testExec("editor/main.cs"); +schedule(2500, 0, "mbOpenProject"); + +// A real project, loaded the way the project selector does it, so the bars are +// drawn wearing a project's menu profiles rather than the engine's fallbacks. +function mbOpenProject() +{ + ProjectManager.setProjectFolder("PlanetX"); + EditorCore.projectSelector.onProjectSelected(pathConcat(getMainDotCsDir(), "PlanetX")); + schedule(2500, 0, "mbOpenEditor"); +} + +// Loading AppCore starts the project's game over the canvas; the editor comes +// back the way Ctrl+~ does it. Pages register in load order: EditorConsole, +// ProjectManager, AssetAdmin, GuiEditor. +function mbOpenEditor() +{ + EditorCore.toggleEditor(); + EditorCore.tabBook.selectPage(3); + schedule(1500, 0, "mbStep1"); +} + +function mbStep1() +{ + $mbTheme = GuiEditor.themeLibrary.createTheme("MenuBarShotTheme"); + GuiEditor.themeName = $mbTheme.getName(); + + // Position is not ours to choose; the bar pins itself to its parent's origin. + $mbBar = new GuiMenuBarCtrl() { Extent = "300 30"; }; + GuiEditor.rootGui.add($mbBar); + GuiEditor.themeApplier.applyToBranch($mbBar, $mbTheme, true); + + createPath(testRoot("shots/")); + schedule(900, 0, "mbShootEmpty"); +} + +function mbShootEmpty() +{ + mbGrab("menubar-empty"); + + for(%i = 1; %i <= 3; %i++) + { + mbItem($mbBar, "Menu " @ %i); + } + schedule(600, 0, "mbShootMenus"); +} + +function mbShootMenus() +{ + mbGrab("menubar-menus"); + + // One of each kind, so the shot says what each looks like: a plain command, a + // separator written as a dash, a toggle that starts on, a radio that does + // not, and one that opens a submenu of its own. + %menu = $mbBar.getObject(0); + mbItem(%menu, "New"); + mbItem(%menu, "-"); + + %toggle = mbItem(%menu, "Show Grid"); + %toggle.Toggle = true; + %toggle.IsOn = true; + + %radio = mbItem(%menu, "Snap to Grid"); + %radio.Radio = true; + + %sub = mbItem(%menu, "Recent"); + mbItem(%sub, "titleGui.gui.taml"); + + schedule(600, 0, "mbShootNested"); +} + +// Nothing selected, so no dropdown: the commands exist but the box does not. +function mbShootNested() +{ + mbGrab("menubar-nested"); + + // Selecting the menu is what opens it. Through the brain, so this is the same + // route a click on the canvas or a row in the Explorer tree takes. + GuiEditor.brain.selectList($mbBar.getObject(0)); + schedule(600, 0, "mbShootOpen"); +} + +function mbShootOpen() +{ + mbGrab("menubar-open"); + + // And a menu with nothing in it, which is the state every menu starts in and + // the one the runtime dropdown could not draw at all - its list is not built + // until a first child arrives. + GuiEditor.brain.selectList($mbBar.getObject(1)); + schedule(600, 0, "mbShootOpenEmpty"); +} + +function mbShootOpenEmpty() +{ + mbGrab("menubar-open-empty"); + + echo("SHOT DONE"); + schedule(300, 0, "quit"); +} + +// Added to its parent BEFORE it is given any children of its own: a menu item +// reads its bar out of its parent when a child arrives. +function mbItem(%parent, %text) +{ + %item = new GuiMenuItemCtrl() { Text = %text; }; + %parent.add(%item); + GuiEditor.themeApplier.applyToBranch(%item, $mbTheme, true); + return %item; +} + +function mbGrab(%name) +{ + screenShot(testRoot("shots/" @ %name @ ".png"), "PNG"); + echo("SHOT: wrote " @ %name @ ".png"); +} diff --git a/tests/smoke/inspectorPane.cs b/tests/smoke/inspectorPane.cs index 41c290df7..48880670b 100644 --- a/tests/smoke/inspectorPane.cs +++ b/tests/smoke/inspectorPane.cs @@ -499,13 +499,36 @@ function pStep6() pCheck("profile picker shows what the control wears", isObject(%current) && %header.profileRow.getValue() $= %current.getName()); - // A menu item has no GuiControl fields at all, so the header sheds - // everything but its name and its caption. + // A menu item has no GuiControl fields at all, so the header sheds everything + // generic and shows a block of its own instead - caption included, on one + // line, because a menu label is one line and it is also how wide the menu is. $pMenuItem = new GuiMenuItemCtrl(); %pane = pBind($pMenuItem); + %block = %pane.header.menuItemBlock; pCheck("menu item hides the profile row", !%pane.header.profileRow.isVisible()); pCheck("menu item hides geometry", !%pane.header.geometryGrid.isVisible()); - pCheck("menu item keeps its caption", %pane.header.textGrid.isVisible()); + pCheck("menu item shows its own block", %block.isVisible()); + pCheck("which stands the shared text block down", !%pane.header.textBlock.isVisible()); + pCheck("its caption is single line", %block.textRow.kind $= "text"); + pCheck("it carries the command fields", %block.commandRow.isVisible() && + %block.acceleratorRow.isVisible()); + + // Visible and Active are GuiControl's names, but a menu item registers them + // again for itself, so those two switches really do work here. + pCheck("menu item keeps Visible and Active", %pane.header.visibleButton.isVisible() && + %pane.header.activeButton.isVisible()); + pCheck("but not Accepts Input", !%pane.header.inputButton.isVisible()); + + // The section those fields used to be in is gone, so nothing is left showing + // a header that cannot open. + pCheck("no dead Menu Item section", !isObject(%pane.panel["Item"]) || + !%pane.panel["Item"].isVisible()); + + // An ordinary control is untouched by any of it. + %pane = pBind($pButton); + pCheck("an ordinary control shows no menu item block", + !%pane.header.menuItemBlock.isVisible()); + pCheck("and keeps the shared text block", %pane.header.textBlock.isVisible()); schedule(200, 0, "pStep7"); } diff --git a/tests/smoke/menuBar.cs b/tests/smoke/menuBar.cs new file mode 100644 index 000000000..aca88cbdb --- /dev/null +++ b/tests/smoke/menuBar.cs @@ -0,0 +1,516 @@ +//----------------------------------------------------------------------------- +// Authoring a GuiMenuBarCtrl in the Gui Editor. +// +// A menu item is not something the control palette offers - it means nothing +// outside a bar - so before the "+" there was no way whatsoever to put anything +// into a menu bar you dropped. Now the bar makes its own: one when it is +// dropped, one for every click on the "+" after the last menu, and one for every +// click on the "+" at the foot of an open menu. +// +// Menus nest, which is the whole difficulty. This checks both levels, that the +// dropdown follows the SELECTION rather than a toggle of its own, that a menu +// with nothing in it still offers the row that fills it, and that an item cannot +// be dragged, dropped or pasted anywhere but into a bar or another item. +// +// Runs on the real editor UI throughout, because all of it is gated on +// isEditMode(). +// +// Run: tests/run.ps1 menuBar ; grep MENUBAR in console.log. +//----------------------------------------------------------------------------- + +setLogMode(2); +setScriptExecEcho(false); +trace(false); +$Scripts::ignoreDSOs = true; +setCompanyAndProduct("Torque Game Engines", "Torque2D"); +ModuleDatabase.EchoInfo = false; +AssetDatabase.EchoInfo = false; +AssetDatabase.IgnoreAutoUnload = true; + +testExec("editor/main.cs"); + +function mbCheck(%label, %condition) +{ + echo(%condition ? ("MENUBAR PASS: " @ %label) : ("MENUBAR FAIL: " @ %label)); +} + +function mbUndoCount() +{ + return GuiEditor.undoRecorder.undoCount(); +} + +function mbSelect(%ctrl) +{ + GuiEditor.brain.clearSelection(); + GuiEditor.brain.select(%ctrl); +} + +// Is the inner rect wholly inside the outer one? Both are "x y width height". +function mbInside(%inner, %outer) +{ + return getWord(%inner, 0) >= getWord(%outer, 0) && + getWord(%inner, 1) >= getWord(%outer, 1) && + (getWord(%inner, 0) + getWord(%inner, 2)) <= (getWord(%outer, 0) + getWord(%outer, 2)) && + (getWord(%inner, 1) + getWord(%inner, 3)) <= (getWord(%outer, 1) + getWord(%outer, 3)); +} + +schedule(2000, 0, "mbStep1"); + +function mbStep1() +{ + ProjectManager.setProjectFolder("PlanetX"); + ModuleDatabase.ScanModules("PlanetX"); + ModuleDatabase.LoadExplicit("AppCore", 1); + + // By id: a bare identifier in TorqueScript is a string, and everything here + // compares object handles. + $mbTheme = nameToID("PlanetX"); + mbCheck("PlanetX theme loaded", isObject($mbTheme)); + + GuiEditor.open(); + GuiEditor.setTheme($mbTheme, false); + + EditorCore.open(); + EditorCore.tabBook.selectPageName("Gui Editor"); + + schedule(800, 0, "mbStepPalette"); +} + +//----------------------------------------------------------------------------- +// The palette, which has refused menu items all along. +//----------------------------------------------------------------------------- + +function mbStepPalette() +{ + %icons = GuiEditor.controlIcons; + + mbCheck("the palette will not place a menu item", !%icons.isPlaceableClass("GuiMenuItemCtrl")); + mbCheck("but a menu bar is still offered", + strstr(%icons.keysInGroup("Advanced"), "GuiMenuBarCtrl") != -1); + + // Unlike GuiTabPageCtrl, which keeps its icon row and only loses its tile, a + // menu item has no row in the table at all - so refusedNames is the only + // thing standing between it and the sweep over the class registry. + mbCheck("a menu item has no icon row to lose", !%icons.isKnown("GuiMenuItemCtrl")); + mbCheck("and is not covered by any entry", !%icons.coversClass("GuiMenuItemCtrl")); + + schedule(300, 0, "mbStepDrop"); +} + +//----------------------------------------------------------------------------- +// Dropping a bar, which has to arrive with a menu in it. +//----------------------------------------------------------------------------- + +function mbStepDrop() +{ + GuiEditor.undoRecorder.clear(); + GuiEditor.brain.setCurrentAddSet(GuiEditor.rootGui); + + // Through placeControl, which is what clicking a palette tile does. NOT + // onControlDropped: that opens with a cursor test a menu bar can never pass. + // A bar owns nothing but its height - GuiMenuBarCtrl::resize throws away the + // position it is handed - so its payload sits at 0,0 whatever it is told, and + // the middle of a 300x30 rectangle at 0,0 is up in the editor's own chrome. + $mbBar = new GuiMenuBarCtrl() { Extent = "300 30"; }; + $mbBar.Position = GuiEditor.brain.centredPlacement($mbBar); + GuiEditor.brain.placeControl($mbBar); + + mbCheck("the bar arrived", $mbBar.getParent() == GuiEditor.rootGui); + mbCheck("a bar refuses the position it was given (" @ $mbBar.getPosition() @ ")", + $mbBar.getPosition() $= "0 0"); + mbCheck("carrying exactly one menu (" @ $mbBar.getCount() @ ")", $mbBar.getCount() == 1); + + $mbMenu1 = $mbBar.getObject(0); + mbCheck("which is a menu item", $mbMenu1.getClassName() $= "GuiMenuItemCtrl"); + mbCheck("captioned \"" @ $mbMenu1.getText() @ "\"", $mbMenu1.getText() $= "Menu 1"); + + // The bar's own profile, and the two slots it dresses its items out of. + // + // Not the item's own Profile: GuiMenuItemCtrl calls SimObject's + // initPersistFields rather than GuiControl's, so it has no Profile FIELD at + // all - which is the same reason the properties pane calls it "bare". Its + // profile is assigned in C++ from these slots (onChildAdded does + // setControlProfile(mMenuProfile)), so these are the thing worth checking. + mbCheck("the bar was themed on arrival (" @ $mbBar.Profile.category @ ")", + $mbBar.Profile.category $= "MenuBar"); + mbCheck("its menu slot was themed (" @ $mbBar.MenuProfile.category @ ")", + $mbBar.MenuProfile.category $= "Menu"); + mbCheck("and its menu-item slot (" @ $mbBar.MenuItemProfile.category @ ")", + $mbBar.MenuItemProfile.category $= "MenuItem"); + + mbCheck("the whole thing is one undo step (" @ mbUndoCount() @ ")", mbUndoCount() == 1); + + // After the drop's own schedule(40), so the undo is not racing it. + schedule(200, 0, "mbStepDropUndo"); +} + +function mbStepDropUndo() +{ + %trash = GuiEditor.brain.getTrash(); + + GuiEditor.Undo(); + + // getGroup, not getParent: a control sitting in the trash - a plain SimGroup + // - reads as having no parent at all. + mbCheck("undo took the bar out of the Gui", $mbBar.getGroup() == %trash); + mbCheck("with its menu still inside it", $mbMenu1.getParent() == $mbBar); + + GuiEditor.Redo(); + mbCheck("redo put the bar back", $mbBar.getParent() == GuiEditor.rootGui); + mbCheck("still holding its menu (" @ $mbBar.getCount() @ ")", $mbBar.getCount() == 1); + + schedule(300, 0, "mbStepTopLevel"); +} + +//----------------------------------------------------------------------------- +// The bar's "+", which asks GuiEditorBrain::onAddMenuItem for a top-level menu. +//----------------------------------------------------------------------------- + +function mbStepTopLevel() +{ + GuiEditor.undoRecorder.clear(); + + GuiEditor.brain.onAddMenuItem($mbBar, ""); + + mbCheck("the bar grew a menu (" @ $mbBar.getCount() @ ")", $mbBar.getCount() == 2); + + $mbMenu2 = $mbBar.getObject(1); + mbCheck("numbered on from the last (" @ $mbMenu2.getText() @ ")", $mbMenu2.getText() $= "Menu 2"); + mbCheck("the new menu is selected", GuiEditor.brain.selectionList() $= $mbMenu2); + mbCheck("and is where the next control would land", + GuiEditor.brain.getCurrentAddSet() == $mbMenu2); + + mbCheck("adding a menu is one step (" @ mbUndoCount() @ ")", mbUndoCount() == 1); + + GuiEditor.Undo(); + mbCheck("undo took it back off (" @ $mbBar.getCount() @ ")", $mbBar.getCount() == 1); + GuiEditor.Redo(); + mbCheck("redo put it back (" @ $mbBar.getCount() @ ")", $mbBar.getCount() == 2); + + schedule(300, 0, "mbStepNested"); +} + +//----------------------------------------------------------------------------- +// The dropdown's "+", which is the same call with a parent named. +//----------------------------------------------------------------------------- + +function mbStepNested() +{ + GuiEditor.undoRecorder.clear(); + + GuiEditor.brain.onAddMenuItem($mbBar, $mbMenu1); + + mbCheck("the menu grew a command (" @ $mbMenu1.getCount() @ ")", $mbMenu1.getCount() == 1); + mbCheck("the bar did not (" @ $mbBar.getCount() @ ")", $mbBar.getCount() == 2); + + %command = $mbMenu1.getObject(0); + + // Numbering is per parent, so a menu's commands start from 1 rather than + // carrying on from the bar's count. + mbCheck("numbered from 1 inside its own menu (" @ %command.getText() @ ")", + %command.getText() $= "Menu 1"); + mbCheck("adding a command is one step (" @ mbUndoCount() @ ")", mbUndoCount() == 1); + + GuiEditor.brain.onAddMenuItem($mbBar, $mbMenu1); + mbCheck("a second command counts on (" @ $mbMenu1.getObject(1).getText() @ ")", + $mbMenu1.getObject(1).getText() $= "Menu 2"); + + schedule(300, 0, "mbStepDropdown"); +} + +//----------------------------------------------------------------------------- +// Which dropdown is showing, and where its "+" row is. Derived from the +// selection rather than toggled, so the Explorer tree opens it too. +//----------------------------------------------------------------------------- + +function mbStepDropdown() +{ + %bar = mbGlobalRect($mbBar); + + mbCheck("the bar reports a \"+\" (" @ $mbBar.getAddItemRect() @ ")", + getWord($mbBar.getAddItemRect(), 2) > 0); + mbCheck("square, as an affordance rather than a menu", + getWord($mbBar.getAddItemRect(), 2) == getWord($mbBar.getAddItemRect(), 3)); + mbCheck("inside the bar it belongs to", mbInside($mbBar.getAddItemRect(), %bar)); + mbCheck("after the menus rather than before them", + getWord($mbBar.getAddItemRect(), 0) > getWord(%bar, 0)); + + // Nothing selected, no dropdown. + GuiEditor.brain.clearSelection(); + mbCheck("nothing selected means no dropdown (" @ $mbBar.getAddSubItemRect() @ ")", + getWord($mbBar.getAddSubItemRect(), 2) == 0); + + // The menu itself. + mbSelect($mbMenu1); + %row = $mbBar.getAddSubItemRect(); + mbCheck("selecting a menu opens it (" @ %row @ ")", getWord(%row, 2) > 0); + mbCheck("and its \"+\" row sits below the bar", + getWord(%row, 1) >= (getWord(%bar, 1) + getWord(%bar, 3))); + + // A command inside it keeps it open, which is what makes the dropdown usable + // at all: selecting the row you just made must not close the menu. + mbSelect($mbMenu1.getObject(0)); + mbCheck("selecting a command keeps it open (" @ $mbBar.getAddSubItemRect() @ ")", + $mbBar.getAddSubItemRect() $= %row); + + // Another menu switches it. + mbSelect($mbMenu2); + mbCheck("selecting another menu switches the dropdown", + $mbBar.getAddSubItemRect() !$= %row && + getWord($mbBar.getAddSubItemRect(), 2) > 0); + + // Menu 2 has nothing in it, and that is the case that matters: a menu the + // "+" just made has no GuiMenuListCtrl at all, so the runtime machinery could + // not draw this even if it were open. + mbCheck("an empty menu still offers a \"+\" row (" @ $mbMenu2.getCount() @ " children)", + $mbMenu2.getCount() == 0 && getWord($mbBar.getAddSubItemRect(), 2) > 0); + + // A command's own bounds are the row it is drawn in, so everything that asks + // a control where it is - the editor's selection outline most of all - gets + // the answer the user can see. Before this they were the 64x64 a GuiControl + // is constructed with, and the outline appeared nowhere near the row. + mbSelect($mbMenu1); + %command = $mbMenu1.getObject(0); + + // The dropdown is laid out in onPreRender, or on demand by either of the two + // geometry accessors. Asking for the "+" row first is what makes the rows + // current for a selection that has not been drawn yet - without it these read + // whatever the previously open menu left behind. + %addRow = $mbBar.getAddSubItemRect(); + %row = %command.getGlobalPosition() SPC %command.getExtent(); + + mbCheck("a command is not still 64x64 (" @ %command.getExtent() @ ")", + %command.getExtent() !$= "64 64"); + mbCheck("it lines up with the \"+\" row below it (" @ %row @ " vs " @ %addRow @ ")", + getWord(%row, 0) == getWord(%addRow, 0) && getWord(%row, 2) == getWord(%addRow, 2)); + mbCheck("and sits inside the bar's dropdown", + getWord(%row, 1) < getWord(%addRow, 1)); + + // The bar itself is not one of its own menus. + mbSelect($mbBar); + mbCheck("selecting the bar closes the dropdown (" @ $mbBar.getAddSubItemRect() @ ")", + getWord($mbBar.getAddSubItemRect(), 2) == 0); + + schedule(300, 0, "mbStepCaption"); +} + +//----------------------------------------------------------------------------- +// A top-level menu is exactly as wide as its caption, so the strip has to be +// re-laid whenever the text moves - including on every keystroke, which is what +// the properties pane does while a caption is being typed. +//----------------------------------------------------------------------------- + +function mbStepCaption() +{ + %menu = $mbBar.getObject(0); + %wide = getWord(%menu.getExtent(), 0); + + // A plain field assignment, which is exactly what the pane's per-keystroke + // path does - not setEditFieldValue, so nothing here is going through + // inspectPostApply. + %menu.text = "A Considerably Longer Caption"; + mbCheck("a longer caption widens the menu (" @ %wide @ " -> " @ + getWord(%menu.getExtent(), 0) @ ")", getWord(%menu.getExtent(), 0) > %wide); + + %menu.text = "M"; + mbCheck("and a shorter one narrows it again (" @ getWord(%menu.getExtent(), 0) @ ")", + getWord(%menu.getExtent(), 0) < %wide); + + // The menu after it moves too, because the strip packs left to right. + %second = $mbBar.getObject(1); + mbCheck("the menu after it moved up (" @ %second.getPosition() @ ")", + getWord(%second.getPosition(), 0) == getWord(%menu.getExtent(), 0)); + + %menu.text = "Menu 1"; + + schedule(300, 0, "mbStepSpacer"); +} + +//----------------------------------------------------------------------------- +// Separators. A single dash in the caption is the whole of one - there is no +// field for it - which is what the documentation says and what a .gui file +// carries, so it has to survive being read back as well as being typed. +//----------------------------------------------------------------------------- + +function mbStepSpacer() +{ + %menu = $mbBar.getObject(0); + %command = %menu.getObject(0); + + mbSelect(%menu); + + // Asking for the "+" row is what lays the dropdown out, and the row height is + // how a separator shows itself: it is the profile's chrome with no line of + // text in it. + $mbBar.getAddSubItemRect(); + %tall = getWord(%command.getExtent(), 1); + + %command.text = "-"; + $mbBar.getAddSubItemRect(); + + mbCheck("a dash keeps its dash (" @ %command.getText() @ ")", %command.getText() $= "-"); + mbCheck("and makes the row a thin rule (" @ %tall @ " -> " @ + getWord(%command.getExtent(), 1) @ ")", getWord(%command.getExtent(), 1) < %tall); + + // The pane has to notice it, because typing the dash is how you make one. + %pane = GuiEditor.inspectorWindow.pane; + %pane.bind(%command); + mbCheck("the pane calls it a spacer (" @ %pane.header.menuItemBlock.kindRow.getValue() @ ")", + %pane.header.menuItemBlock.kindRow.getValue() $= "spacer"); + mbCheck("and drops the fields a rule cannot use", + !%pane.header.menuItemBlock.commandRow.isVisible()); + + // And back out of it again. + %command.text = "Open"; + $mbBar.getAddSubItemRect(); + mbCheck("typing over the dash makes it a command again (" @ + getWord(%command.getExtent(), 1) @ ")", getWord(%command.getExtent(), 1) == %tall); + + %pane.bind(%command); + mbCheck("and the pane agrees (" @ %pane.header.menuItemBlock.kindRow.getValue() @ ")", + %pane.header.menuItemBlock.kindRow.getValue() $= "command"); + + // A dash on the BAR is just a caption: a rule across a menu bar would have + // nothing either side of it to separate. + %menu.text = "-"; + $mbBar.getAddSubItemRect(); + %pane.bind(%menu); + mbCheck("a dash on the bar is not a separator (" @ + %pane.header.menuItemBlock.kindRow.getValue() @ ")", + %pane.header.menuItemBlock.kindRow.getValue() $= "command"); + mbCheck("and the bar is not offered the choice", + !%pane.header.menuItemBlock.kindRow.choiceButton[3].isVisible()); + %menu.text = "Menu 1"; + + // A command inside a menu is. + %pane.bind(%command); + mbCheck("a command inside a menu is offered it", + %pane.header.menuItemBlock.kindRow.choiceButton[3].isVisible()); + + schedule(300, 0, "mbStepDelete"); +} + +function mbGlobalRect(%ctrl) +{ + return %ctrl.getGlobalPosition() SPC %ctrl.getExtent(); +} + +//----------------------------------------------------------------------------- +// Emptying a bar, which used to be permanent. +//----------------------------------------------------------------------------- + +function mbStepDelete() +{ + GuiEditor.undoRecorder.clear(); + + while($mbBar.getCount() > 0) + { + %menu = $mbBar.getObject(0); + mbSelect(%menu); + GuiEditor.brain.onObjectRemoved(%menu); + } + + mbCheck("the bar can be emptied (" @ $mbBar.getCount() @ ")", $mbBar.getCount() == 0); + mbCheck("and is still recoverable (" @ $mbBar.getAddItemRect() @ ")", + getWord($mbBar.getAddItemRect(), 2) > 0); + + GuiEditor.brain.onAddMenuItem($mbBar, ""); + mbCheck("the \"+\" refills an emptied bar (" @ $mbBar.getCount() @ ")", + $mbBar.getCount() == 1); + mbCheck("numbering from the start again (" @ $mbBar.getObject(0).getText() @ ")", + $mbBar.getObject(0).getText() $= "Menu 1"); + + schedule(300, 0, "mbStepRules"); +} + +//----------------------------------------------------------------------------- +// Where a menu item is allowed to live. Two legal kinds of parent, unlike a tab +// page, because moving a command between menus is an ordinary thing to want. +//----------------------------------------------------------------------------- + +function mbStepRules() +{ + $mbPanel = new GuiControl() { Position = "10 400"; Extent = "300 200"; }; + GuiEditor.rootGui.add($mbPanel); + + $mbButton = new GuiButtonCtrl() { Position = "10 10"; Extent = "80 30"; Text = "B"; }; + $mbPanel.add($mbButton); + + %menu = $mbBar.getObject(0); + GuiEditor.brain.onAddMenuItem($mbBar, %menu); + %command = %menu.getObject(0); + + mbCheck("a menu belongs in its bar", %menu.canBeChildOf($mbBar)); + mbCheck("a command belongs in its menu", %command.canBeChildOf(%menu)); + mbCheck("and in a bar too", %command.canBeChildOf($mbBar)); + mbCheck("but not in a panel", !%command.canBeChildOf($mbPanel)); + mbCheck("nor on the root", !%command.canBeChildOf(GuiEditor.rootGui)); + mbCheck("an ordinary control still goes anywhere", $mbButton.canBeChildOf($mbPanel)); + + // And what the editor may do to it once it is there. A menu item's position + // and extent are the bar's to write, so the canvas draws it an outline rather + // than eight handles you could drag to no effect. + mbCheck("a menu's geometry is not the editor's", !%menu.isGeometryEditable()); + mbCheck("nor a command's", !%command.isGeometryEditable()); + mbCheck("an ordinary control's is", $mbButton.isGeometryEditable()); + + // The canvas drag. Both halves, because a rule that refused everything would + // pass the first check on its own. + mbSelect(%command); + GuiEditor.brain.moveSelectionToCtrl($mbPanel); + mbCheck("dragging a command onto a panel leaves it alone", %command.getParent() == %menu); + + mbSelect(%command); + GuiEditor.brain.moveSelectionToCtrl($mbBar); + mbCheck("dragging it onto the bar moves it", %command.getParent() == $mbBar); + + mbSelect($mbButton); + GuiEditor.brain.moveSelectionToCtrl($mbPanel); + mbCheck("an ordinary control is not caught by the rule", + $mbButton.getParent() == $mbPanel); + + schedule(300, 0, "mbStepPaste"); +} + +function mbStepPaste() +{ + %menu = $mbBar.getObject(0); + + mbSelect(%menu); + GuiEditor.Copy(); + + // Into a panel: refused, and the clipboard is left holding it. + %before = $mbPanel.getCount(); + GuiEditor.brain.setCurrentAddSet($mbPanel); + GuiEditor.Paste(); + mbCheck("pasting a menu into a panel puts nothing there (" @ $mbPanel.getCount() @ ")", + $mbPanel.getCount() == %before); + + %before = $mbBar.getCount(); + GuiEditor.brain.setCurrentAddSet($mbBar); + GuiEditor.Paste(); + mbCheck("pasting it into a bar does (" @ $mbBar.getCount() @ ")", + $mbBar.getCount() == (%before + 1)); + + schedule(300, 0, "mbStepChrome"); +} + +//----------------------------------------------------------------------------- +// The editor's own menu bar, which is a real GuiMenuBarCtrl full of real menu +// items and must be untouched by any of this. It is not inside the Gui being +// authored, so isEditMode() is false for it. +//----------------------------------------------------------------------------- + +function mbStepChrome() +{ + mbCheck("the editor's own bar exists", isObject(EditorCore.menuBar)); + mbCheck("and has its menus (" @ EditorCore.menuBar.getCount() @ ")", + EditorCore.menuBar.getCount() > 0); + mbCheck("but no \"+\" of its own (" @ EditorCore.menuBar.getAddItemRect() @ ")", + getWord(EditorCore.menuBar.getAddItemRect(), 2) == 0); + mbCheck("and no dropdown of its own (" @ EditorCore.menuBar.getAddSubItemRect() @ ")", + getWord(EditorCore.menuBar.getAddSubItemRect(), 2) == 0); + + echo("MENUBAR DONE"); + quit(); +} diff --git a/tests/smoke/menuBarClick.cs b/tests/smoke/menuBarClick.cs new file mode 100644 index 000000000..06bdbb49d --- /dev/null +++ b/tests/smoke/menuBarClick.cs @@ -0,0 +1,239 @@ +//----------------------------------------------------------------------------- +// Clicking the menu bar's two "+" affordances, for real. +// +// menuBar.cs calls GuiEditorBrain::onAddMenuItem directly, which is the right +// test for what the editor does once asked. This is the half that asks, and all +// of it is C++ the script cannot reach: GuiMenuBarCtrl::onMouseDownEditor +// converts the mouse point into the bar's content coordinates and tests it +// against the strip's "+", then the open dropdown's "+" row, then the rows, then +// the menus. Four rectangles in a space nothing else in the file uses - the +// runtime findHitMenu reaches it a third way, through a child's render inset. +// +// The sequence is the real one: click the bar's "+", get a menu, and because the +// editor selects what it just made, that menu's dropdown is already open - so +// the second click puts the first command in it. +// +// Driven by menuBarClick.input.ps1. Neither point is hard-coded there: the +// engine works out where each "+" actually landed and hands it over in a file. A +// hard-coded click that drifted off the target would report a missing item, +// which is exactly what a broken hit test reports. +//----------------------------------------------------------------------------- + +setLogMode(2); +setScriptExecEcho(false); +trace(false); +$Scripts::ignoreDSOs = true; +setCompanyAndProduct("Torque Game Engines", "Torque2D"); +ModuleDatabase.EchoInfo = false; +AssetDatabase.EchoInfo = false; +AssetDatabase.IgnoreAutoUnload = true; + +testExec("editor/main.cs"); + +function mcCheck(%label, %condition) +{ + echo(%condition ? ("MENUCLICK PASS: " @ %label) : ("MENUCLICK FAIL: " @ %label)); +} + +// Where the engine leaves the next point for the driver to click. +function mcTargetFile() +{ + return testRoot("shots/menuBarClickTarget.txt"); +} + +function mcAimAt(%rect, %what) +{ + %x = getWord(%rect, 0) + mFloor(getWord(%rect, 2) / 2); + %y = getWord(%rect, 1) + mFloor(getWord(%rect, 3) / 2); + + %file = new FileObject(); + %file.openForWrite(mcTargetFile()); + %file.writeLine(%x SPC %y); + %file.close(); + %file.delete(); + + echo("MENUCLICK: " @ %what @ " at " @ %x SPC %y); +} + +schedule(2500, 0, "mcStep1"); + +// Through the project selector, the way a person opens a project. Calling +// GuiEditor.open() directly leaves the selector sitting on top, and a posted +// click lands on whatever is actually in front. +function mcStep1() +{ + ProjectManager.setProjectFolder("PlanetX"); + EditorCore.projectSelector.onProjectSelected(pathConcat(getMainDotCsDir(), "PlanetX")); + + schedule(2500, 0, "mcStep2"); +} + +function mcStep2() +{ + EditorCore.toggleEditor(); + EditorCore.tabBook.selectPage(3); + + createPath(testRoot("shots/")); + + schedule(1500, 0, "mcStep3"); +} + +function mcStep3() +{ + GuiEditor.brain.setCurrentAddSet(GuiEditor.rootGui); + + // placeControl, which is what clicking a palette tile does. A menu bar pins + // itself to its parent's origin, so there is no position to choose. + $mcBar = new GuiMenuBarCtrl() { Extent = "300 30"; }; + $mcBar.Position = GuiEditor.brain.centredPlacement($mcBar); + GuiEditor.brain.placeControl($mcBar); + + // Nothing selected: a drop selects what it dropped, and the edit control + // tests its sizing knobs before it hands the click to the control. + GuiEditor.brain.clearSelection(); + + schedule(300, 0, "mcStep4"); +} + +function mcStep4() +{ + mcCheck("the bar arrived with a menu (" @ $mcBar.getCount() @ ")", $mcBar.getCount() == 1); + + %rect = $mcBar.getAddItemRect(); + mcCheck("and reports a \"+\" to aim at (" @ %rect @ ")", getWord(%rect, 2) > 0); + mcAimAt(%rect, "the bar's +"); + + GuiEditor.undoRecorder.clear(); + + // The driver posts its first click during this window. + schedule(8000, 0, "mcStep5"); +} + +function mcStep5() +{ + mcCheck("clicking the bar's \"+\" added a menu (" @ $mcBar.getCount() @ ")", + $mcBar.getCount() == 2); + + if($mcBar.getCount() != 2) + { + echo("MENUCLICK DONE"); + schedule(400, 0, "quit"); + return; + } + + $mcMenu = $mcBar.getObject(1); + mcCheck("made by the editor, not the raw C++ (" @ $mcMenu.getText() @ ")", + $mcMenu.getText() $= "Menu 2"); + mcCheck("one undo step for the click (" @ GuiEditor.undoRecorder.undoCount() @ ")", + GuiEditor.undoRecorder.undoCount() == 1); + + // The editor selected what it made, and the dropdown follows the selection - + // so the new menu is already open, with nothing in it but its "+" row. That + // is the whole point of deriving it from the selection rather than a toggle. + mcCheck("the new menu is selected", GuiEditor.brain.selectionList() $= $mcMenu); + + %rect = $mcBar.getAddSubItemRect(); + mcCheck("and its dropdown is already open (" @ %rect @ ")", getWord(%rect, 2) > 0); + mcAimAt(%rect, "the dropdown's +"); + + GuiEditor.undoRecorder.clear(); + + // And the second click. + schedule(8000, 0, "mcStep6"); +} + +function mcStep6() +{ + mcCheck("clicking the dropdown's \"+\" added a command (" @ $mcMenu.getCount() @ ")", + $mcMenu.getCount() == 1); + mcCheck("inside the menu, not on the bar (" @ $mcBar.getCount() @ ")", + $mcBar.getCount() == 2); + + if($mcMenu.getCount() == 1) + { + mcCheck("numbered from 1 in its own menu (" @ $mcMenu.getObject(0).getText() @ ")", + $mcMenu.getObject(0).getText() $= "Menu 1"); + } + + mcCheck("one undo step for that click too (" @ GuiEditor.undoRecorder.undoCount() @ ")", + GuiEditor.undoRecorder.undoCount() == 1); + + screenShot(testRoot("shots/menuBarClick.png"), "PNG"); + + schedule(300, 0, "mcStep7"); +} + +//----------------------------------------------------------------------------- +// And the editor's OWN menu bar, which is a real GuiMenuBarCtrl full of real +// menu items sitting a few pixels above everything this suite just did. +// +// It must still open the ordinary way. Nothing here touched openMenu or the +// full-canvas dialog it pushes, but the bar's findHitControl was given +// GuiControl's signature so that it overrides rather than hides - and that +// changes which control the canvas resolves a point to at runtime, not just in +// the editor. This is the check for that. +//----------------------------------------------------------------------------- + +function mcStep7() +{ + %file = mcFindItem(EditorCore.menuBar, "File"); + mcCheck("the editor has a File menu", isObject(%file)); + mcCheck("which has commands in it (" @ %file.getCount() @ ")", %file.getCount() > 0); + mcCheck("and no \"+\" of its own (" @ EditorCore.menuBar.getAddItemRect() @ ")", + getWord(EditorCore.menuBar.getAddItemRect(), 2) == 0); + + // An open menu is a dialog pushed on the canvas, so the canvas child count is + // what says whether it opened. + $mcDialogs = Canvas.getCount(); + + mcAimAt(%file.getGlobalPosition() SPC %file.getExtent(), "the editor's own File menu"); + + schedule(8000, 0, "mcStep8"); +} + +function mcStep8() +{ + mcCheck("clicking a real menu still opens it (" @ $mcDialogs @ " -> " @ + Canvas.getCount() @ ")", Canvas.getCount() == ($mcDialogs + 1)); + + screenShot(testRoot("shots/menuBarRuntime.png"), "PNG"); + + // And shut it again before leaving. An open menu holds its scroller inside a + // dialog pushed on the canvas, and quitting out from under that hangs the + // engine on the way down - which is nothing to do with this feature, but is + // a state no test should leave behind. The background catcher is full-canvas, + // so a click anywhere closes it. + mcAimAt("500 600 4 4", "somewhere else, to close it"); + + schedule(8000, 0, "mcStep9"); +} + +function mcStep9() +{ + mcCheck("clicking away closes it again (" @ Canvas.getCount() @ ")", + Canvas.getCount() == $mcDialogs); + + echo("MENUCLICK DONE"); + schedule(400, 0, "quit"); +} + +// Menu items are nested controls, so this walks rather than indexes. +function mcFindItem(%parent, %text) +{ + for(%i = 0; %i < %parent.getCount(); %i++) + { + %item = %parent.getObject(%i); + if(%item.Text $= %text) + { + return %item; + } + + %found = mcFindItem(%item, %text); + if(isObject(%found)) + { + return %found; + } + } + + return 0; +} diff --git a/tests/smoke/menuBarClick.input.ps1 b/tests/smoke/menuBarClick.input.ps1 new file mode 100644 index 000000000..b0d8b4219 --- /dev/null +++ b/tests/smoke/menuBarClick.input.ps1 @@ -0,0 +1,50 @@ +# Input for menuBarClick.cs. Posts two real clicks: one on the menu bar's "+", +# and then one on the "+" row at the foot of the dropdown the first click opened. +# +# Neither point is written here. The engine works out where each "+" actually +# landed - which depends on the theme's font, the profile's borders and how wide +# the menus turned out - and leaves it in a file for this script to pick up. A +# hard-coded point that drifted off the target would report a missing item, which +# is precisely what a broken hit test reports, and the test would be lying either +# way. +param([IntPtr]$Hwnd) + +. "$PSScriptRoot\..\lib\input.ps1" + +$target = Join-Path $PSScriptRoot "..\..\shots\menuBarClickTarget.txt" + +# Anything left by an earlier run would be clicked before this run has even +# placed its bar. +if (Test-Path $target) { Remove-Item $target -Force } + +function Wait-ForTarget { + param([string]$Path, [int]$Seconds = 20) + + $deadline = (Get-Date).AddSeconds($Seconds) + while ((Get-Date) -lt $deadline) { + if (Test-Path $Path) { + $line = (Get-Content $Path -TotalCount 1) + if ($line -and $line.Trim()) { return $line.Trim().Split(' ') } + } + Start-Sleep -Milliseconds 250 + } + return $null +} + +# Three rounds, each written by the engine about eight seconds before it is read +# back: the bar's "+", then the dropdown's "+" row, then the editor's own File +# menu - which is not part of the feature at all, but is the check that a real +# menu still opens the ordinary way. +$labels = @('the bar''s +', 'the dropdown''s +', 'the editor''s File menu', 'away, to close it') +foreach ($label in $labels) { + $point = Wait-ForTarget -Path $target + if (-not $point) { + Write-Host " the engine never reported $label" + return + } + + Remove-Item $target -Force + + Send-EngineClick -Hwnd $Hwnd -X ([int]$point[0]) -Y ([int]$point[1]) + Write-Host " clicked $label at ($($point[0]),$($point[1]))" +} From 6e888f90564f781146aaad7915183f00aae35a6a Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Sat, 1 Aug 2026 09:55:03 -0400 Subject: [PATCH 21/55] Gui Editor: a list carries the rows it was authored with A list box or a drop down dropped into a Gui was an empty rectangle. Its rows lived only in mItems, filled by addItem, and initPersistFields registered two fields -- AllowMultipleSelections and FitParentWidth -- neither of which is a row. So a difficulty picker, a resolution list or a language menu had to be built in script, in an onWake somewhere away from the Gui that shows it, and the canvas showed nothing at all while the screen was being laid out. Now the rows are authored in an Items section of the properties pane, drawn on the canvas as they are typed, and saved with the Gui. Script keeps every method it had; a list filled at runtime is untouched. On disk. An item is neither a field nor a child object -- GuiListBoxCtrl::addObject refuses children outright -- so neither of the two things a writer walks can see one. They go out as TAML custom nodes, which is what GuiFrameSetCtrl does with its frame tree and for the same reason: Only what differs from an LBItem's defaults is written, so an ordinary list of captions is one attribute a row. TamlXmlWriter::compileCustomElements names the section after the element it is writing, so a subclass gets its own heading with nothing extra to do. getItemList/setItemList is the whole list as one opaque string, and it is not the file format: it is what the pane reads and writes in a single call and what the undo stack records, exactly as getFrameLayout/setFrameLayout serve a frame set. deepCloneChildren copies the rows, which is what makes copy, cut and paste carry them without the clipboard knowing anything about them. A drop down keeps its rows in a GuiDropDownListBoxCtrl built in its constructor and added to no set anything walks, so all four of those are forwarded. A tree is not a list here. GuiTreeViewCtrl derives from GuiListBoxCtrl but generates its rows from a root object, so a written-out set would be stale the moment it next built itself: writesItems() returns false, and the pane's hasItemList names the two classes rather than testing ancestry. The Items section. Nine controls on one line: caption, ID, show color, the color, active, starts selected, up, down, remove. Nine rather than eight because the color is two values, hasColor and color, and nothing can be read off a swatch alone -- a swatch always holds SOME color. Show color, not "own color": hasColor does not tint anything, it draws a bullet in front of the caption and indents the text past it (renderColorBullet). The block edits the list as a WHOLE, reading it in one getItemList and writing it back in one setItemList. Every gesture on offer 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 and shown per class, which is the arrangement the dynamic fields section 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. Its rows are rebuilt, always from a schedule(0), because the click that removes or moves one arrives from a button inside the row about to be freed. Four things that had to be got right, each commented where it lives: ColorF's default constructor is empty (gColor.h), so a row that has no color carries uninitialized memory in item->color. Serializing it made two identical lists compare unequal. Adding, removing and moving read the CONTROL, not the row widgets. The widgets lag the deferred rebuild, so two clicks on Add inside one frame read the same stale chain and the first row added was lost. Only the deferred path re-measures the section. forceLayout nudges the pane's width by a pixel and back, and an expanded GuiPanelCtrl takes the nudge up without giving it back -- so a second one in the same bind left the section wider than the pane every time a control was selected, and the row's right-hand icons were what fell off the edge. The rows sit in a GuiGridCtrl at one column, not a chain. 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. TAML custom-node names must be interned case-INSENSITIVELY. They are matched by StringTable pointer, and TamlCustomNodes::findNode and the XML parser both intern with the default. A name interned the case-sensitive way is a different pointer and matches nothing. This is not theoretical: StringTable hands back the first spelling of a name it was ever given, so a case-sensitive "ID" wrote itself out as Id="1" -- whichever spelling reached the table first -- and then failed to recognise its own output, dropping every ID in the file with nothing but a warnf. Which spelling wins depends on static initialisation order across translation units, so it passed one build and failed the next. GuiFrameSetCtrl's ten frame-node constants had the same form and are fixed with it. That one has been lucky so far, and reverting it and rebuilding still passed, which is the hazard rather than a refutation of it -- so the frame-set test that comes with this is not a demonstration of that fix. It is here because frame-set persistence had no coverage at all, and a flat tree with both children piled into it is the shape the failure takes. Saving as .gui says what it cannot keep. FileObject::writeObject walks fields and children and that is the whole of it, so the script format drops custom nodes. GuiEditor::tamlOnlyStateSummary names what would go -- rows on any list, and any frame set that has been split -- in the save dialog's feedback line and through warn() from SaveCore, since a re-save never opens a dialog. Frame layouts have been lost this way silently since they were written; they are covered too. tests/smoke/listItems.cs is 74 checks over the encoding, the TAML round trip for both classes, deep clone, the tree exclusion, the pane, undo and redo, the single-selection rule and the warning. tests/smoke/frameSet.cs is 12 over a split frame set through a file and a clone. tests/shots/listItems.cs draws the section for looking at. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qi2pZ9UzF5rsLCNVErpzwt --- editor/GuiEditor/GuiEditor.cs | 96 ++++ .../GuiEditor/scripts/GuiEditorControlSpec.cs | 14 + .../scripts/GuiEditorInspectorPane.cs | 47 ++ editor/GuiEditor/scripts/GuiEditorItemRow.cs | 349 ++++++++++++ .../GuiEditor/scripts/GuiEditorItemsBlock.cs | 509 ++++++++++++++++++ .../scripts/GuiEditorSaveGuiDialog.cs | 24 +- .../GuiEditor/scripts/GuiEditorUndoAction.cs | 36 +- .../scripts/GuiEditorUndoRecorder.cs | 33 ++ engine/source/gui/buttons/guiDropDownCtrl.cc | 58 ++ engine/source/gui/buttons/guiDropDownCtrl.h | 18 + .../buttons/guiDropDownCtrl_ScriptBinding.h | 22 + .../source/gui/containers/guiFrameSetCtrl.cc | 34 +- engine/source/gui/guiListBoxCtrl.cc | 375 +++++++++++++ engine/source/gui/guiListBoxCtrl.h | 36 +- .../source/gui/guiListBoxCtrl_ScriptBinding.h | 26 + engine/source/gui/guiTreeViewCtrl.h | 7 + tests/shots/listItems.cs | 124 +++++ tests/smoke/frameSet.cs | 170 ++++++ tests/smoke/listItems.cs | 507 +++++++++++++++++ 19 files changed, 2469 insertions(+), 16 deletions(-) create mode 100644 editor/GuiEditor/scripts/GuiEditorItemRow.cs create mode 100644 editor/GuiEditor/scripts/GuiEditorItemsBlock.cs create mode 100644 tests/shots/listItems.cs create mode 100644 tests/smoke/frameSet.cs create mode 100644 tests/smoke/listItems.cs diff --git a/editor/GuiEditor/GuiEditor.cs b/editor/GuiEditor/GuiEditor.cs index 9a58c7415..2e7658ea2 100644 --- a/editor/GuiEditor/GuiEditor.cs +++ b/editor/GuiEditor/GuiEditor.cs @@ -61,6 +61,8 @@ 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 @@ -764,6 +766,91 @@ class = "GuiEditorThemeDialog"; } } +//----------------------------------------------------------------------------- +// 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 @@ -778,6 +865,15 @@ class = "GuiEditorThemeDialog"; 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 ---//"); diff --git a/editor/GuiEditor/scripts/GuiEditorControlSpec.cs b/editor/GuiEditor/scripts/GuiEditorControlSpec.cs index 96ec3fd0a..907fa5eb8 100644 --- a/editor/GuiEditor/scripts/GuiEditorControlSpec.cs +++ b/editor/GuiEditor/scripts/GuiEditorControlSpec.cs @@ -254,6 +254,20 @@ 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. diff --git a/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs b/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs index e6329130e..05207b51d 100644 --- a/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs +++ b/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs @@ -124,6 +124,7 @@ class = "GuiEditorHeaderBlock"; // 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. @@ -282,6 +283,39 @@ class = "GuiEditorDynamicFields"; %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() @@ -493,6 +527,7 @@ class = "GuiProfileEditorFieldRow"; %this.header.bindClass(%ctrl, %class); %this.buildVariantsSection(); %this.dynamicFields.bind(%ctrl); + %this.itemsBlock.bind(%this.spec.hasItemList(%class) ? %ctrl : ""); %this.applyFilter(); %this.refresh(); @@ -824,6 +859,10 @@ class = "GuiProfileEditorFieldRow"; // 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)); } @@ -932,6 +971,14 @@ class = "GuiProfileEditorFieldRow"; %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"), diff --git a/editor/GuiEditor/scripts/GuiEditorItemRow.cs b/editor/GuiEditor/scripts/GuiEditorItemRow.cs new file mode 100644 index 000000000..5dc24f08c --- /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 GuiEditorToggleIcon 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 = "GuiEditorToggleIcon"; + 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/GuiEditorSaveGuiDialog.cs b/editor/GuiEditor/scripts/GuiEditorSaveGuiDialog.cs index 456ea95e7..27cf5e9fe 100644 --- a/editor/GuiEditor/scripts/GuiEditorSaveGuiDialog.cs +++ b/editor/GuiEditor/scripts/GuiEditorSaveGuiDialog.cs @@ -157,15 +157,35 @@ class = "EditorForm"; if(isFile(%filePath)) { %this.createButton.active = true; - %this.feedback.setText("A file by this name already exists. It will be overwritten."); + %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.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()) diff --git a/editor/GuiEditor/scripts/GuiEditorUndoAction.cs b/editor/GuiEditor/scripts/GuiEditorUndoAction.cs index 2018cf72a..298b9c9a5 100644 --- a/editor/GuiEditor/scripts/GuiEditorUndoAction.cs +++ b/editor/GuiEditor/scripts/GuiEditorUndoAction.cs @@ -22,6 +22,9 @@ // 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 @@ -121,6 +124,19 @@ %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; @@ -160,6 +176,10 @@ { %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], @@ -193,9 +213,10 @@ continue; } - // A move or an order 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" || %this.opField[%i] $= %field) + // 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; } @@ -397,6 +418,15 @@ 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]; diff --git a/editor/GuiEditor/scripts/GuiEditorUndoRecorder.cs b/editor/GuiEditor/scripts/GuiEditorUndoRecorder.cs index 4edc0124f..508576a35 100644 --- a/editor/GuiEditor/scripts/GuiEditorUndoRecorder.cs +++ b/editor/GuiEditor/scripts/GuiEditorUndoRecorder.cs @@ -197,6 +197,39 @@ class = "GuiEditorUndoAction"; %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 diff --git a/engine/source/gui/buttons/guiDropDownCtrl.cc b/engine/source/gui/buttons/guiDropDownCtrl.cc index 3af32adac..6606126c5 100644 --- a/engine/source/gui/buttons/guiDropDownCtrl.cc +++ b/engine/source/gui/buttons/guiDropDownCtrl.cc @@ -142,6 +142,64 @@ void GuiDropDownCtrl::initPersistFields() endGroup("Drop Down"); } +//----------------------------------------------------------------------------- +// Static rows. All four of these belong to the list box; a drop down only has to +// find it, because mListBox is not a child of anything a writer or a clone +// walks. It writes rather than +// with nothing extra to do: TamlXmlWriter::compileCustomElements names the +// section after the element it is writing. +//----------------------------------------------------------------------------- + +void GuiDropDownCtrl::onTamlCustomWrite(TamlCustomNodes& customNodes) +{ + Parent::onTamlCustomWrite(customNodes); + + if (mListBox != NULL) + { + mListBox->onTamlCustomWrite(customNodes); + } +} + +void GuiDropDownCtrl::onTamlCustomRead(const TamlCustomNodes& customNodes) +{ + Parent::onTamlCustomRead(customNodes); + + if (mListBox != NULL) + { + mListBox->onTamlCustomRead(customNodes); + } +} + +const char* GuiDropDownCtrl::getItemList() +{ + return (mListBox != NULL) ? mListBox->getItemList() : ""; +} + +void GuiDropDownCtrl::setItemList(const char* itemList) +{ + if (mListBox != NULL) + { + mListBox->setItemList(itemList); + } + + // The button draws the selected row's caption, so a list arriving with one + // already on changes what the drop down itself reads. + setUpdate(); +} + +void GuiDropDownCtrl::deepCloneChildren(SimObject* clone) +{ + Parent::deepCloneChildren(clone); + + GuiDropDownCtrl* pDropDown = dynamic_cast(clone); + if (pDropDown == NULL) + { + return; + } + + pDropDown->setItemList(getItemList()); +} + void GuiDropDownCtrl::onTouchUp(const GuiEvent &event) { if (!mActive) diff --git a/engine/source/gui/buttons/guiDropDownCtrl.h b/engine/source/gui/buttons/guiDropDownCtrl.h index eb6dcf8af..b036205e9 100644 --- a/engine/source/gui/buttons/guiDropDownCtrl.h +++ b/engine/source/gui/buttons/guiDropDownCtrl.h @@ -91,6 +91,19 @@ class GuiDropDownCtrl : public GuiButtonCtrl GuiDropDownCtrl(); static void initPersistFields(); + /// @name Static rows + /// + /// A drop down's rows live in mListBox, which is built in the constructor and + /// added to no set anything walks - so none of GuiListBoxCtrl's persistence + /// reaches it on its own. Each of these is the list box's, forwarded, the way + /// the ~30 item methods in guiDropDownCtrl_ScriptBinding.h already are. + /// @{ + virtual void onTamlCustomWrite(TamlCustomNodes& customNodes); + virtual void onTamlCustomRead(const TamlCustomNodes& customNodes); + const char* getItemList(); + void setItemList(const char* itemList); + /// @} + virtual void onTouchUp(const GuiEvent &event); GuiControlState getCurrentState(); void onRender(Point2I offset, const RectI &updateRect); @@ -121,6 +134,11 @@ class GuiDropDownCtrl : public GuiButtonCtrl static bool writeScrollBarThicknessFn(void* obj, StringTableEntry pFieldName) { return static_cast(obj)->mScrollBarThickness != DEFAULT_THICKNESS; } DECLARE_CONOBJECT(GuiDropDownCtrl); + +protected: + /// The rows again: a deep clone copies fields and children, and the list box + /// holding them is neither. + virtual void deepCloneChildren(SimObject* clone); }; #endif \ No newline at end of file diff --git a/engine/source/gui/buttons/guiDropDownCtrl_ScriptBinding.h b/engine/source/gui/buttons/guiDropDownCtrl_ScriptBinding.h index d73cc9e14..57c8807fe 100644 --- a/engine/source/gui/buttons/guiDropDownCtrl_ScriptBinding.h +++ b/engine/source/gui/buttons/guiDropDownCtrl_ScriptBinding.h @@ -461,4 +461,26 @@ ConsoleMethodWithDocs(GuiDropDownCtrl, sortByID, ConsoleVoid, 2, 3, "([bool asce object->getList()->sortByID(direction); } +/*! Gets the whole list as text: every row, with its ID, color and state. Opaque + - hand it back to setItemList unchanged. + @return The list, as a string. +*/ +ConsoleMethodWithDocs(GuiDropDownCtrl, getItemList, ConsoleString, 2, 2, ()) +{ + return object->getItemList(); +} + +/*! Replaces every row in the list with the ones described by text taken earlier + by getItemList. + + A record may stop short: a caption on its own is a row, and every field left + off keeps its default. So "Easy\nNormal\nHard" is three plain rows. + @param itemList A string from getItemList. + @return No return value. +*/ +ConsoleMethodWithDocs(GuiDropDownCtrl, setItemList, ConsoleVoid, 3, 3, (itemList)) +{ + object->setItemList(argv[2]); +} + ConsoleMethodGroupEndWithDocs(GuiDropDownCtrl) \ No newline at end of file diff --git a/engine/source/gui/containers/guiFrameSetCtrl.cc b/engine/source/gui/containers/guiFrameSetCtrl.cc index 222239d3b..1c91f5f8e 100644 --- a/engine/source/gui/containers/guiFrameSetCtrl.cc +++ b/engine/source/gui/containers/guiFrameSetCtrl.cc @@ -1590,16 +1590,30 @@ const char* GuiFrameSetCtrl::getDataField(const char* tag, const U32 id) return SimObject::getDataField(tagFieldName, idBuffer); } -static StringTableEntry frameNodeSectionName = StringTable->insert("Frames", true); -static StringTableEntry frameNodeName = StringTable->insert("Frame", true); -static StringTableEntry frameIDName = StringTable->insert("ID", true); -static StringTableEntry frameChild1Name = StringTable->insert("Child1ID", true); -static StringTableEntry frameChild2Name = StringTable->insert("Child2ID", true); -static StringTableEntry frameIsVerticalName = StringTable->insert("IsVertical", true); -static StringTableEntry frameExtentXName = StringTable->insert("ExtentX", true); -static StringTableEntry frameExtentYName = StringTable->insert("ExtentY", true); -static StringTableEntry frameIsAnchoredName = StringTable->insert("IsAnchored", true); -static StringTableEntry frameChildMapName = StringTable->insert("ChildMap", true); +// Interned case-INSENSITIVELY, which is what all of these are compared against: +// TamlCustomNodes::findNode and the XML parser both intern with the default, so +// a name interned the case-sensitive way is a different pointer and matches +// nothing. +// +// It is not theoretical. StringTable hands back the first spelling of a name it +// was ever given, so a case-sensitive "ID" here could write itself out as "Id" - +// whichever spelling reached the table first, from anywhere in the engine - and +// then fail to recognise its own output on the way back in, dropping the field +// with nothing but a warnf. Which spelling wins depends on static-initialisation +// order across translation units, so it can change from one build to the next. +// GuiListBoxCtrl's Items nodes had exactly that happen to them. +// +// It also means a hand-edited Gui may spell these however it likes. +static StringTableEntry frameNodeSectionName = StringTable->insert("Frames"); +static StringTableEntry frameNodeName = StringTable->insert("Frame"); +static StringTableEntry frameIDName = StringTable->insert("ID"); +static StringTableEntry frameChild1Name = StringTable->insert("Child1ID"); +static StringTableEntry frameChild2Name = StringTable->insert("Child2ID"); +static StringTableEntry frameIsVerticalName = StringTable->insert("IsVertical"); +static StringTableEntry frameExtentXName = StringTable->insert("ExtentX"); +static StringTableEntry frameExtentYName = StringTable->insert("ExtentY"); +static StringTableEntry frameIsAnchoredName = StringTable->insert("IsAnchored"); +static StringTableEntry frameChildMapName = StringTable->insert("ChildMap"); void GuiFrameSetCtrl::onTamlCustomWrite(TamlCustomNodes& customNodes) { diff --git a/engine/source/gui/guiListBoxCtrl.cc b/engine/source/gui/guiListBoxCtrl.cc index bc0bb89f9..649046a4e 100755 --- a/engine/source/gui/guiListBoxCtrl.cc +++ b/engine/source/gui/guiListBoxCtrl.cc @@ -22,6 +22,7 @@ #include "platform/platform.h" #include "gui/guiListBoxCtrl.h" #include "gui/guiCanvas.h" +#include "persistence/taml/tamlCustom.h" #include #include "guiListBoxCtrl_ScriptBinding.h" @@ -610,6 +611,380 @@ void GuiListBoxCtrl::setItemText( S32 index, StringTableEntry text ) } #pragma endregion +#pragma region Persistence +//----------------------------------------------------------------------------- +// Static rows: the ones a list is authored with rather than filled with at +// runtime. +// +// An item is not a field and not a child object - GuiListBoxCtrl::addObject +// refuses children outright - so neither of the two things a writer walks can +// see one. They go out as TAML custom nodes instead, which is what +// GuiFrameSetCtrl does with its frame tree for exactly the same reason: +// +// +// +// +// +// +// +// +// +// TamlXmlWriter::compileCustomElements prefixes the section with the element +// name it is writing, so a subclass gets with nothing +// extra to do, and the reader's findNode() sees the unprefixed name either way. +// +// Everything but the caption is written only when it differs from what LBItem's +// constructor sets, so an ordinary list of captions stays one attribute a row. +// +// The two limitations are the frame set's, and worth naming: the legacy .gui +// script writer knows nothing of custom nodes, so a Gui saved in that format +// loses its rows (the Gui Editor's save dialog says so), and a deep clone has +// to copy them by hand - see deepCloneChildren below. +//----------------------------------------------------------------------------- + +// Interned case-INSENSITIVELY, which is what every one of these is compared +// against: TamlCustomNodes::findNode and the XML parser both intern with the +// default, so a name interned the case-sensitive way is a different pointer and +// silently matches nothing. +// +// It is not theoretical. StringTable hands back the first spelling of a name it +// was ever given, so a case-sensitive "ID" here wrote itself out as whatever +// spelling reached the table first - "Id", from somewhere else in the engine - +// and then failed to recognise it on the way back in, dropping every ID in the +// file with a warning. Which spelling wins depends on static initialisation +// order across translation units, so it can change from one build to the next. +// +// It also means a hand-edited file may spell these however it likes. +static StringTableEntry itemsNodeName = StringTable->insert("Items"); +static StringTableEntry itemNodeName = StringTable->insert("Item"); +static StringTableEntry itemTextName = StringTable->insert("Text"); +static StringTableEntry itemIDName = StringTable->insert("ID"); +static StringTableEntry itemActiveName = StringTable->insert("Active"); +static StringTableEntry itemSelectedName = StringTable->insert("Selected"); +static StringTableEntry itemColorName = StringTable->insert("Color"); + +void GuiListBoxCtrl::onTamlCustomWrite(TamlCustomNodes& customNodes) +{ + // Debug Profiling. + PROFILE_SCOPE(GuiListBoxCtrl_OnTamlCustomWrite); + + // Call parent. + Parent::onTamlCustomWrite(customNodes); + + if (!writesItems() || mItems.size() == 0) + { + return; + } + + TamlCustomNode* pItemsNode = customNodes.addNode(itemsNodeName); + + for (S32 i = 0; i < mItems.size(); i++) + { + LBItem* item = mItems[i]; + TamlCustomNode* pItemNode = pItemsNode->addNode(itemNodeName); + + pItemNode->addField(itemTextName, item->itemText); + + if (item->ID != 0) + { + pItemNode->addField(itemIDName, item->ID); + } + if (!item->isActive) + { + pItemNode->addField(itemActiveName, item->isActive); + } + if (item->isSelected) + { + pItemNode->addField(itemSelectedName, item->isSelected); + } + if (item->hasColor) + { + pItemNode->addField(itemColorName, item->color); + } + } +} + +void GuiListBoxCtrl::onTamlCustomRead(const TamlCustomNodes& customNodes) +{ + // Debug Profiling. + PROFILE_SCOPE(GuiListBoxCtrl_OnTamlCustomRead); + + // Call parent. + Parent::onTamlCustomRead(customNodes); + + const TamlCustomNode* pItemsNode = customNodes.findNode(itemsNodeName); + + if (pItemsNode == NULL) + { + return; + } + + // Whatever the control was built holding. A list read into an object that + // already has rows is a replacement, not an append. + clearItems(); + + const TamlCustomNodeVector& itemNodes = pItemsNode->getChildren(); + for (TamlCustomNodeVector::const_iterator nodeItr = itemNodes.begin(); nodeItr != itemNodes.end(); ++nodeItr) + { + const TamlCustomNode* pItemNode = *nodeItr; + + if (pItemNode->getNodeName() != itemNodeName) + { + Con::warnf("GuiListBoxCtrl::onTamlCustomRead() - Unknown tag name of '%s'. Only '%s' is valid.", + pItemNode->getNodeName(), itemNodeName); + continue; + } + + // Made before the fields are read, because every field below writes into + // it. A row with no Text at all is still a row - an empty caption is a + // legal item, and the editor can make one. + LBItem* item = appendItemInternal(StringTable->EmptyString); + if (!item) + { + continue; + } + + const TamlCustomFieldVector& fields = pItemNode->getFields(); + for (TamlCustomFieldVector::const_iterator fieldItr = fields.begin(); fieldItr != fields.end(); ++fieldItr) + { + const TamlCustomField* pField = *fieldItr; + StringTableEntry fieldName = pField->getFieldName(); + + if (fieldName == itemTextName) + { + item->itemText = StringTable->insert(pField->getFieldValue(), true); + } + else if (fieldName == itemIDName) + { + pField->getFieldValue(item->ID); + } + else if (fieldName == itemActiveName) + { + pField->getFieldValue(item->isActive); + } + else if (fieldName == itemSelectedName) + { + pField->getFieldValue(item->isSelected); + } + else if (fieldName == itemColorName) + { + pField->getFieldValue(item->color); + item->hasColor = true; + } + else + { + Con::warnf("GuiListBoxCtrl::onTamlCustomRead() - Encountered an unknown field name of '%s'.", fieldName); + } + } + + // The selection list is kept beside the items and has to agree with them. + if (item->isSelected) + { + mSelectedItems.push_front(item); + } + } + + updateSize(); +} + +//----------------------------------------------------------------------------- +// The list as one string, for the two callers that want all of it at once: the +// Gui Editor's Items section, which edits the list as a whole rather than a row +// at a time, and its undo stack, which records what the list was and what it +// became. The same job getFrameLayout/setFrameLayout do for a frame set, and +// like those it is opaque - the file format is the custom nodes above. +// +// One record per item, newline separated; TAB between fields, in a fixed order: +// +// text ID active selected hasColor "r g b a" +// +// A caption may hold neither character. A list row is one line, and a +// GuiTextEditCtrl can produce neither - Enter commits (handleEnterKey has no +// insert path) and Tab moves the focus - but setItemList strips them anyway, +// because a caption arriving from script has been through neither. +//----------------------------------------------------------------------------- + +const char* GuiListBoxCtrl::getItemList() +{ + // Measured rather than guessed: a list is any length, and the return buffer + // has to be asked for at the size it will actually need. 64 covers the five + // numbers, the four color components and the separators. + U32 size = 1; + for (S32 i = 0; i < mItems.size(); i++) + { + size += dStrlen(mItems[i]->itemText) + 64; + } + + char* buffer = Con::getReturnBuffer(size); + buffer[0] = '\0'; + + U32 used = 0; + for (S32 i = 0; i < mItems.size(); i++) + { + LBItem* item = mItems[i]; + + // A row without a color has no color to write: ColorF's default + // constructor is empty (gColor.h), so item->color is whatever was in + // that memory until something sets it. Printing it would make two + // identical lists read as different strings, which is the one thing an + // opaque round-trippable form may not do. + ColorF color = item->hasColor ? item->color : ColorF(0.0f, 0.0f, 0.0f, 0.0f); + + used += dSprintf(buffer + used, size - used, "%s%s\t%d\t%d\t%d\t%d\t%g %g %g %g", + (i == 0) ? "" : "\n", + item->itemText, + item->ID, + item->isActive ? 1 : 0, + item->isSelected ? 1 : 0, + item->hasColor ? 1 : 0, + color.red, color.green, color.blue, color.alpha); + } + + return buffer; +} + +// One field of a record, copied out of the middle of the string it sits in. +// Returns the buffer, so it reads as an argument to dAtoi and friends. +static const char* copyItemField(char* buffer, const U32 size, const char* start, S32 length) +{ + if (length < 0) + { + length = 0; + } + if ((U32)length >= size) + { + length = size - 1; + } + + dMemcpy(buffer, start, length); + buffer[length] = '\0'; + + return buffer; +} + +void GuiListBoxCtrl::setItemList(const char* itemList) +{ + // The order the fields are written in above. A record may stop short of all + // six: a script handing over a bare list of captions is a legal list, and + // every field left off keeps whatever LBItem's constructor gave it. + const S32 fieldMax = 6; + + clearItems(); + + for (const char* at = itemList; at != NULL && *at; ) + { + const char* recordEnd = dStrchr(at, '\n'); + if (recordEnd == NULL) + { + recordEnd = at + dStrlen(at); + } + + // Split on TAB by pointer rather than through getWord or getUnit: the + // first field is a caption, which may be empty and may hold spaces, and + // neither of those survives a word split. + const char* field[fieldMax]; + S32 fieldLength[fieldMax]; + S32 fieldCount = 0; + + const char* fieldStart = at; + for (const char* scan = at; scan <= recordEnd; scan++) + { + if (scan != recordEnd && *scan != '\t') + { + continue; + } + + if (fieldCount < fieldMax) + { + field[fieldCount] = fieldStart; + fieldLength[fieldCount] = (S32)(scan - fieldStart); + fieldCount++; + } + fieldStart = scan + 1; + } + + char scratch[1024]; + + LBItem* item = appendItemInternal(StringTable->insert( + copyItemField(scratch, sizeof(scratch), field[0], fieldLength[0]), true)); + + if (item != NULL) + { + if (fieldCount > 1) + { + item->ID = dAtoi(copyItemField(scratch, sizeof(scratch), field[1], fieldLength[1])); + } + if (fieldCount > 2) + { + item->isActive = dAtob(copyItemField(scratch, sizeof(scratch), field[2], fieldLength[2])); + } + if (fieldCount > 3) + { + item->isSelected = dAtob(copyItemField(scratch, sizeof(scratch), field[3], fieldLength[3])); + } + if (fieldCount > 4) + { + item->hasColor = dAtob(copyItemField(scratch, sizeof(scratch), field[4], fieldLength[4])); + } + if (fieldCount > 5 && item->hasColor) + { + const char* colorText = copyItemField(scratch, sizeof(scratch), field[5], fieldLength[5]); + Con::setData(TypeColorF, &item->color, 0, 1, &colorText); + } + + // The selection list is kept beside the items and has to agree with + // them, or getSelectedItem and the render disagree about what is on. + if (item->isSelected) + { + mSelectedItems.push_front(item); + } + } + + at = (*recordEnd == '\0') ? recordEnd : (recordEnd + 1); + } + + // The list is the control's size, so the canvas has to be told. This is what + // makes a row typed in the Gui Editor appear under the cursor as it is typed. + updateSize(); + setUpdate(); +} + +// A row added with none of the noise the public paths make: insertItem resizes +// the control once per item and addSelection scrolls and fires onSelect, and +// neither belongs in the middle of loading a list. +GuiListBoxCtrl::LBItem* GuiListBoxCtrl::appendItemInternal(StringTableEntry text) +{ + LBItem* newItem = createItem(); + if (!newItem) + { + Con::warnf("GuiListBoxCtrl::appendItemInternal - error allocating item memory!"); + return NULL; + } + + newItem->itemText = text; + mItems.push_back(newItem); + + return newItem; +} + +// Items are neither fields nor children, so a clone made by copying both comes +// back with an empty list. GuiFrameSetCtrl overrides the same phase for the same +// reason, and it is what makes the Gui Editor's copy, cut and paste carry the +// rows without knowing anything about them. +void GuiListBoxCtrl::deepCloneChildren(SimObject* clone) +{ + Parent::deepCloneChildren(clone); + + GuiListBoxCtrl* pListBox = dynamic_cast(clone); + if (pListBox == NULL) + { + return; + } + + pListBox->setItemList(getItemList()); +} +#pragma endregion + #pragma region Sizing void GuiListBoxCtrl::updateSize() { diff --git a/engine/source/gui/guiListBoxCtrl.h b/engine/source/gui/guiListBoxCtrl.h index 950c63561..47754876e 100755 --- a/engine/source/gui/guiListBoxCtrl.h +++ b/engine/source/gui/guiListBoxCtrl.h @@ -101,7 +101,32 @@ class GuiListBoxCtrl : public GuiControl // Persistence - static void initPersistFields(); + static void initPersistFields(); + + /// @name Static rows + /// + /// The rows a list is authored with, as opposed to the ones a script fills in + /// at runtime. An item is neither a field nor a child object, so it is + /// written as TAML custom nodes - the arrangement GuiFrameSetCtrl already + /// uses for its frame tree, with the same two consequences: the legacy .gui + /// script writer cannot carry them, and a deep clone has to copy them itself. + /// @{ + + virtual void onTamlCustomWrite( TamlCustomNodes& customNodes ); + virtual void onTamlCustomRead( const TamlCustomNodes& customNodes ); + + /// The whole list as one opaque string: one record per item, TAB-separated + /// fields in a fixed order. Not the file format - it is what the Gui Editor + /// reads and writes in a single call, and what its undo stack records, in the + /// same way getFrameLayout/setFrameLayout serve a frame set. + const char* getItemList(); + void setItemList( const char* itemList ); + + /// Whether the rows this control holds are its own to save. A GuiTreeViewCtrl + /// generates its items from a root object, so a written-out set of them would + /// be stale the moment the tree next builds itself. + virtual bool writesItems() { return true; } + /// @} // Item Accessors S32 getItemCount(); @@ -175,6 +200,15 @@ class GuiListBoxCtrl : public GuiControl protected: GuiControl *caller; + + /// Items are not children and not fields, so a deep clone would otherwise + /// come back with an empty list. This is the phase GuiFrameSetCtrl uses for + /// the same reason. + virtual void deepCloneChildren(SimObject* clone); + + /// Add an item without any of the noise addSelection/insertItem make: no + /// onSelect callback, no scroll, no resize per row. For loading a list in. + LBItem* appendItemInternal(StringTableEntry text); }; #endif \ No newline at end of file diff --git a/engine/source/gui/guiListBoxCtrl_ScriptBinding.h b/engine/source/gui/guiListBoxCtrl_ScriptBinding.h index 26dc53353..204f35996 100644 --- a/engine/source/gui/guiListBoxCtrl_ScriptBinding.h +++ b/engine/source/gui/guiListBoxCtrl_ScriptBinding.h @@ -428,4 +428,30 @@ ConsoleMethodWithDocs(GuiListBoxCtrl, sortByID, ConsoleVoid, 2, 3, "([bool ascen object->sortByID(direction); } +/*! Gets the whole list as text: every row, with its ID, color and state. Opaque + - hand it back to setItemList unchanged. + + A list is saved as TAML custom nodes rather than as fields, so anything that + needs to keep one and put it back - the Gui Editor's Items pane, and its undo + - reads it through here rather than a row at a time. + @return The list, as a string. +*/ +ConsoleMethodWithDocs(GuiListBoxCtrl, getItemList, ConsoleString, 2, 2, ()) +{ + return object->getItemList(); +} + +/*! Replaces every row in the list with the ones described by text taken earlier + by getItemList. + + A record may stop short: a caption on its own is a row, and every field left + off keeps its default. So "Easy\nNormal\nHard" is three plain rows. + @param itemList A string from getItemList. + @return No return value. +*/ +ConsoleMethodWithDocs(GuiListBoxCtrl, setItemList, ConsoleVoid, 3, 3, (itemList)) +{ + object->setItemList(argv[2]); +} + ConsoleMethodGroupEndWithDocs(GuiListBoxCtrl) \ No newline at end of file diff --git a/engine/source/gui/guiTreeViewCtrl.h b/engine/source/gui/guiTreeViewCtrl.h index c95666145..f32b0faa2 100755 --- a/engine/source/gui/guiTreeViewCtrl.h +++ b/engine/source/gui/guiTreeViewCtrl.h @@ -100,6 +100,13 @@ class GuiTreeViewCtrl : public GuiListBoxCtrl bool itemHasBranches(S32 index); public: + /// A tree's rows are not its own to save. Every TreeItem is generated from + /// mRootObject - inspectObject builds the lot and rebuilds them whenever the + /// object under them changes - so a set written into the .gui.taml would be + /// stale the moment the tree next built itself, and would then be thrown away + /// unread. The list box's static rows stop here. + virtual bool writesItems() { return false; } + // GuiControl //bool onWake(); //void onSleep(); diff --git a/tests/shots/listItems.cs b/tests/shots/listItems.cs new file mode 100644 index 000000000..b4f557922 --- /dev/null +++ b/tests/shots/listItems.cs @@ -0,0 +1,124 @@ +// Visual harness for the Items section of the Gui Editor's properties pane: the +// static rows a list box or a drop down is authored with. +// +// Worth looking at rather than only asserting on, because the row is dense -- +// nine controls on one line -- and whether all nine still fit at the pane's +// width is the whole question. The canvas is in the shot too: a row typed here +// is drawn on the list immediately, and that is the point of the feature. +// +// Every section but Items is shut, so the rows are in frame rather than eight +// screens below it. +// +// Run: tests/run.ps1 -Shots listItems ; look in shots/. + +setLogMode(1); +$Scripts::ignoreDSOs = true; +setScriptExecEcho(false); +trace(false); + +testExec("editor/main.cs"); +schedule(2500, 0, "sOpenProject"); + +// A real project, loaded the way the project selector does it, so the pane and +// the list on the canvas are both drawn wearing a project's theme. +function sOpenProject() +{ + ProjectManager.setProjectFolder("PlanetX"); + EditorCore.projectSelector.onProjectSelected(pathConcat(getMainDotCsDir(), "PlanetX")); + schedule(2500, 0, "sOpenEditor"); +} + +function sOpenEditor() +{ + EditorCore.toggleEditor(); + EditorCore.tabBook.selectPage(3); + schedule(1500, 0, "sStep1"); +} + +function sStep1() +{ + $sTheme = GuiEditor.themeLibrary.createTheme("ItemsShotTheme"); + GuiEditor.themeName = $sTheme.getName(); + + $sList = sPlace("GuiListBoxCtrl"); + $sList.resize(30, 30, 200, 140); + + // Five rows that between them use every field the section offers: an ID, a + // color dot, an inactive row and one that starts selected. + $sList.setItemList( + "Easy" TAB "1" TAB "1" TAB "0" TAB "0" TAB "1 1 1 1" NL + "Normal" TAB "2" TAB "1" TAB "1" TAB "0" TAB "1 1 1 1" NL + "Hard" TAB "3" TAB "1" TAB "0" TAB "1" TAB "1 0.4 0.2 1" NL + "Nightmare" TAB "4" TAB "0" TAB "0" TAB "1" TAB "0.8 0.2 0.2 1" NL + "Impossible" TAB "5" TAB "0" TAB "0" TAB "0" TAB "1 1 1 1"); + + $sDrop = sPlace("GuiDropDownCtrl"); + $sDrop.resize(30, 190, 200, 26); + $sDrop.setItemList( + "Windowed" TAB "1" NL + "Fullscreen" TAB "2" TAB "1" TAB "1" NL + "Borderless window" TAB "3"); + + createPath(testRoot("shots/")); + + // case TAB object global + $sCases = + "items-list" TAB "$sList" NL + "items-dropdown" TAB "$sDrop"; + $sIndex = 0; + schedule(1000, 0, "sShoot"); +} + +function sPlace(%class) +{ + %ctrl = eval("return new " @ %class @ "();"); + GuiEditor.rootGui.add(%ctrl); + GuiEditor.themeApplier.applyToBranch(%ctrl, $sTheme, true); + return %ctrl; +} + +function sShoot() +{ + %rec = getRecord($sCases, $sIndex); + %name = getField(%rec, 0); + %ctrl = eval("return " @ getField(%rec, 1) @ ";"); + + %pane = GuiEditor.inspectorWindow.pane; + %pane.bind(%ctrl); + + // Everything but Items shut, so the rows are in the shot rather than eight + // screens below it. The header cannot be collapsed and does not need to be. + %panels = %pane.panelList SPC %pane.classPanels; + for(%i = 0; %i < getWordCount(%panels); %i++) + { + %panel = %pane.panel[getWord(%panels, %i)]; + if(isObject(%panel)) + { + %panel.setExpanded(false); + } + } + %pane.textPanel.setExpanded(false); + %pane.dynamicPanel.setExpanded(true); + %pane.itemsPanel.setExpanded(true); + + GuiEditor.inspectorWindow.scroller.scrollToBottom(); + + // Let the chain, the rows and the panels settle before grabbing. + schedule(500, 0, "sGrab", %name); +} + +function sGrab(%name) +{ + screenShot(testRoot("shots/" @ %name @ ".png"), "PNG"); + echo("SHOT: wrote " @ %name @ ".png"); + + $sIndex++; + if($sIndex < getRecordCount($sCases)) + { + schedule(300, 0, "sShoot"); + return; + } + + echo("SHOT DONE"); + schedule(300, 0, "quit"); +} diff --git a/tests/smoke/frameSet.cs b/tests/smoke/frameSet.cs new file mode 100644 index 000000000..249d98d77 --- /dev/null +++ b/tests/smoke/frameSet.cs @@ -0,0 +1,170 @@ +//----------------------------------------------------------------------------- +// GuiFrameSetCtrl persistence. +// +// A frame set is the one container that keeps a layout of its own beside its +// child list, and none of it is a persist field: the split tree is written as +// TAML custom nodes and nothing else. So it is the one control whose file can +// look complete and load back as something else entirely - every frame merged +// into one, with the children piled into it - and until now nothing tested that. +// +// The failure it is shaped around: TAML matches custom-node field names by +// StringTable pointer, so a name that stops matching takes every value in a +// frame node with it - loadTamlFrame gives up on a frame of extent zero and the +// tree comes back flat, with nothing but a warnf to say so. That is what the +// value-count checks below are for. +// +// It cannot be provoked on demand. Whether a name matches depends on which +// spelling of it reached the string table first, which depends on static +// initialisation order across translation units - so the same source can pass +// one build and fail the next. GuiListBoxCtrl's Items nodes did exactly that; +// the frame set has so far been lucky. Both now intern case-insensitively, which +// is what removes the dependence. This test does not prove that fix on any given +// build; it is here because frame-set persistence had no coverage at all, and +// this is the shape a broken one takes. +// +// Run: tests/run.ps1 frameSet ; grep FRAMESET in console.log. +//----------------------------------------------------------------------------- + +setLogMode(2); +setScriptExecEcho(false); +trace(false); +$Scripts::ignoreDSOs = true; +setCompanyAndProduct("Torque Game Engines", "Torque2D"); +ModuleDatabase.EchoInfo = false; +AssetDatabase.EchoInfo = false; +AssetDatabase.IgnoreAutoUnload = true; + +function fsCheck(%label, %condition) +{ + echo(%condition ? ("FRAMESET PASS: " @ %label) : ("FRAMESET FAIL: " @ %label)); +} + +function fsScratch() +{ + return testRoot("shots/frameSetScratch"); +} + +function fsReadFile(%file) +{ + %fo = new FileObject(); + if(!%fo.openForRead(%file)) + { + %fo.delete(); + return ""; + } + + %text = ""; + while(!%fo.isEOF()) + { + %text = %text @ %fo.readLine() @ " "; + } + %fo.close(); + %fo.delete(); + + return %text; +} + +// A frame layout with the control ids taken out. Every eighth value is the id of +// the control standing in that frame, which is a different number in a Gui that +// has been read back off disk - so it is the only part of the layout that cannot +// be compared, and the only part that says nothing about the tree's shape. +function fsShape(%layout) +{ + %out = ""; + %count = getWordCount(%layout); + for(%i = 0; %i < %count; %i++) + { + if((%i % 8) == 7) + { + continue; + } + %out = (%out $= "") ? getWord(%layout, %i) : (%out SPC getWord(%layout, %i)); + } + + return %out; +} + +testExec("editor/main.cs"); +schedule(2000, 0, "fsStep1"); + +function fsStep1() +{ + createPath(fsScratch() @ "/"); + + %frames = new GuiFrameSetCtrl() + { + Extent = "400 200"; + }; + + // One split, and a control in each half. A frame takes the control as it + // arrives (onChildAdded fills an empty frame; it never makes one), so the + // split has to come first. + %ids = %frames.createHorizontalSplit(1); + %frames.setFrameSize(getWord(%ids, 0), 150); + + %left = new GuiButtonCtrl() { Text = "Left"; }; + %frames.add(%left); + %right = new GuiButtonCtrl() { Text = "Right"; }; + %frames.add(%right); + + %before = %frames.getFrameLayout(); + fsCheck("a split frame set is three frames (" @ getWordCount(%before) @ " values)", + getWordCount(%before) == 24); + fsCheck("both controls are in it", %frames.getCount() == 2); + + %file = pathConcat(fsScratch(), "frames.gui.taml"); + TAMLWrite(%frames, %file); + + // Lower-cased, because which capitalisation an attribute is written in is not + // the writer's to decide - StringTable hands back the first spelling of a name + // it was ever given. + %text = strlwr(fsReadFile(%file)); + fsCheck("the file carries a Frames section", strstr(%text, "guiframesetctrl.frames") != -1); + fsCheck("with frame nodes in it", strstr(%text, " Date: Sun, 2 Aug 2026 00:44:00 -0400 Subject: [PATCH 22/55] Gui Editor: one caret, on the line the return made Pressing return in a multi-line text box left two carets blinking: one at the end of the line just left, one at the start of the new line. The condition deciding which line block draws the caret suppressed it at a line's end unless the caret sat at the end of the whole text -- a sound proxy for "there is no next line to hand it to" only while getLineList built its paragraphs with getline, which drops the empty paragraph a trailing return makes. 7b351186 fixed that drop, so an empty last line now starts at the end of the text too, and both blocks answered yes. Nothing local to a line can tell it that it is the last one: a line can end at the end of the text without being last, which is exactly what a trailing return makes. So renderLineList says which one is, and the decision moved out of renderIbeam into isIbeamOnLine, which draws nothing and can therefore be tested. Three more defects turned up on the way, each found by a test: - insertNewLine never cleared the end-of-line flag, so a click at the end of a wrapped line followed by return drew the caret on the line just left. - GuiTextEditSelection's constructor set ten of its twelve members. mTextLength is the bound every caret move clamps against, so the caret landed wherever the leftover memory allowed -- 99 in a three-character box in one test, 1 in another. The copy constructor had always covered all twelve, which is what made the omission visible. - Initializing it alone would have made things worse rather than better. setText is the one path that changes the text with no keystroke behind it -- it is how a control loaded from TAML gets its text -- and it never passed the new length on, so a fixed zero would have clamped every setCursorPos to 0. It says so now. guiTextEditTests.cc holds twenty tests: caret ownership at every seam, an empty box, wrap boundaries either side of the end-of-line flag, both return cases, blink and focus, the length clamp, and the paragraph split. Only half of getLineList needs a font, and measuring text is the one thing a C++ unit test here cannot do -- a font registers a texture, and that suite runs with no canvas to make one in. splitParagraphs is that half separated out: pure, and the half that has broken. Its five tests were checked against the pre-7b351186 implementation and each failed for its own reason. The wrapping half stays with tests/smoke/textEdit.cs, which has a real canvas. 119 unit tests and all 33 script suites pass. tooltipProfile failed once along the way and is the known posted-hover flake: it passes alone, and in a full re-run on the same binary. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qi2pZ9UzF5rsLCNVErpzwt --- cmake/EngineSources.cmake | 1 + engine/source/gui/guiControl.cc | 62 ++- engine/source/gui/guiControl.h | 4 + engine/source/gui/guiTextEditCtrl.cc | 57 +- engine/source/gui/guiTextEditCtrl.h | 5 +- .../source/testing/tests/guiTextEditTests.cc | 516 ++++++++++++++++++ 6 files changed, 609 insertions(+), 36 deletions(-) create mode 100644 engine/source/testing/tests/guiTextEditTests.cc diff --git a/cmake/EngineSources.cmake b/cmake/EngineSources.cmake index 6881bfd59..54765d4f3 100644 --- a/cmake/EngineSources.cmake +++ b/cmake/EngineSources.cmake @@ -339,6 +339,7 @@ set(TORQUE_ENGINE_SOURCES ${TORQUE_SRC}/testing/unitTesting.cc # ---- testing/tests ---- ${TORQUE_SRC}/testing/tests/guiProfileThemeTests.cc + ${TORQUE_SRC}/testing/tests/guiTextEditTests.cc ${TORQUE_SRC}/testing/tests/platformFileIoTests.cc ${TORQUE_SRC}/testing/tests/platformMemoryTests.cc ${TORQUE_SRC}/testing/tests/platformStringTests.cc diff --git a/engine/source/gui/guiControl.cc b/engine/source/gui/guiControl.cc index 1ebf02a48..4b58d9e6a 100755 --- a/engine/source/gui/guiControl.cc +++ b/engine/source/gui/guiControl.cc @@ -2448,6 +2448,41 @@ void GuiControl::renderLineList(const Point2I& offset, const Point2I& extent, co } } +// Split on newlines by hand rather than with getline, which cannot tell an +// empty string from no string: it fails immediately on "", so an empty control +// produced NO lines at all. A line block is what draws a GuiTextEditCtrl's +// caret, which is why an empty multi-line box had no cursor in it. getline also +// drops the empty paragraph a trailing newline creates, so pressing return at +// the end of the text left the caret with no line to sit on. +// +// The newline stays on the end of its paragraph, for the same reason the +// wrapping in getLineList re-appends the space it consumed: GuiTextEditCtrl +// finds the character offset of a line by summing the lengths of the lines +// above it (renderLineList), so a character dropped here moves the caret. +// Nothing draws or measures it -- dglDrawText and GFont::getStrWidth both skip +// a line break, which GFont::isValidChar answers false for. +// +// Kept apart from the wrapping below because it needs no font: measuring text +// loads one, and a font registers a texture, which cannot be done in the C++ +// unit tests -- they run with no canvas. See guiTextEditTests.cc. +vector GuiControl::splitParagraphs(const char* text) +{ + vector paragraphList = vector(); + string paragraphBuffer; + for (const char* c = text; *c != '\0'; c++) + { + paragraphBuffer += *c; + if (*c == '\n') + { + paragraphList.push_back(paragraphBuffer); + paragraphBuffer.clear(); + } + } + paragraphList.push_back(paragraphBuffer); + + return paragraphList; +} + vector GuiControl::getLineList(const char* text, GuiControlProfile* profile, S32 totalWidth) { GFont* font = profile->getFont(mFontSizeAdjust); @@ -2459,32 +2494,7 @@ vector GuiControl::getLineList(const char* text, GuiControlProfile* prof } else { - // Split on newlines by hand rather than with getline, which cannot tell - // an empty string from no string: it fails immediately on "", so an - // empty control produced NO lines at all. A line block is what draws a - // GuiTextEditCtrl's caret, which is why an empty multi-line box had no - // cursor in it. getline also drops the empty paragraph a trailing - // newline creates, so pressing return at the end of the text left the - // caret with no line to sit on. - // - // The newline stays on the end of its paragraph, for the same reason the - // wrapping below re-appends the space it consumed: GuiTextEditCtrl finds - // the character offset of a line by summing the lengths of the lines - // above it (renderLineList), so a character dropped here moves the - // caret. Nothing draws or measures it -- both dglDrawText and - // GFont::getStrWidth skip a character the font has no glyph for. - vector paragraphList = vector(); - string paragraphBuffer; - for (const char* c = text; *c != '\0'; c++) - { - paragraphBuffer += *c; - if (*c == '\n') - { - paragraphList.push_back(paragraphBuffer); - paragraphBuffer.clear(); - } - } - paragraphList.push_back(paragraphBuffer); + vector paragraphList = splitParagraphs(text); for (string& paragraph : paragraphList) { diff --git a/engine/source/gui/guiControl.h b/engine/source/gui/guiControl.h index bc54d857e..ce351b42f 100755 --- a/engine/source/gui/guiControl.h +++ b/engine/source/gui/guiControl.h @@ -842,6 +842,10 @@ class GuiControl : public SimGroup, public virtual Tickable /// @note This should move into the graphics library at some point void renderText(const Point2I &offset, const Point2I &extent, const char *text, GuiControlProfile *profile, TextRotationOptions rot = tRotateNone); virtual void renderLineList(const Point2I& offset, const Point2I& extent, const S32 startOffsetY, const vector lineList, GuiControlProfile* profile, const TextRotationOptions rot = tRotateNone); + /// Splits text into paragraphs on its line breaks. The measuring half of + /// getLineList needs a font; this half does not, which is what lets it be + /// tested on its own. + static vector splitParagraphs(const char* text); virtual vector getLineList(const char* text, GuiControlProfile* profile, S32 totalWidth); virtual void renderTextLine(const Point2I& startPoint, const string line, GuiControlProfile* profile, F32 rotationInDegrees, U32 ibeamPosAtLineStart, U32 lineNumber); diff --git a/engine/source/gui/guiTextEditCtrl.cc b/engine/source/gui/guiTextEditCtrl.cc index c3c3a864f..1fe9bd11c 100755 --- a/engine/source/gui/guiTextEditCtrl.cc +++ b/engine/source/gui/guiTextEditCtrl.cc @@ -45,7 +45,7 @@ GuiTextEditTextBlock::GuiTextEditTextBlock() mLineStartIbeamValue = 0; } -void GuiTextEditTextBlock::render(const RectI& bounds, string line, U32 ibeamStartValue, GuiControlProfile* profile, GuiControlState currentState, GuiTextEditSelection& selector, AlignmentType align, GFont* font, bool overrideFontColor) +void GuiTextEditTextBlock::render(const RectI& bounds, string line, U32 ibeamStartValue, GuiControlProfile* profile, GuiControlState currentState, GuiTextEditSelection& selector, AlignmentType align, GFont* font, bool isLastLine, bool overrideFontColor) { mGlobalBounds.set(bounds.point, bounds.extent); mText.assign(line); @@ -69,7 +69,7 @@ void GuiTextEditTextBlock::render(const RectI& bounds, string line, U32 ibeamSta } Point2I textStartPoint = getGlobalTextStart(); - if (selector.renderIbeam(textStartPoint, mGlobalBounds.extent, line, mLineStartIbeamValue, mLineStartIbeamValue + line.length(), profile, font)) + if (selector.renderIbeam(textStartPoint, mGlobalBounds.extent, line, mLineStartIbeamValue, mLineStartIbeamValue + line.length(), isLastLine, profile, font)) { Point2I cursorCenter = selector.getCursorCenter(); performScrollJumpX(cursorCenter.x, clipRect.point.x, clipRect.point.x + clipRect.extent.x); @@ -173,33 +173,60 @@ void GuiTextEditTextBlock::processTextAlignment(const string line, GFont* font, #pragma endregion #pragma region GuiTextEditSelection +// In declaration order, and every member once, so that a missing one shows. +// mBlockAnchor and mTextLength used to be missing, and mTextLength is the one +// every caret move clamps against: a caret could be placed anywhere at all in a +// box whose text had never been typed or clicked into, because the bound it was +// checked against was whatever had last used the memory. GuiTextEditSelection::GuiTextEditSelection() { + mBlockAnchor = 0; mBlockStart = 0; mBlockEnd = 0; mCursorPos = 0; - mCursorOn = false; - mNumFramesElapsed = 0; mCursorAtEOL = false; mIsFirstResponder = false; mGlobalUnadjustedCursorRect.set(0, 0, 0, 0); mCursorRendered = false; + mTextLength = 0; mNumFramesElapsed = 0; mTimeLastCursorFlipped = 0; mCursorOn = false; } -bool GuiTextEditSelection::renderIbeam(const Point2I& startPoint, const Point2I& extent, const string line, const U32 start, const U32 end, GuiControlProfile* profile, GFont* font) +// Whether this line draws the caret. The caret is a position BETWEEN two +// characters, so the seam between two lines is one position with two homes -- +// the end of the line above and the start of the line below -- and mCursorAtEOL +// says which of them it is. Exactly one line may answer yes, or the box blinks +// two carets at once. +// +// A seam needs both its lines to exist. The first line is recognizable on its +// own -- only it starts at 0 -- but the last one is not, because a line can end +// at the end of the text without being last: that is precisely what a trailing +// return makes, a line ending at the text's end followed by the empty line the +// caret has just moved to. So the caller says which line is last. Reading it +// off mCursorPos instead is what drew the second caret. +bool GuiTextEditSelection::isIbeamOnLine(const U32 start, const U32 end, const bool isLastLine) const { if (!mIsFirstResponder || !mCursorOn || - (mCursorAtEOL && mCursorPos == start && mCursorPos != 0) || - (!mCursorAtEOL && mCursorPos == end && mCursorPos != mTextLength) || + (mCursorAtEOL && mCursorPos == start && start != 0) || + (!mCursorAtEOL && mCursorPos == end && !isLastLine) || (mCursorPos < start || mCursorPos > end)) { return false; } + return true; +} + +bool GuiTextEditSelection::renderIbeam(const Point2I& startPoint, const Point2I& extent, const string line, const U32 start, const U32 end, const bool isLastLine, GuiControlProfile* profile, GFont* font) +{ + if (!isIbeamOnLine(start, end, isLastLine)) + { + return false; + } + string blockText = line.substr(0, mCursorPos - start); U32 blockStrWidth = font->getStrWidth(blockText.c_str()); RectI ibeamRect = RectI(startPoint.x + blockStrWidth - 1, startPoint.y, 2, extent.y); @@ -529,6 +556,13 @@ void GuiTextEditCtrl::setText( const UTF8 *txt ) else mTextBuffer.clear(); + // Every caret move clamps to the length the selection was last told, and + // every other path that changes the text is a keystroke or a click that + // says so itself. This is the one that isn't -- it is how a control loaded + // from TAML gets its text -- so without this a box could answer setCursorPos + // with a position outside the text it is holding. + mSelector.setTextLength(mTextBuffer.length()); + setVariable(mTextBuffer.c_str()); } @@ -1086,7 +1120,7 @@ void GuiTextEditCtrl::renderLineList(const Point2I& offset, const Point2I& exten { dglSetBitmapModulation(getFontColor(profile, NormalState)); } - mTextBlockList[i].render(blockBounds, lineList[i], ibeamPos, mProfile, getCurrentState(), mSelector, getAlignmentType(), font, mOverrideFontColor); + mTextBlockList[i].render(blockBounds, lineList[i], ibeamPos, mProfile, getCurrentState(), mSelector, getAlignmentType(), font, (i == (lineList.size() - 1)), mOverrideFontColor); offsetY += textHeight; ibeamPos += lineList[i].length(); @@ -1678,6 +1712,13 @@ bool GuiTextEditCtrl::insertNewLine() mTextBuffer.insert(mSelector.getCursorPos(), "\n"); mSelector.setTextLength(mTextBuffer.length()); mSelector.stepCursorForward(); + + // The caret has just moved to the start of the new line, which is the far + // side of a seam it may have been sitting on: a click at the end of a line + // leaves the end-of-line flag set, and left set it would draw the caret on + // the line the user has just left. + mSelector.setCursorAtEOL(false); + setText(mTextBuffer); execConsoleCallback(); diff --git a/engine/source/gui/guiTextEditCtrl.h b/engine/source/gui/guiTextEditCtrl.h index 7d62f8be5..4f2d482d3 100755 --- a/engine/source/gui/guiTextEditCtrl.h +++ b/engine/source/gui/guiTextEditCtrl.h @@ -62,7 +62,8 @@ class GuiTextEditSelection void selectTo(const U32 target); inline bool hasSelection() { return mBlockEnd > mBlockStart; } void onPreRender(const U32 time); - bool renderIbeam(const Point2I& startPoint, const Point2I& extent, const string line, const U32 start, const U32 end, GuiControlProfile* profile, GFont* font); + bool isIbeamOnLine(const U32 start, const U32 end, const bool isLastLine) const; + bool renderIbeam(const Point2I& startPoint, const Point2I& extent, const string line, const U32 start, const U32 end, const bool isLastLine, GuiControlProfile* profile, GFont* font); inline string getSelection(const string& fullText) { return hasSelection() ? fullText.substr(mBlockStart, mBlockEnd - mBlockStart) : string(); } void eraseSelection(string& fullText); void stepCursorForward(); @@ -85,7 +86,7 @@ class GuiTextEditTextBlock inline const U32 getStartValue() const { return mLineStartIbeamValue; } inline RectI getGlobalBounds() const { return mGlobalBounds; } inline Point2I getGlobalTextStart() { return Point2I(mGlobalBounds.point.x + mTextOffsetX - mTextScrollX, mGlobalBounds.point.y); } - void render(const RectI& bounds, string line, U32 ibeamStartValue, GuiControlProfile* profile, GuiControlState currentState, GuiTextEditSelection& selector, AlignmentType align, GFont* font, bool overrideFontColor = false); + void render(const RectI& bounds, string line, U32 ibeamStartValue, GuiControlProfile* profile, GuiControlState currentState, GuiTextEditSelection& selector, AlignmentType align, GFont* font, bool isLastLine, bool overrideFontColor = false); U32 renderTextSection(const Point2I& startPoint, const U32 subStrStart, const U32 subStrLen, GuiControlProfile* profile, const GuiControlState currentState, GFont* font, bool isSelectedText = false, bool overrideFontColor = false); void performScrollJumpX(const S32 targetX, const S32 areaStart, const S32 areaEnd); U32 calculateIbeamPositionInLine(const S32 targetX, GFont* font); diff --git a/engine/source/testing/tests/guiTextEditTests.cc b/engine/source/testing/tests/guiTextEditTests.cc new file mode 100644 index 000000000..1a4e54857 --- /dev/null +++ b/engine/source/testing/tests/guiTextEditTests.cc @@ -0,0 +1,516 @@ +//----------------------------------------------------------------------------- +// 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. +//----------------------------------------------------------------------------- + +// We don't want tests in a shipping version. +#ifndef TORQUE_SHIPPING + +#ifndef _UNIT_TESTING_H_ +#include "testing/unitTesting.h" +#endif + +// guiTextEditCtrl.h names GuiControl as a base without including it, so the +// order here matters. +#ifndef _GUICONTROL_H_ +#include "gui/guiControl.h" +#endif + +#ifndef _GUITEXTEDITCTRL_H_ +#include "gui/guiTextEditCtrl.h" +#endif + +#ifndef _GUITYPES_H_ +#include "gui/guiTypes.h" +#endif + +//----------------------------------------------------------------------------- +// The caret in a multi-line text box, and the line list it stands on. +// +// A text box draws itself one line block at a time, and each block is asked +// whether the caret belongs to it. A caret is a position BETWEEN two +// characters, so the seam between two lines is a single position with two +// homes: the end of the line above and the start of the line below. Exactly one +// of them may draw it, or the box blinks two carets at once. +// +// That decision is GuiTextEditSelection::isIbeamOnLine, and these tests are the +// whole of it. They walk a line list the way GuiTextEditCtrl::renderLineList +// does -- a line's first caret position is the sum of the lengths of the lines +// above it -- and count how many lines claim the caret. The answer is one. +// +// The line lists below are what GuiControl::getLineList returns for the text +// each test names. The caret arithmetic here is only right for as long as the +// line list keeps that shape, and both halves have broken in turn -- see the +// note at the foot of this file for what holds the two together. +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +// A caret that is showing: the box holds the first responder and the blink is +// in its "on" half-second. Text length first -- setCursorPosition clamps to it. +static void placeCaret( GuiTextEditSelection& selector, const U32 textLength, const U32 cursorPos, const bool atEOL ) +{ + selector.setTextLength( textLength ); + selector.setCursorPosition( cursorPos ); + selector.setCursorAtEOL( atEOL ); + selector.setFirstResponder( true ); + selector.resetCursorBlink(); +} + +// Every line of the list that would draw the caret, by index. One entry means a +// working text box; two means the bug this file exists for. +static vector caretLines( const GuiTextEditSelection& selector, const vector& lineList ) +{ + vector claimed = vector(); + U32 ibeamPos = 0; + for ( U32 i = 0; i < lineList.size(); i++ ) + { + const bool isLastLine = ( i == ( lineList.size() - 1 ) ); + if ( selector.isIbeamOnLine( ibeamPos, ibeamPos + lineList[i].length(), isLastLine ) ) + { + claimed.push_back( i ); + } + ibeamPos += lineList[i].length(); + } + return claimed; +} + +static vector lines( const char* a, const char* b = NULL, const char* c = NULL ) +{ + vector lineList = vector(); + lineList.push_back( string( a ) ); + if ( b != NULL ) lineList.push_back( string( b ) ); + if ( c != NULL ) lineList.push_back( string( c ) ); + return lineList; +} + +// The whole text back out of a line list, so a test states its text once. +static U32 textLengthOf( const vector& lineList ) +{ + U32 length = 0; + for ( U32 i = 0; i < lineList.size(); i++ ) + { + length += lineList[i].length(); + } + return length; +} + +//----------------------------------------------------------------------------- +// One caret, wherever it is +//----------------------------------------------------------------------------- + +TEST( GuiTextEditTests, CaretShowsOnceAtEveryPositionInASingleLine ) +{ + const vector lineList = lines( "abc" ); + + for ( U32 pos = 0; pos <= 3; pos++ ) + { + GuiTextEditSelection selector; + placeCaret( selector, 3, pos, false ); + + const vector claimed = caretLines( selector, lineList ); + ASSERT_EQ( claimed.size(), 1 ) << "One line, so one caret, at position " << pos << "."; + ASSERT_EQ( claimed[0], 0 ); + } + + SUCCEED(); +} + +TEST( GuiTextEditTests, CaretShowsOnceInAnEmptyBox ) +{ + // Empty text is one blank line, and the caret sits at the start of it. + const vector lineList = lines( "" ); + + GuiTextEditSelection selector; + placeCaret( selector, 0, 0, false ); + + const vector claimed = caretLines( selector, lineList ); + ASSERT_EQ( claimed.size(), 1 ) << "An empty box still shows where typing would go."; + ASSERT_EQ( claimed[0], 0 ); + + SUCCEED(); +} + +TEST( GuiTextEditTests, WrapSeamGivesTheCaretToTheLineBelow ) +{ + // "hello world" wrapped: the space is kept on the line it ended. + const vector lineList = lines( "hello ", "world" ); + + GuiTextEditSelection selector; + placeCaret( selector, 11, 6, false ); + + const vector claimed = caretLines( selector, lineList ); + ASSERT_EQ( claimed.size(), 1 ) << "Position 6 is the end of one line and the start of the next."; + ASSERT_EQ( claimed[0], 1 ) << "Not at end of line, so the caret is on the lower line."; + + SUCCEED(); +} + +TEST( GuiTextEditTests, WrapSeamGivesTheCaretToTheLineAboveWhenAtEOL ) +{ + const vector lineList = lines( "hello ", "world" ); + + GuiTextEditSelection selector; + placeCaret( selector, 11, 6, true ); + + const vector claimed = caretLines( selector, lineList ); + ASSERT_EQ( claimed.size(), 1 ); + ASSERT_EQ( claimed[0], 0 ) << "At end of line, so the caret stays on the upper line."; + + SUCCEED(); +} + +TEST( GuiTextEditTests, CaretShowsOnceAtTheEndOfTheText ) +{ + const vector lineList = lines( "hello ", "world" ); + + GuiTextEditSelection selector; + placeCaret( selector, 11, 11, false ); + + const vector claimed = caretLines( selector, lineList ); + ASSERT_EQ( claimed.size(), 1 ) << "There is no line after the last one to hand it to."; + ASSERT_EQ( claimed[0], 1 ); + + SUCCEED(); +} + +//----------------------------------------------------------------------------- +// The line a return makes +//----------------------------------------------------------------------------- + +TEST( GuiTextEditTests, CaretShowsOnceOnTheEmptyLineAReturnMakes ) +{ + // "abc" and a return: the newline stays on the end of its paragraph and the + // empty paragraph after it is a line of its own. The caret is at position 4, + // which is both the end of the first line and the start of the second -- + // and the end of the whole text, which is what used to make both of them + // draw it. + const vector lineList = lines( "abc\n", "" ); + + GuiTextEditSelection selector; + placeCaret( selector, 4, 4, false ); + + const vector claimed = caretLines( selector, lineList ); + ASSERT_EQ( claimed.size(), 1 ) << "Two carets: one at the end of the old line, one on the new line."; + ASSERT_EQ( claimed[0], 1 ) << "Return moves the caret to the new line, not the end of the old one."; + + SUCCEED(); +} + +TEST( GuiTextEditTests, CaretShowsOnceAfterTwoReturns ) +{ + // The blank line in the middle is a paragraph holding nothing but its own + // newline, so it is a line with length, unlike the empty one at the end. + const vector lineList = lines( "abc\n", "\n", "" ); + + GuiTextEditSelection selector; + placeCaret( selector, 5, 5, false ); + + const vector claimed = caretLines( selector, lineList ); + ASSERT_EQ( claimed.size(), 1 ); + ASSERT_EQ( claimed[0], 2 ) << "The caret is on the last of the empty lines."; + + SUCCEED(); +} + +TEST( GuiTextEditTests, CaretShowsOnceAtEveryPositionOfTextEndingInAReturn ) +{ + // The sweep the two tests above are samples of: every caret position of + // "ab\ncd\n", either side of the end-of-line flag, is owned by one line. + const vector lineList = lines( "ab\n", "cd\n", "" ); + const U32 textLength = textLengthOf( lineList ); + + for ( U32 pos = 0; pos <= textLength; pos++ ) + { + for ( U32 eol = 0; eol <= 1; eol++ ) + { + GuiTextEditSelection selector; + placeCaret( selector, textLength, pos, ( eol == 1 ) ); + + const vector claimed = caretLines( selector, lineList ); + ASSERT_EQ( claimed.size(), 1 ) + << "Position " << pos << ( eol == 1 ? " at end of line" : "" ) << " drew " << claimed.size() << " carets."; + } + } + + SUCCEED(); +} + +//----------------------------------------------------------------------------- +// No caret at all +//----------------------------------------------------------------------------- + +TEST( GuiTextEditTests, CaretIsHiddenWhenTheBoxIsNotTheFirstResponder ) +{ + const vector lineList = lines( "abc\n", "" ); + + GuiTextEditSelection selector; + placeCaret( selector, 4, 4, false ); + selector.setFirstResponder( false ); + + ASSERT_EQ( caretLines( selector, lineList ).size(), 0 ) << "A box that is not being edited has no caret."; + + SUCCEED(); +} + +TEST( GuiTextEditTests, CaretIsHiddenWhileTheBlinkIsOff ) +{ + const vector lineList = lines( "abc\n", "" ); + + // A fresh selection has not blinked on yet, so this is the dark half of the + // blink without waiting half a second for it. + GuiTextEditSelection selector; + selector.setTextLength( 4 ); + selector.setCursorPosition( 4 ); + selector.setFirstResponder( true ); + + ASSERT_EQ( caretLines( selector, lineList ).size(), 0 ); + + SUCCEED(); +} + +//----------------------------------------------------------------------------- +// Return, from the control +//----------------------------------------------------------------------------- + +// Reaches the key handler and the caret state through the class itself rather +// than through new public methods: nothing here exists for the test's benefit. +class TestTextEditCtrl : public GuiTextEditCtrl +{ +public: + using GuiTextEditCtrl::mSelector; + using GuiTextEditCtrl::insertNewLine; +}; + +TEST( GuiTextEditTests, ReturnPutsTheCaretOnTheNewLine ) +{ + TestTextEditCtrl* box = new TestTextEditCtrl(); + box->registerObject(); + box->setTextWrap( true ); + box->setText( "hello world" ); + + // What a click at the end of a wrapped line leaves behind: the box is being + // edited, and the caret is at the seam, belonging to the line above. + box->mSelector.setTextLength( 11 ); + box->mSelector.setCursorPosition( 6 ); + box->mSelector.setCursorAtEOL( true ); + box->mSelector.setFirstResponder( true ); + box->mSelector.resetCursorBlink(); + + box->insertNewLine(); + + ASSERT_EQ( box->mSelector.getCursorPos(), 7 ) << "The caret steps over the line break it just made."; + + // "hello \n" is now a paragraph of its own, so position 7 is the seam + // between it and "world". A caret still flagged end-of-line draws at the end + // of the line above -- the line the user just left. + const vector lineList = lines( "hello \n", "world" ); + const vector claimed = caretLines( box->mSelector, lineList ); + + ASSERT_EQ( claimed.size(), 1 ); + ASSERT_EQ( claimed[0], 1 ) << "Return moves the caret onto the new line."; + + box->deleteObject(); + + SUCCEED(); +} + +TEST( GuiTextEditTests, ReturnInsertsALineBreakAtTheCaret ) +{ + TestTextEditCtrl* box = new TestTextEditCtrl(); + box->registerObject(); + box->setTextWrap( true ); + box->setText( "abcdef" ); + + box->mSelector.setTextLength( 6 ); + box->mSelector.setCursorPosition( 3 ); + + box->insertNewLine(); + + ASSERT_STREQ( box->getText(), "abc\ndef" ); + ASSERT_EQ( box->mSelector.getCursorPos(), 4 ); + + box->deleteObject(); + + SUCCEED(); +} + +//----------------------------------------------------------------------------- +// The caret and the length of the text it is in +// +// Every caret move clamps to the length of the text, so the selection has to be +// told that length by whoever changes the text. Both halves of that have been +// missing: the length started as uninitialized memory, and setText -- the one +// path that changes the text with no keystroke behind it -- never passed it on. +//----------------------------------------------------------------------------- + +TEST( GuiTextEditTests, AFreshSelectionHoldsNoText ) +{ + GuiTextEditSelection selector; + + ASSERT_EQ( selector.getCursorPos(), 0 ); + + // setCursorPosition clamps to the text length. Left uninitialized, that + // length is whatever was in the memory, so the caret lands anywhere. + selector.setCursorPosition( 5 ); + + ASSERT_EQ( selector.getCursorPos(), 0 ) << "No text, so there is nowhere for the caret to go."; + + SUCCEED(); +} + +TEST( GuiTextEditTests, TheCaretReachesTextThatWasSetRatherThanTyped ) +{ + TestTextEditCtrl* box = new TestTextEditCtrl(); + box->registerObject(); + box->setText( "abc" ); + + // What a control loaded from TAML has: text, and no keystroke or click + // behind it to have told the selection how long that text is. + box->setIbeamPosition( 2 ); + + ASSERT_EQ( box->getIbeamPosition(), 2 ) << "The caret can go anywhere inside text the box was given."; + + box->deleteObject(); + + SUCCEED(); +} + +TEST( GuiTextEditTests, TheCaretCannotBePlacedPastTheEndOfTheText ) +{ + TestTextEditCtrl* box = new TestTextEditCtrl(); + box->registerObject(); + box->setText( "abc" ); + + box->setIbeamPosition( 99 ); + + ASSERT_EQ( box->getIbeamPosition(), 3 ) << "Past the end is the end."; + + box->deleteObject(); + + SUCCEED(); +} + +TEST( GuiTextEditTests, ClearingTheTextBringsTheCaretBack ) +{ + TestTextEditCtrl* box = new TestTextEditCtrl(); + box->registerObject(); + box->setText( "abc" ); + box->setIbeamPosition( 3 ); + + box->setText( "" ); + + ASSERT_EQ( box->getIbeamPosition(), 0 ) << "The text it pointed into is gone."; + + box->deleteObject(); + + SUCCEED(); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +// The line list the caret arithmetic rests on +// +// GuiControl::getLineList is two jobs: splitting text into paragraphs on its +// line breaks, and wrapping each of those to a width. Only the second needs a +// font, so only the second is out of reach here -- measuring text loads a font, +// a font registers a texture, and TextureManager::refresh asserts because this +// suite runs with no canvas and so no GL context to make one in. +// +// The split is the half the caret cares about, and the half that has broken: +// every line list written out by hand above is what it returns. +//----------------------------------------------------------------------------- + +TEST( GuiTextEditTests, EmptyTextIsOneParagraph ) +{ + const vector paragraphs = GuiControl::splitParagraphs( "" ); + + ASSERT_EQ( paragraphs.size(), 1 ) << "No paragraph means no line block, and a line block is what draws the caret."; + ASSERT_EQ( paragraphs[0].length(), 0 ); + + SUCCEED(); +} + +TEST( GuiTextEditTests, ALineBreakStaysOnTheEndOfItsParagraph ) +{ + const vector paragraphs = GuiControl::splitParagraphs( "ab\ncd" ); + + ASSERT_EQ( paragraphs.size(), 2 ); + ASSERT_STREQ( paragraphs[0].c_str(), "ab\n" ) << "Dropping the break here moves the caret on every line below it."; + ASSERT_STREQ( paragraphs[1].c_str(), "cd" ); + + SUCCEED(); +} + +TEST( GuiTextEditTests, ATrailingReturnMakesAnEmptyLastParagraph ) +{ + const vector paragraphs = GuiControl::splitParagraphs( "abc\n" ); + + ASSERT_EQ( paragraphs.size(), 2 ) << "The caret has to have a line to sit on after a return."; + ASSERT_STREQ( paragraphs[0].c_str(), "abc\n" ); + ASSERT_EQ( paragraphs[1].length(), 0 ); + + SUCCEED(); +} + +TEST( GuiTextEditTests, ABlankLineBetweenTwoParagraphsIsKept ) +{ + const vector paragraphs = GuiControl::splitParagraphs( "a\n\nb" ); + + ASSERT_EQ( paragraphs.size(), 3 ); + ASSERT_STREQ( paragraphs[1].c_str(), "\n" ) << "A blank line is a paragraph holding nothing but its own break."; + + SUCCEED(); +} + +TEST( GuiTextEditTests, ParagraphLengthsSumToTheTextLength ) +{ + // The invariant the caret stands on: renderLineList finds a line's first + // caret position by summing the lengths of the lines above it, so a + // character dropped or invented here moves the caret. + const char* texts[] = { "", "abc", "abc\n", "abc\n\n", "\n", "ab\ncd\n", "hello world" }; + + for ( U32 i = 0; i < ( sizeof( texts ) / sizeof( texts[0] ) ); i++ ) + { + const vector paragraphs = GuiControl::splitParagraphs( texts[i] ); + + U32 sum = 0; + for ( U32 p = 0; p < paragraphs.size(); p++ ) + { + sum += paragraphs[p].length(); + } + + ASSERT_EQ( sum, dStrlen( texts[i] ) ) + << "The paragraphs of '" << texts[i] << "' hold " << sum << " characters, not " << dStrlen( texts[i] ) << "."; + } + + SUCCEED(); +} + +//----------------------------------------------------------------------------- +// The wrapping half is covered by tests/smoke/textEdit.cs, which runs in a real +// engine with a real canvas: it reads the line count off a control that sizes +// itself to its text. +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +#endif // TORQUE_SHIPPING From b3c4d43b059fc8ddabbc02d3d6ad7476e7f93d2f Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Sun, 2 Aug 2026 01:06:40 -0400 Subject: [PATCH 23/55] Gui Editor: an icon takes its color from the theme it is worn in The control palette's pictures are greyscale art, drawn to be modulated. The tiles never modulated them: nothing called setImageColor, so every icon kept GuiSpriteCtrl's opaque white. That looked right on the theme the editor starts in -- which draws its text white too -- and became a white smear on a pale panel the moment anyone chose the light one. The two view buttons above them had the other half of the same problem. GuiEditorToggleIcon::refresh copies a color off iconButtonProfile onto its sprite, and a copy does not follow the profile it came from. ThemeManager swaps the profile object on everything that registered one, so the button's background changed theme and the picture on it did not -- until something happened to call refresh() again, which is why the icons corrected themselves the moment you clicked one. Both now listen to ThemeManager and re-read the color when it posts, the way EditorIconButton already did. Fixing it inside the toggle repairs the anchor picker, the header block, the text block and the item rows as well: they all wear iconButtonProfile and all went stale together. The palette suite grows a section that checks both tints against the profiles they claim to come from, under the default theme and under the light one. Only the second catches anything -- under the default, white-because-themed and white-because-untouched are the same pixel, which is what hid this. Comparing colors needs care: TypeColorI reads back as a stock NAME when the components match one, so a white profile color answers "White" while the sprite it was set from answers "255 255 255 255". Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qi2pZ9UzF5rsLCNVErpzwt --- .../GuiEditor/scripts/GuiEditorControlTile.cs | 33 ++++++++ .../GuiEditor/scripts/GuiEditorToggleIcon.cs | 13 +++ tests/smoke/palette.cs | 80 +++++++++++++++++++ 3 files changed, 126 insertions(+) diff --git a/editor/GuiEditor/scripts/GuiEditorControlTile.cs b/editor/GuiEditor/scripts/GuiEditorControlTile.cs index 8bbcd7610..cd2792e09 100644 --- a/editor/GuiEditor/scripts/GuiEditorControlTile.cs +++ b/editor/GuiEditor/scripts/GuiEditorControlTile.cs @@ -68,6 +68,39 @@ %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(); } //----------------------------------------------------------------------------- diff --git a/editor/GuiEditor/scripts/GuiEditorToggleIcon.cs b/editor/GuiEditor/scripts/GuiEditorToggleIcon.cs index 8cdd41a26..47b83c7f0 100644 --- a/editor/GuiEditor/scripts/GuiEditorToggleIcon.cs +++ b/editor/GuiEditor/scripts/GuiEditorToggleIcon.cs @@ -65,10 +65,23 @@ 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 GuiEditorToggleIcon::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. diff --git a/tests/smoke/palette.cs b/tests/smoke/palette.cs index 9c06c3fe5..6b9d92d93 100644 --- a/tests/smoke/palette.cs +++ b/tests/smoke/palette.cs @@ -245,9 +245,89 @@ function palModeChecks() palCheck("a tile survives its group being collapsed", %tile.isVisible()); palCheck("the group reopened", %group.getExpanded()); + palThemeChecks(); +} + +//----------------------------------------------------------------------------- +// The editor theme. +// +// Every picture in this window is greyscale art that means nothing until it is +// modulated: on a light theme an untinted icon is a white smear on a pale +// panel. So the tint has to be set when the control is built AND set again when +// the theme changes -- and the second half is the one that goes missing, +// because it looks as though ThemeManager already does it. It does not: it +// swaps the profile OBJECT on every control that registered one, which repaints +// backgrounds and text on its own, and cannot reach a color that script has +// already copied onto a sprite. +// +// Checked against the default theme and the light one, because the default's +// text color is white -- the same color an untinted sprite draws in -- so a +// tile that is never tinted at all still looks right there and only breaks when +// someone switches. +//----------------------------------------------------------------------------- + +function palThemeChecks() +{ + %was = ThemeManager.curTheme; + + palThemeTints("as built"); + + // Lab Coat is the light theme: its text color is near-black where the + // default's is white. Assert they differ, or every check below could pass + // without a single sprite being touched. + %before = ThemeManager.activeTheme.itemSelectProfile.fontColor; + ThemeManager.setTheme(1); + %after = ThemeManager.activeTheme.itemSelectProfile.fontColor; + palCheck("the two themes really do differ (" @ %before @ " / " @ %after @ ")", + !palSameColor(%before, %after)); + + palThemeTints("after a theme change"); + + ThemeManager.setTheme(%was); + palThemeTints("after changing back"); + palDropChecks(); } +function palThemeTints(%when) +{ + %window = GuiEditor.ctrlListWindow; + %theme = ThemeManager.activeTheme; + %tile = %window.group[0].tile[0]; + + // The tile is drawn on its own profile, so that is where its picture takes + // its color from -- not from the caption's label profile, which happens to + // name the same color in every theme that ships. + palCheck("a tile tints its icon " @ %when @ " (" @ %tile.icon.getImageColor() @ ")", + palSameColor(%tile.icon.getImageColor(), %theme.itemSelectProfile.fontColor)); + + // The mode row is a radio group: one button is down and one is up, and the + // two take different colors out of the same profile. Checking both is what + // catches a refresh that only ever runs for one state. + %on = %window.modeRow.choiceButton[0]; + %off = %window.modeRow.choiceButton[1]; + palCheck("the chosen mode button tints its icon " @ %when @ " (" @ + %on.icon.getImageColor() @ ")", + palSameColor(%on.icon.getImageColor(), %theme.iconButtonProfile.fontColorHL)); + palCheck("the other mode button tints its icon " @ %when @ " (" @ + %off.icon.getImageColor() @ ")", + palSameColor(%off.icon.getImageColor(), %theme.iconButtonProfile.fontColor)); +} + +// TypeColorI reads back as a stock color NAME whenever the components match one, +// so a white profile color answers "White" while the sprite that was set from it +// answers "255 255 255 255". Comparing the two as strings fails on exactly the +// colors a theme is most likely to use. +function palSameColor(%a, %b) +{ + return palColorI(%a) $= palColorI(%b); +} + +function palColorI(%color) +{ + return isStockColor(%color) ? getStockColorI(%color) : %color; +} + //----------------------------------------------------------------------------- // The two gestures. A click is a drop that never moved, and it has to reach the // document by the same path a drag does -- that is where theming, selection and From 44e14f0f4c46ae1a2b14d0fb0166366f6dc24a30 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Sun, 2 Aug 2026 01:42:30 -0400 Subject: [PATCH 24/55] Gui Editor: fixed a few sizing issues with Torque Suit Theme --- editor/EditorCore/Themes/TorqueSuit/TorqueSuitTheme.cs | 3 +++ editor/GuiEditor/scripts/GuiEditorAnchorPicker.cs | 8 ++++---- editor/GuiEditor/scripts/GuiEditorTextBlock.cs | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/editor/EditorCore/Themes/TorqueSuit/TorqueSuitTheme.cs b/editor/EditorCore/Themes/TorqueSuit/TorqueSuitTheme.cs index dc0c2b634..24e29c3e6 100644 --- a/editor/EditorCore/Themes/TorqueSuit/TorqueSuitTheme.cs +++ b/editor/EditorCore/Themes/TorqueSuit/TorqueSuitTheme.cs @@ -61,6 +61,9 @@ paddingHL = 2; paddingSL = 2; paddingNA = 2; + + borderColorSL = %this.color5; + borderSL = 2; }; %this.iconButtonProfile = new GuiControlProfile() diff --git a/editor/GuiEditor/scripts/GuiEditorAnchorPicker.cs b/editor/GuiEditor/scripts/GuiEditorAnchorPicker.cs index 8d095ec98..f51b590a7 100644 --- a/editor/GuiEditor/scripts/GuiEditorAnchorPicker.cs +++ b/editor/GuiEditor/scripts/GuiEditorAnchorPicker.cs @@ -65,13 +65,13 @@ %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, 72, "h", "scale", "Scale", + %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, 72, "v", "scale", "Scale", + %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 @@ -125,12 +125,12 @@ class = "GuiEditorToggleIcon"; %chip = new GuiCheckBoxCtrl() { Position = %x SPC %y; - Extent = %w SPC 20; + Extent = %w SPC 22; Text = %text; boxOffset = "0 1"; boxExtent = "16 16"; textOffset = "19 1"; - textExtent = (%w - 19) SPC 16; + textExtent = (%w - 19) SPC 21; Tooltip = %tip; Command = %this.getID() @ ".onChipClicked(\"" @ %axis @ "\",\"" @ %mode @ "\");"; }; diff --git a/editor/GuiEditor/scripts/GuiEditorTextBlock.cs b/editor/GuiEditor/scripts/GuiEditorTextBlock.cs index fe1237bfa..799e3a973 100644 --- a/editor/GuiEditor/scripts/GuiEditorTextBlock.cs +++ b/editor/GuiEditor/scripts/GuiEditorTextBlock.cs @@ -232,7 +232,7 @@ class = "GuiEditorToggleIcon"; class = "GuiEditorChoiceRow"; Position = "0 0"; labelText = %label; - labelWidth = 20; + labelWidth = 24; fieldName = %field; owner = %this; }; From 089e4cbaf941b670396fc882a70ee3df767e4f6f Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Sun, 2 Aug 2026 16:09:52 -0400 Subject: [PATCH 25/55] Gui Editor: a control tile shows its name under the picture Grid mode drew an 80px icon and hid the caption, so the only way to learn what a tile was, was to hover it for the tooltip. The icon comes down to 56 and the name sits under it -- centered, on the floor of the tile, wrapping to a second line -- so "Check Box" and "Radio Button" read level across a row instead of stepping up and down. The caption fills the tile and bottom-aligns its text rather than being a band placed by arithmetic. A tile's profile insets 3 pixels a side on the base theme and 4 on Torque Suit, a number script cannot ask for, and a band positioned against the outer extent hangs below the inner rect, where renderChild clips the last line's descenders off. Filling also measures that inset, so the picture is centered in whatever room is actually left above the name whatever a theme's border costs. Both view modes share the one caption, so each branch now sets every property the other touches; a switch back stops being a switch back otherwise. palette.cs covers the caption, its alignment and its wrap. The shots harness gains a fourth shot, scrolled to the two-line names -- every name in Basics fits on one line, so the first three could not show what the caption band is sized for. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qi2pZ9UzF5rsLCNVErpzwt --- .../scripts/GuiEditorControlGroup.cs | 20 ++++-- .../GuiEditor/scripts/GuiEditorControlTile.cs | 72 ++++++++++++++++--- tests/shots/controlPalette.cs | 46 +++++++++--- tests/smoke/palette.cs | 41 +++++++++-- 4 files changed, 152 insertions(+), 27 deletions(-) diff --git a/editor/GuiEditor/scripts/GuiEditorControlGroup.cs b/editor/GuiEditor/scripts/GuiEditorControlGroup.cs index 14761c1d7..6bbcfa08a 100644 --- a/editor/GuiEditor/scripts/GuiEditorControlGroup.cs +++ b/editor/GuiEditor/scripts/GuiEditorControlGroup.cs @@ -25,7 +25,19 @@ // 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; @@ -41,11 +53,11 @@ // 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 100-pixel tile in a 40-pixel row. + // child, which in row mode means a 116-pixel tile in a 40-pixel row. CellModeX = "variable"; CellModeY = "absolute"; CellSizeX = $GuiEditorControlGroup::gridCell; - CellSizeY = $GuiEditorControlGroup::gridCell; + CellSizeY = $GuiEditorControlGroup::gridCellHeight; CellSpacingX = 4; CellSpacingY = 4; MaxColCount = 0; @@ -69,7 +81,7 @@ class = "GuiEditorControlTile"; HorizSizing = "width"; VertSizing = "height"; Position = "0 0"; - Extent = $GuiEditorControlGroup::gridCell SPC $GuiEditorControlGroup::gridCell; + Extent = $GuiEditorControlGroup::gridCell SPC $GuiEditorControlGroup::gridCellHeight; Text = ""; key = %key; owner = %this; @@ -101,7 +113,7 @@ class = "GuiEditorControlTile"; else { %this.grid.CellSizeX = $GuiEditorControlGroup::gridCell; - %this.grid.CellSizeY = $GuiEditorControlGroup::gridCell; + %this.grid.CellSizeY = $GuiEditorControlGroup::gridCellHeight; } // The grid lays out on resize, so nudge it before the tiles read their own diff --git a/editor/GuiEditor/scripts/GuiEditorControlTile.cs b/editor/GuiEditor/scripts/GuiEditorControlTile.cs index cd2792e09..581ad51a0 100644 --- a/editor/GuiEditor/scripts/GuiEditorControlTile.cs +++ b/editor/GuiEditor/scripts/GuiEditorControlTile.cs @@ -13,18 +13,33 @@ // carry sizing flags that keep them placed, so dragging the frame narrower // reflows the grid and the tiles follow without script. // -// grid an 80px picture centred in the cell, no caption, name in the tooltip +// 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. //----------------------------------------------------------------------------- -// Grid mode wants the 128 sheet (sheetFor shrinks it, which stays sharp); rows -// mode is under the small sheet's own resolution and takes it 1:1. -$GuiEditorControlTile::gridArt = 80; +// 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. @@ -39,8 +54,8 @@ // 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 -- and in grid - // mode, where there is no caption at all, the tooltip is the only name. + // 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() @@ -113,6 +128,9 @@ %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; @@ -129,18 +147,52 @@ %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 = "center"; + %this.icon.VertSizing = "anchorTop"; %this.icon.setExtent(%art, %art); - %this.icon.setPosition((%w - %art) / 2, (%h - %art) / 2); - - %this.caption.setVisible(false); + %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, diff --git a/tests/shots/controlPalette.cs b/tests/shots/controlPalette.cs index 5e79f3bc9..7d33d9435 100644 --- a/tests/shots/controlPalette.cs +++ b/tests/shots/controlPalette.cs @@ -1,13 +1,16 @@ -// Visual harness for the Gui Editor's control palette. Three shots: +// Visual harness for the Gui Editor's control palette. Four shots: // -// 0 grid mode, every group open -- the default view -// 1 row mode, every group open -- icon plus human-readable name +// 0 grid mode, every group open -- a picture over its name, the default view +// 1 row mode, every group open -- a small picture with the name beside it // 2 grid mode with two groups collapsed +// 3 grid mode scrolled to the two-line names // -// The third is not decoration. GuiExpandCtrl force-writes mVisible on every -// direct child of a panel whenever it expands or collapses, which is why the -// tiles live in an inner grid; collapsing and reopening is what proves the tiles -// survived it. +// Neither of the last two is decoration. GuiExpandCtrl force-writes mVisible on +// every direct child of a panel whenever it expands or collapses, which is why +// the tiles live in an inner grid; collapsing and reopening is what proves the +// tiles survived it. And every name in Basics fits on one line, so only the +// fourth shot shows what the caption band is sized for -- a name that wraps, +// stacking upward off the floor of the tile without reaching the picture. // // Run: tests/run.ps1 -Shots controlPalette ; look in shots/. @@ -75,13 +78,38 @@ function pFinish() { pGrab(2); - // Reopen what was closed, so the last thing the shot proves is that a tile - // survives a collapse rather than being left hidden by the expand control. + // Reopen what was closed, so the shot has proved that a tile survives a + // collapse rather than being left hidden by the expand control. %window = GuiEditor.ctrlListWindow; %window.group[0].setExpanded(true); %window.group[2].setExpanded(true); %window.relayout(); + schedule(800, 0, "pWrapShot"); +} + +// The long names live in Input & Data -- "Radio Button", "Image Button" -- which +// is the third group and so below the fold. Shutting the two above it is what +// brings it to the top; scrolling would depend on how tall the window happens to +// be on the machine running this. +function pWrapShot() +{ + %window = GuiEditor.ctrlListWindow; + %window.group[0].setExpanded(false); + %window.group[1].setExpanded(false); + %window.relayout(); + schedule(800, 0, "pWrapGrab"); +} + +function pWrapGrab() +{ + pGrab(3); + + %window = GuiEditor.ctrlListWindow; + %window.group[0].setExpanded(true); + %window.group[1].setExpanded(true); + %window.relayout(); + echo("SHOTS DONE"); schedule(500, 0, "quit"); } diff --git a/tests/smoke/palette.cs b/tests/smoke/palette.cs index 6b9d92d93..1be0a3452 100644 --- a/tests/smoke/palette.cs +++ b/tests/smoke/palette.cs @@ -214,10 +214,35 @@ function palModeChecks() %group = %window.group[0]; %tile = %group.tile[0]; - palCheck("grid mode hides the caption", !%tile.caption.isVisible()); - palCheck("grid mode draws 80px art (" @ getWord(%tile.icon.getExtent(), 0) @ ")", + palCheck("grid mode shows the caption", %tile.caption.isVisible()); + palCheck("grid mode names the control, not the class", + %tile.caption.getText() $= GuiEditor.controlIcons.labelFor(%tile.key)); + palCheck("grid mode centers the caption and sits it on the floor", + %tile.caption.align $= "center" && %tile.caption.vAlign $= "bottom"); + palCheck("grid mode wraps a long name onto a second line", %tile.caption.textWrap); + + // The caption fills the tile's INNER rect -- that is both how the text finds + // the floor and how the tile measures a border it cannot ask the theme for. + // So it starts at the origin and is strictly smaller than the outer extent. + palCheck("the grid caption fills the tile", %tile.caption.getPosition() $= "0 0"); + %innerH = getWord(%tile.caption.getExtent(), 1); + palCheck("the caption measures the inner rect, not the outer (" @ %innerH @ + " inside " @ getWord(%tile.getExtent(), 1) @ ")", + %innerH < getWord(%tile.getExtent(), 1)); + + // The picture has to leave the band clear, or the second line of a long name + // draws over it. This is the assertion the cell height exists to satisfy. + %iconBottom = getWord(%tile.icon.getPosition(), 1) + getWord(%tile.icon.getExtent(), 1); + palCheck("the icon clears the caption band (" @ %iconBottom @ " vs " @ + (%innerH - $GuiEditorControlTile::gridCaption) @ ")", + %iconBottom <= (%innerH - $GuiEditorControlTile::gridCaption)); + palCheck("the icon is not pushed off the top of the tile", + getWord(%tile.icon.getPosition(), 1) >= 0); + palCheck("grid mode draws 56px art (" @ getWord(%tile.icon.getExtent(), 0) @ ")", getWord(%tile.icon.getExtent(), 0) == $GuiEditorControlTile::gridArt); - palCheck("grid mode takes the 128 sheet", %tile.icon.Image $= "GuiEditor:controlIcons128"); + palCheck("grid mode takes the 64 sheet", %tile.icon.Image $= "GuiEditor:controlIcons64"); + palCheck("the grid cell is tall enough for the picture and the band", + %group.grid.CellSizeY == $GuiEditorControlGroup::gridCellHeight); %window.setMode("rows"); palCheck("row mode shows the caption", %tile.caption.isVisible()); @@ -225,6 +250,12 @@ function palModeChecks() %tile.caption.getText() $= GuiEditor.controlIcons.labelFor(%tile.key)); palCheck("the tooltip still names the class", %tile.tooltip $= GuiEditor.controlIcons.classFor(%tile.key)); + + // The two modes share one caption, so each has to put back what the other + // wrote. This is the half that would rot silently. + palCheck("row mode puts the caption back beside the icon", + %tile.caption.align $= "left" && %tile.caption.vAlign $= "middle"); + palCheck("row mode stops wrapping", !%tile.caption.textWrap); palCheck("row mode draws 32px art (" @ getWord(%tile.icon.getExtent(), 0) @ ")", getWord(%tile.icon.getExtent(), 0) == $GuiEditorControlTile::rowArt); palCheck("row mode takes the 64 sheet", %tile.icon.Image $= "GuiEditor:controlIcons64"); @@ -234,7 +265,9 @@ function palModeChecks() %window.setMode("grid"); palCheck("switching back restores the grid cell", %group.grid.CellSizeX == $GuiEditorControlGroup::gridCell); - palCheck("switching back hides the caption again", !%tile.caption.isVisible()); + palCheck("switching back centers the caption again", + %tile.caption.isVisible() && %tile.caption.align $= "center" && + %tile.caption.vAlign $= "bottom" && %tile.caption.textWrap); // Collapsing force-writes mVisible on the panel's direct children. The tiles // are grandchildren, so they must come back. From e71a846fc80c9ed5b0f4eac1ed08e3356a216cc5 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Sun, 2 Aug 2026 16:10:32 -0400 Subject: [PATCH 26/55] Gui: a scroll control leaves room for its own scroll bar GuiScrollCtrl subtracted its bar only where it CLIPPED children -- applyScrollBarSpacing, from onRender -- and nowhere else, so a content child kept its full extent and was simply drawn cut off. That is right for a scroller, whose content is meant to be free to exceed it, and wrong for content that is supposed to fit ACROSS: it sized itself from a width that included the bar. The Gui Editor control palette laid out one grid column underneath the bar, and the names in that column read "Number Bo:". The rule this now follows: an axis whose bar is alwaysOff does not scroll, so the space is bounded and the control has a real size to hand a child. An axis that is alwaysOn or dynamic is a window onto content of any length -- there is no size to give, and filling there would clamp the content and leave nothing to scroll. - getInnerRect subtracts whatever bars are showing, and onRender uses it, so the rect children are sized against and the rect they are clipped into are one definition rather than two that could drift apart. - addObject strips fill and center only in an axis that can scroll, via preventUnsizedModes. GuiControl gained per-axis variants of the two prevent calls; the existing both-axis ones now delegate to them. - computeSizes announces a bar appearing or going away, with old and new content extents measured from the same bar-free rect so the difference is the bars and nothing else. Nothing announced it before: the bar usually arrives from childResized, nowhere near a resize, and the code in resize() passed identical old and new, so every sizing mode that works from a delta computed zero and did nothing. The mRenderInsetRB it poked was overwritten by the next renderChild anyway. - calcBarPresence judges each axis against the room that REMAINS, so a vertical bar can call a horizontal one into being. The old code compared both against the un-narrowed extent, so its second look at the horizontal bar asked a question it had already answered. The bar arithmetic is two static helpers, subtractScrollBars and calcBarPresence, so it can be tested away from a canvas, a Sim and a GL context -- guiScrollLayoutTests.cc, 13 tests including the circular case. The palette's script workarounds go with it: the chain asks to fill and the engine answers, including as the frame is dragged and as the bar comes and goes. Its smoke test now sweeps the window across 31 widths instead of asserting once, because a single static check is exactly what the script workarounds passed while still breaking on the next drag. Note for anyone reaching for fill next: a GuiPanelCtrl cannot be filled. GuiExpandCtrl::parentResized ends by writing mExpandedExtent straight into mBounds.extent, and a direct write goes around resize, which is the only thing that honours fill. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qi2pZ9UzF5rsLCNVErpzwt --- cmake/EngineSources.cmake | 1 + .../scripts/GuiEditorControlListWindow.cs | 32 ++- engine/source/gui/containers/guiScrollCtrl.cc | 212 +++++++++++---- engine/source/gui/containers/guiScrollCtrl.h | 45 ++++ engine/source/gui/guiControl.cc | 22 +- engine/source/gui/guiControl.h | 11 + .../testing/tests/guiScrollLayoutTests.cc | 244 ++++++++++++++++++ tests/smoke/palette.cs | 88 +++++++ 8 files changed, 596 insertions(+), 59 deletions(-) create mode 100644 engine/source/testing/tests/guiScrollLayoutTests.cc diff --git a/cmake/EngineSources.cmake b/cmake/EngineSources.cmake index 54765d4f3..f4caefbdc 100644 --- a/cmake/EngineSources.cmake +++ b/cmake/EngineSources.cmake @@ -339,6 +339,7 @@ set(TORQUE_ENGINE_SOURCES ${TORQUE_SRC}/testing/unitTesting.cc # ---- testing/tests ---- ${TORQUE_SRC}/testing/tests/guiProfileThemeTests.cc + ${TORQUE_SRC}/testing/tests/guiScrollLayoutTests.cc ${TORQUE_SRC}/testing/tests/guiTextEditTests.cc ${TORQUE_SRC}/testing/tests/platformFileIoTests.cc ${TORQUE_SRC}/testing/tests/platformMemoryTests.cc diff --git a/editor/GuiEditor/scripts/GuiEditorControlListWindow.cs b/editor/GuiEditor/scripts/GuiEditorControlListWindow.cs index 5bb90c99f..a9a3bab38 100644 --- a/editor/GuiEditor/scripts/GuiEditorControlListWindow.cs +++ b/editor/GuiEditor/scripts/GuiEditorControlListWindow.cs @@ -43,11 +43,19 @@ %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 = "width"; + HorizSizing = "fill"; Position = "0 0"; - Extent = "242 4"; + Extent = "228 4"; IsVertical = true; ChildSpacing = 2; }; @@ -126,8 +134,8 @@ class = "GuiEditorChoiceRow"; }; %this.add(%this.modeRow); - %this.modeRow.addChoice("grid", $EditorIcon::grid_2x2, "Show controls as a grid of icons"); - %this.modeRow.addChoice("rows", $EditorIcon::list_bullets, "Show controls as rows with names"); + %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); } @@ -153,6 +161,15 @@ class = "GuiEditorChoiceRow"; // 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++) @@ -207,6 +224,13 @@ class = "GuiEditorChoiceRow"; %group = new GuiPanelCtrl() { class = "GuiEditorControlGroup"; + + // 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; diff --git a/engine/source/gui/containers/guiScrollCtrl.cc b/engine/source/gui/containers/guiScrollCtrl.cc index e4679bda1..b17adf1d5 100755 --- a/engine/source/gui/containers/guiScrollCtrl.cc +++ b/engine/source/gui/containers/guiScrollCtrl.cc @@ -80,6 +80,7 @@ GuiScrollCtrl::GuiScrollCtrl() mEventBubbled = false; mCalcGuard = false; mResizeGuard = false; + mNotifyGuard = false; } void GuiScrollCtrl::initPersistFields() @@ -110,21 +111,18 @@ void GuiScrollCtrl::resize(const Point2I &newPos, const Point2I &newExt) mCalcGuard = false; computeSizes(); + // The bar appearing or going away is now announced by computeSizes, which + // is where it is actually decided -- and which catches the far more + // common case of a bar arriving because a child grew, nowhere near a + // resize of this control. All that is left here is to settle the + // rectangles against the size the children have just been given. + // + // What stood here instead adjusted each child's mRenderInsetRB and then + // called parentResized with the same extent for old AND new, so every + // sizing mode that works from a delta computed zero and did nothing; the + // inset it wrote was overwritten by the next renderChild anyway. if (hasH != mHasHScrollBar || hasV != mHasVScrollBar) { - S32 deltaY = hasH != mHasHScrollBar ? (mHasHScrollBar ? mScrollBarThickness : -mScrollBarThickness) : 0; - S32 deltaX = hasV != mHasVScrollBar ? (mHasVScrollBar ? mScrollBarThickness : -mScrollBarThickness) : 0; - - iterator i; - for (i = begin(); i != end(); i++) - { - GuiControl* ctrl = static_cast(*i); - ctrl->mRenderInsetRB = Point2I(ctrl->mRenderInsetRB.x + deltaX, ctrl->mRenderInsetRB.y + deltaY); - ctrl->preventResizeModeFill(); - ctrl->preventResizeModeCenter(); - ctrl->parentResized(mBounds.extent - (ctrl->mRenderInsetLT + ctrl->mRenderInsetRB), mBounds.extent - (ctrl->mRenderInsetLT + ctrl->mRenderInsetRB)); - } - mCalcGuard = true; Parent::resize(newPos, newExt); mCalcGuard = false; @@ -140,17 +138,57 @@ void GuiScrollCtrl::childResized(GuiControl *child) computeSizes(); } +RectI GuiScrollCtrl::getInnerRect(Point2I &offset, Point2I &extent, GuiControlState currentState, GuiControlProfile *profile) +{ + // The margins, borders and padding first, then whatever the bars are using. + // A child asking this control how big it is has to be told what it can + // actually see, or it lays its last column out underneath the bar. + RectI inner = Parent::getInnerRect(offset, extent, currentState, profile); + inner.extent = subtractScrollBars(inner.extent, mHasHScrollBar, mHasVScrollBar, mScrollBarThickness); + + return inner; +} + +void GuiScrollCtrl::preventUnsizedModes(GuiControl *child) +{ + // Fill and center both mean "put me where the parent's size says", and in an + // axis this control can scroll there is no such size: the content is as long + // as it wants to be and this is a window onto it. Filling there would clamp + // the content to what is visible and leave nothing to scroll. + // + // An axis whose bar is alwaysOff is a different thing entirely. Nothing + // scrolls across it, the room is exactly the inner rect less any bar on the + // other axis, and fill is the honest way for a child to ask for it. Refusing + // it there is what forced callers to compute widths in script. + if (canScrollHorizontally()) + { + child->preventHorizResizeModeFill(); + child->preventHorizResizeModeCenter(); + } + + if (canScrollVertically()) + { + child->preventVertResizeModeFill(); + child->preventVertResizeModeCenter(); + } +} + void GuiScrollCtrl::addObject(SimObject* object) { - //Fill is not supported inside a scroll control GuiControl* child = dynamic_cast(object); if (child) { - child->preventResizeModeFill(); - child->preventResizeModeCenter(); + preventUnsizedModes(child); } Parent::addObject(object); computeSizes(); + + // A child that fills wants its size now rather than at whatever resize + // happens to come next -- the same reason GuiControl::applySizing exists. + if (child) + { + child->parentResized(mContentExt, mContentExt); + } } bool GuiScrollCtrl::onWake() @@ -293,12 +331,61 @@ GuiScrollCtrl::Region GuiScrollCtrl::findHitRegion(const Point2I& pt) } #pragma region CalculationFunctions +Point2I GuiScrollCtrl::subtractScrollBars(const Point2I &extent, const bool hasHBar, const bool hasVBar, const S32 barThickness) +{ + // A vertical bar stands down the side and so costs WIDTH; a horizontal one + // costs height. Getting that pair the wrong way round is the easiest + // mistake here, which is most of why this is one function and not four. + return Point2I(extent.x - (hasVBar ? barThickness : 0), + extent.y - (hasHBar ? barThickness : 0)); +} + +void GuiScrollCtrl::calcBarPresence(const S32 forceHBar, const S32 forceVBar, const Point2I &childExtent, + const Point2I &contentExtent, const S32 barThickness, bool &outHasHBar, bool &outHasVBar) +{ + outHasHBar = (forceHBar == ScrollBarAlwaysOn); + outHasVBar = (forceVBar == ScrollBarAlwaysOn); + + // Every test below is against the room that actually REMAINS, which is what + // lets one bar call the other into being. The old code compared both against + // the un-narrowed extent, so its second look at the horizontal bar asked a + // question it had already answered and could never say yes. + Point2I room = subtractScrollBars(contentExtent, outHasHBar, outHasVBar, barThickness); + + if (!outHasHBar && forceHBar == ScrollBarDynamic && childExtent.x > room.x) + { + outHasHBar = true; + room.y -= barThickness; + } + + if (!outHasVBar && forceVBar == ScrollBarDynamic && childExtent.y > room.y) + { + outHasVBar = true; + room.x -= barThickness; + + // The vertical bar just narrowed the content, and that can be what + // pushes it wide enough to need a horizontal one after all. + if (!outHasHBar && forceHBar == ScrollBarDynamic && childExtent.x > room.x) + { + outHasHBar = true; + } + } +} + void GuiScrollCtrl::computeSizes() { if (!mCalcGuard)//Prevent needless calcuations { + const bool hadHBar = mHasHScrollBar; + const bool hadVBar = mHasVScrollBar; + calcContentExtents(); + // What there would be with no bars at all. Kept because the bars are + // decided from it and then taken off it, and because a child that has to + // be told the room changed needs both answers. + const Point2I barFreeExtent = mContentExt; + mHBarEnabled = false; mVBarEnabled = false; mHasVScrollBar = (mForceVScrollBar == ScrollBarAlwaysOn); @@ -306,28 +393,21 @@ void GuiScrollCtrl::computeSizes() setUpdate(); - if (calcChildExtents()) + const bool hasChildren = calcChildExtents(); + if (hasChildren) { - if (mChildExt.x > mContentExt.x && (mForceHScrollBar == ScrollBarDynamic)) - { - mHasHScrollBar = true; - } - if (mChildExt.y > mContentExt.y && (mForceVScrollBar == ScrollBarDynamic)) - { - mHasVScrollBar = true; - - // If Extent X Changed, check Horiz Scrollbar. - if (mChildExt.x > mContentExt.x && !mHasHScrollBar && (mForceHScrollBar == ScrollBarDynamic)) - { - mHasHScrollBar = true; - } - } + calcBarPresence(mForceHScrollBar, mForceVScrollBar, mChildExt, barFreeExtent, + mScrollBarThickness, mHasHScrollBar, mHasVScrollBar); + } - if (mHasVScrollBar) - mContentExt.x -= mScrollBarThickness; - if (mHasHScrollBar) - mContentExt.y -= mScrollBarThickness; + // Outside the children test, unlike before: a bar forced alwaysOn takes + // its space whether or not anything has been put in the control yet, and + // a scroller that reported its whole width until its first child arrived + // would hand that width to the child. + mContentExt = subtractScrollBars(barFreeExtent, mHasHScrollBar, mHasVScrollBar, mScrollBarThickness); + if (hasChildren) + { // enable needed scroll bars if (mChildExt.x > mContentExt.x) mHBarEnabled = true; @@ -337,6 +417,15 @@ void GuiScrollCtrl::computeSizes() //Are we now over-scrolled? calcScrollOffset(); } + + // The bars appearing is a change in how much room the children have, and + // this is the only place that knows it happened -- most of the time the + // bar arrives from childResized, nowhere near a resize of this control. + if (hadHBar != mHasHScrollBar || hadVBar != mHasVScrollBar) + { + notifyChildrenOfBarChange(barFreeExtent, hadHBar, hadVBar); + } + // build all the rectangles and such... Point2I zero = mBounds.point.Zero; RectI ctrlRect = applyMargins(zero, mBounds.extent, NormalState, mProfile); @@ -346,6 +435,32 @@ void GuiScrollCtrl::computeSizes() } } +void GuiScrollCtrl::notifyChildrenOfBarChange(const Point2I &barFreeExtent, const bool hadHBar, const bool hadVBar) +{ + // Resizing a child calls back into childResized and so into here again. The + // re-entered pass does its own arithmetic and settles; what it must not do + // is announce this same change a second time. + if (mNotifyGuard) + { + return; + } + mNotifyGuard = true; + + // Both extents measured from the SAME bar-free rect, so the difference + // between them is the bars and nothing else. Any change to the control's own + // size has already been passed down by GuiControl::resize. + const Point2I before = subtractScrollBars(barFreeExtent, hadHBar, hadVBar, mScrollBarThickness); + const Point2I after = subtractScrollBars(barFreeExtent, mHasHScrollBar, mHasVScrollBar, mScrollBarThickness); + + for (iterator i = begin(); i != end(); i++) + { + GuiControl *ctrl = static_cast(*i); + ctrl->parentResized(before, after); + } + + mNotifyGuard = false; +} + void GuiScrollCtrl::calcContentExtents() { RectI ctrlRect = applyMargins(mBounds.point, mBounds.extent, NormalState, mProfile); @@ -931,8 +1046,10 @@ void GuiScrollCtrl::onRender(Point2I offset, const RectI &updateRect) renderUniversalRect(ctrlRect, mProfile, NormalState); - RectI fillRect = applyBorders(ctrlRect.point, ctrlRect.extent, NormalState, mProfile); - RectI contentRect = applyScrollBarSpacing(fillRect.point, fillRect.extent); + // The same rect the children were SIZED against -- getInnerRect is now the + // one definition of it. Two separate subtractions were how the visible area + // and the laid-out area came to disagree in the first place. + RectI contentRect = getInnerRect(offset, mBounds.extent, NormalState, mProfile); mChildArea.set(contentRect.point, contentRect.extent); renderVScrollBar(offset); @@ -946,23 +1063,10 @@ void GuiScrollCtrl::onRender(Point2I offset, const RectI &updateRect) RectI GuiScrollCtrl::applyScrollBarSpacing(Point2I offset, Point2I extent) { - RectI contentRect = RectI(offset, extent); - - if (mHasVScrollBar && mHasHScrollBar) - { - contentRect.extent.x -= mScrollBarThickness; - contentRect.extent.y -= mScrollBarThickness; - } - else if (mHasVScrollBar) - { - contentRect.extent.x -= mScrollBarThickness; - } - else if (mHasHScrollBar) - { - contentRect.extent.y -= mScrollBarThickness; - } - - return contentRect; + // Kept for anything overriding or calling it, but no longer a second copy of + // the arithmetic: getInnerRect and this both go through subtractScrollBars, + // so they cannot come to disagree about what a bar costs. + return RectI(offset, subtractScrollBars(extent, mHasHScrollBar, mHasVScrollBar, mScrollBarThickness)); } GuiControlState GuiScrollCtrl::getRegionCurrentState(GuiScrollCtrl::Region region) diff --git a/engine/source/gui/containers/guiScrollCtrl.h b/engine/source/gui/containers/guiScrollCtrl.h index 060405dd7..29774550c 100755 --- a/engine/source/gui/containers/guiScrollCtrl.h +++ b/engine/source/gui/containers/guiScrollCtrl.h @@ -34,6 +34,7 @@ class GuiScrollCtrl : public GuiControl bool mEventBubbled; bool mCalcGuard; bool mResizeGuard; + bool mNotifyGuard; protected: @@ -138,6 +139,50 @@ class GuiScrollCtrl : public GuiControl virtual void computeSizes(); + /// @name The size a child is given + /// + /// A scroll control is the one container that does not always have a size to + /// offer a child. In an axis it can scroll, the content is as long as it + /// wants to be and the control merely shows a window onto it -- there is no + /// fixed size to hand down. In an axis whose bar is alwaysOff there is no + /// scrolling, so the space IS bounded, and a child may be sized to it. + /// + /// These say which case an axis is in, and what is left over once whatever + /// bars are showing have taken their share. + /// @{ + + /// True when this axis scrolls, and so has no fixed size to offer a child. + bool canScrollHorizontally() const { return mForceHScrollBar != ScrollBarAlwaysOff; } + bool canScrollVertically() const { return mForceVScrollBar != ScrollBarAlwaysOff; } + + /// What is left of an extent once the showing bars have taken their space. + /// Pure, and the single definition of that arithmetic: the visible rect, the + /// rect children are rendered into and the rect fill resolves against are all + /// this same subtraction, and used to be three separate ones. + static Point2I subtractScrollBars(const Point2I &extent, const bool hasHBar, const bool hasVBar, const S32 barThickness); + + /// Which bars a scroller of this configuration shows. + /// + /// Pure, so that the one genuinely circular part of the layout is testable on + /// its own: a vertical bar narrows the content, which can be what pushes the + /// content wide enough to need a horizontal one. + static void calcBarPresence(const S32 forceHBar, const S32 forceVBar, const Point2I &childExtent, + const Point2I &contentExtent, const S32 barThickness, bool &outHasHBar, bool &outHasVBar); + + /// The visible content rect: the inner rect less the bars that are showing. + virtual RectI getInnerRect(Point2I &offset, Point2I &extent, GuiControlState currentState, GuiControlProfile *profile); + + /// Strips the sizing modes a child may not use in an axis that scrolls. + void preventUnsizedModes(GuiControl *child); + + /// Tells the children that the bars took, or gave back, their share. + /// + /// Only the bars' share: an outer resize has already been passed down by + /// GuiControl::resize, and counting it twice would move every child that + /// sizes on a delta. + void notifyChildrenOfBarChange(const Point2I &barFreeExtent, const bool hadHBar, const bool hadVBar); + /// @} + virtual void addObject(SimObject *obj); virtual void resize(const Point2I &newPosition, const Point2I &newExtent); virtual void childResized(GuiControl *child); diff --git a/engine/source/gui/guiControl.cc b/engine/source/gui/guiControl.cc index 4b58d9e6a..74c3633ef 100755 --- a/engine/source/gui/guiControl.cc +++ b/engine/source/gui/guiControl.cc @@ -668,23 +668,43 @@ void GuiControl::parentResized(const Point2I &oldParentExtent, const Point2I &ne } void GuiControl::preventResizeModeFill() +{ + preventHorizResizeModeFill(); + preventVertResizeModeFill(); +} + +void GuiControl::preventResizeModeCenter() +{ + preventHorizResizeModeCenter(); + preventVertResizeModeCenter(); +} + +void GuiControl::preventHorizResizeModeFill() { if (getHorizSizing() == horizResizeFill) { setHorizSizing(horizResizeRight); } +} + +void GuiControl::preventVertResizeModeFill() +{ if (getVertSizing() == vertResizeFill) { setVertSizing(vertResizeBottom); } } -void GuiControl::preventResizeModeCenter() +void GuiControl::preventHorizResizeModeCenter() { if (getHorizSizing() == horizResizeCenter) { setHorizSizing(horizResizeRight); } +} + +void GuiControl::preventVertResizeModeCenter() +{ if (getVertSizing() == vertResizeCenter) { setVertSizing(vertResizeBottom); diff --git a/engine/source/gui/guiControl.h b/engine/source/gui/guiControl.h index ce351b42f..3a1ba3752 100755 --- a/engine/source/gui/guiControl.h +++ b/engine/source/gui/guiControl.h @@ -491,6 +491,17 @@ class GuiControl : public SimGroup, public virtual Tickable /// Removes the resize mode of center and changes it to right or bottom void preventResizeModeCenter(); + + /// The same, one axis at a time. + /// + /// A container that can only offer a child a fixed size in ONE direction -- + /// a scroll control, whose other axis is as long as its content wants to be + /// -- has to be able to refuse fill across without refusing it down. See + /// GuiScrollCtrl::preventUnsizedModes. + void preventHorizResizeModeFill(); + void preventVertResizeModeFill(); + void preventHorizResizeModeCenter(); + void preventVertResizeModeCenter(); /// @} /// @name Rendering diff --git a/engine/source/testing/tests/guiScrollLayoutTests.cc b/engine/source/testing/tests/guiScrollLayoutTests.cc new file mode 100644 index 000000000..53501324c --- /dev/null +++ b/engine/source/testing/tests/guiScrollLayoutTests.cc @@ -0,0 +1,244 @@ +//----------------------------------------------------------------------------- +// 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. +//----------------------------------------------------------------------------- + +// We don't want tests in a shipping version. +#ifndef TORQUE_SHIPPING + +#ifndef _UNIT_TESTING_H_ +#include "testing/unitTesting.h" +#endif + +#ifndef _GUICONTROL_H_ +#include "gui/guiControl.h" +#endif + +#ifndef _GUISCROLLCTRL_H_ +#include "gui/containers/guiScrollCtrl.h" +#endif + +//----------------------------------------------------------------------------- +// How much room a scroll control has to offer a child. +// +// A scroll control is the one container that does not always have a size to +// give. In an axis it can scroll, the content is as long as it wants to be and +// the control is a window onto it -- there is no fixed size to hand down. In an +// axis whose bar is alwaysOff nothing scrolls, the room is bounded, and a child +// may be sized to it. +// +// Getting that wrong is not cosmetic. The palette in the Gui Editor laid its +// tiles out across a width that included the vertical scroll bar, so the last +// column of every row was drawn underneath it, and the names in that column read +// "Number Bo:". It was fixed in script three times -- by narrowing the container, +// by asking for fill, by measuring and subtracting -- and none of them held, +// because none of them were the thing that was wrong. +// +// These tests are the arithmetic, on its own. They construct nothing and need no +// canvas: the two functions are static and take everything they use, which is +// why they were pulled out of computeSizes in the first place. +//----------------------------------------------------------------------------- + +static const S32 BarThickness = 14; + +//----------------------------------------------------------------------------- +// subtractScrollBars -- what is left once the bars have taken their share. +// +// A vertical bar stands down the side and costs WIDTH; a horizontal one costs +// height. Transposing that pair is the easiest mistake in this file. +//----------------------------------------------------------------------------- + +TEST( GuiScrollLayoutTests, NoBarsTakeNothing ) +{ + const Point2I room = GuiScrollCtrl::subtractScrollBars( Point2I( 300, 200 ), false, false, BarThickness ); + + ASSERT_EQ( room.x, 300 ); + ASSERT_EQ( room.y, 200 ); + + SUCCEED(); +} + +TEST( GuiScrollLayoutTests, AVerticalBarCostsWidth ) +{ + const Point2I room = GuiScrollCtrl::subtractScrollBars( Point2I( 300, 200 ), false, true, BarThickness ); + + ASSERT_EQ( room.x, 286 ) << "A vertical bar stands down the side; it takes width."; + ASSERT_EQ( room.y, 200 ) << "It must not take height."; + + SUCCEED(); +} + +TEST( GuiScrollLayoutTests, AHorizontalBarCostsHeight ) +{ + const Point2I room = GuiScrollCtrl::subtractScrollBars( Point2I( 300, 200 ), true, false, BarThickness ); + + ASSERT_EQ( room.x, 300 ) << "A horizontal bar must not take width."; + ASSERT_EQ( room.y, 186 ); + + SUCCEED(); +} + +TEST( GuiScrollLayoutTests, BothBarsTakeBoth ) +{ + const Point2I room = GuiScrollCtrl::subtractScrollBars( Point2I( 300, 200 ), true, true, BarThickness ); + + ASSERT_EQ( room.x, 286 ); + ASSERT_EQ( room.y, 186 ); + + SUCCEED(); +} + +//----------------------------------------------------------------------------- +// calcBarPresence -- which bars a scroller of this configuration shows. +// +// The content extent passed in is the bar-free one: margins, borders and padding +// already taken off, nothing else. +//----------------------------------------------------------------------------- + +// A helper, because every test below wants both answers and C++ has no tuples +// worth the trouble here. +struct BarPresence +{ + bool horizontal; + bool vertical; + + BarPresence( const S32 forceH, const S32 forceV, const Point2I &child, const Point2I &content ) + { + GuiScrollCtrl::calcBarPresence( forceH, forceV, child, content, BarThickness, horizontal, vertical ); + } +}; + +TEST( GuiScrollLayoutTests, AlwaysOffShowsNoBarHoweverBigTheContent ) +{ + const BarPresence bars( GuiScrollCtrl::ScrollBarAlwaysOff, GuiScrollCtrl::ScrollBarAlwaysOff, + Point2I( 9000, 9000 ), Point2I( 300, 200 ) ); + + ASSERT_FALSE( bars.horizontal ); + ASSERT_FALSE( bars.vertical ) << "alwaysOff is a promise that the axis does not scroll."; + + SUCCEED(); +} + +TEST( GuiScrollLayoutTests, AlwaysOnShowsTheBarWithNoContentAtAll ) +{ + const BarPresence bars( GuiScrollCtrl::ScrollBarAlwaysOn, GuiScrollCtrl::ScrollBarAlwaysOn, + Point2I( 0, 0 ), Point2I( 300, 200 ) ); + + ASSERT_TRUE( bars.horizontal ); + ASSERT_TRUE( bars.vertical ) << "An always-on bar takes its space whether or not it is needed."; + + SUCCEED(); +} + +TEST( GuiScrollLayoutTests, DynamicStaysHiddenWhileTheContentFits ) +{ + const BarPresence bars( GuiScrollCtrl::ScrollBarDynamic, GuiScrollCtrl::ScrollBarDynamic, + Point2I( 300, 200 ), Point2I( 300, 200 ) ); + + ASSERT_FALSE( bars.horizontal ) << "Exactly filling the room is not overflowing it."; + ASSERT_FALSE( bars.vertical ); + + SUCCEED(); +} + +TEST( GuiScrollLayoutTests, DynamicShowsTheVerticalBarWhenTheContentIsTooTall ) +{ + const BarPresence bars( GuiScrollCtrl::ScrollBarAlwaysOff, GuiScrollCtrl::ScrollBarDynamic, + Point2I( 300, 5000 ), Point2I( 300, 200 ) ); + + ASSERT_TRUE( bars.vertical ); + ASSERT_FALSE( bars.horizontal ) << "The width still fits, and across is alwaysOff regardless."; + + SUCCEED(); +} + +// The palette's shape: tall content, no horizontal scrolling, a dynamic vertical +// bar. This is the case the whole bug lived in. +TEST( GuiScrollLayoutTests, TheRoomAcrossIsNarrowedByTheVerticalBar ) +{ + const Point2I content( 334, 321 ); + const BarPresence bars( GuiScrollCtrl::ScrollBarAlwaysOff, GuiScrollCtrl::ScrollBarDynamic, + Point2I( 334, 1500 ), content ); + + ASSERT_TRUE( bars.vertical ); + + const Point2I room = GuiScrollCtrl::subtractScrollBars( content, bars.horizontal, bars.vertical, BarThickness ); + + ASSERT_EQ( room.x, 320 ) << "This is the width the palette's groups must lay out in."; + ASSERT_EQ( room.y, 321 ) << "Nothing takes height here; across is alwaysOff."; + + SUCCEED(); +} + +// The circular part, and the reason calcBarPresence exists as its own function. +// The old code compared both axes against the un-narrowed extent, so its second +// look at the horizontal bar asked a question it had already answered. +TEST( GuiScrollLayoutTests, AVerticalBarCanCallAHorizontalOneIntoBeing ) +{ + // 295 fits across 300, but not across the 286 left once the vertical bar + // has taken its share. + const BarPresence bars( GuiScrollCtrl::ScrollBarDynamic, GuiScrollCtrl::ScrollBarDynamic, + Point2I( 295, 5000 ), Point2I( 300, 200 ) ); + + ASSERT_TRUE( bars.vertical ); + ASSERT_TRUE( bars.horizontal ) + << "The vertical bar narrowed the content past what the width could hold."; + + SUCCEED(); +} + +TEST( GuiScrollLayoutTests, AHorizontalBarCanCallAVerticalOneIntoBeing ) +{ + // 195 fits down 200, but not down the 186 left once the horizontal bar has. + const BarPresence bars( GuiScrollCtrl::ScrollBarDynamic, GuiScrollCtrl::ScrollBarDynamic, + Point2I( 5000, 195 ), Point2I( 300, 200 ) ); + + ASSERT_TRUE( bars.horizontal ); + ASSERT_TRUE( bars.vertical ); + + SUCCEED(); +} + +TEST( GuiScrollLayoutTests, AnAlwaysOnVerticalBarNarrowsTheRoomTheHorizontalOneJudges ) +{ + // Nothing overflows 300 across, but the always-on vertical bar leaves 286. + const BarPresence bars( GuiScrollCtrl::ScrollBarDynamic, GuiScrollCtrl::ScrollBarAlwaysOn, + Point2I( 295, 50 ), Point2I( 300, 200 ) ); + + ASSERT_TRUE( bars.vertical ); + ASSERT_TRUE( bars.horizontal ) + << "An always-on bar takes its space before the other axis is judged."; + + SUCCEED(); +} + +TEST( GuiScrollLayoutTests, AnAlwaysOffAxisIsNeverCalledIntoBeing ) +{ + // Wide enough to overflow twice over, but across cannot scroll. + const BarPresence bars( GuiScrollCtrl::ScrollBarAlwaysOff, GuiScrollCtrl::ScrollBarDynamic, + Point2I( 5000, 5000 ), Point2I( 300, 200 ) ); + + ASSERT_TRUE( bars.vertical ); + ASSERT_FALSE( bars.horizontal ) << "alwaysOff holds even when the content overflows it."; + + SUCCEED(); +} + +#endif // TORQUE_SHIPPING diff --git a/tests/smoke/palette.cs b/tests/smoke/palette.cs index 1be0a3452..c4d44a85f 100644 --- a/tests/smoke/palette.cs +++ b/tests/smoke/palette.cs @@ -201,9 +201,97 @@ function palPaletteChecks() palCheck("29 tiles across the groups (" @ %tiles @ ")", %tiles == 29); palCheck("no Tab Page tile", palFindTile("GuiTabPageCtrl") == 0); + palWidthChecks(); + palResizeChecks(); palModeChecks(); } +//----------------------------------------------------------------------------- +// Dragging the frame. +// +// The static case passing means nothing on its own: a width handed out once is +// right once. The palette was fixed in script that way and it held only until +// the frame was next dragged, because "width" sizing adds the parent's CHANGE to +// a child's own width and never re-reads anything. +// +// So sweep the window across a range of widths, the way a person dragging its +// edge does, and check the invariant at every stop. Nothing calls relayout here +// on purpose -- dragging a frame does not, and if the layout only survives a +// nudge from script then it has not survived. +//----------------------------------------------------------------------------- + +function palResizeChecks() +{ + %window = GuiEditor.ctrlListWindow; + %bar = %window.scroller.scrollBarThickness; + %pos = %window.getPosition(); + %was = %window.getExtent(); + %height = getWord(%was, 1); + + %worst = ""; + %worstOver = 0; + + for(%w = 250; %w <= 430; %w += 6) + { + %window.resize(getWord(%pos, 0), getWord(%pos, 1), %w, %height); + + %scrollerW = getWord(%window.scroller.getExtent(), 0); + for(%g = 0; %g < %window.groupCount; %g++) + { + %over = (getWord(%window.group[%g].getExtent(), 0) + %bar) - %scrollerW; + if(%over > %worstOver) + { + %worstOver = %over; + %worst = "window " @ %w @ ", group " @ %g @ ": " @ + getWord(%window.group[%g].getExtent(), 0) @ " + " @ %bar @ + " is " @ %over @ " past " @ %scrollerW; + } + } + } + + %window.resize(getWord(%pos, 0), getWord(%pos, 1), getWord(%was, 0), %height); + + palCheck("no width leaves a group under the scroll bar (" @ + (%worstOver > 0 ? %worst : "31 widths clear") @ ")", %worstOver <= 0); +} + +//----------------------------------------------------------------------------- +// Room for the scroll bar. +// +// A GuiScrollCtrl subtracts its bar when it CLIPS -- applyScrollBarSpacing feeds +// renderChildControls a narrowed rect -- but never when it lays out: the content +// child keeps its full extent and is simply drawn cut off. A group that took the +// scroller's whole width therefore ran one column under the bar, and the grid, +// sizing itself from that width, fitted a column that could not be seen. +// +// The palette takes the bar off itself. These checks are what says so. +//----------------------------------------------------------------------------- + +function palWidthChecks() +{ + %window = GuiEditor.ctrlListWindow; + %bar = %window.scroller.scrollBarThickness; + %scroller = getWord(%window.scroller.getExtent(), 0); + + for(%g = 0; %g < %window.groupCount; %g++) + { + %group = %window.group[%g]; + %width = getWord(%group.getExtent(), 0); + + // The scroller's own borders are on top of this, so a group that merely + // fits inside the outer extent is still too wide. Leaving a whole bar + // spare is the part that cannot happen by accident. + palCheck("group " @ %g @ " leaves room for the bar (" @ %width @ " + " @ + %bar @ " within " @ %scroller @ ")", (%width + %bar) <= %scroller); + } + + // The grid is the thing that actually reflows, and it is sized from the + // group, so this is the assertion that the columns are countable. + %group = %window.group[0]; + palCheck("the grid is no wider than its group", + getWord(%group.grid.getExtent(), 0) <= getWord(%group.getExtent(), 0)); +} + //----------------------------------------------------------------------------- // Switching views, and surviving a collapse. //----------------------------------------------------------------------------- From 9a089e6dc2ad1f4d2c6d8d0f8b04baedb0e78d4a Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Sun, 2 Aug 2026 21:32:46 -0400 Subject: [PATCH 27/55] Gui: a tree row can carry more than a triangle and a name GuiTreeViewCtrl::onRenderItem drew a row in one unbroken run, and the only way to add anything to it was to override the whole thing -- which cannot be done faithfully anyway, because the editor focus line is driven by private state. It is now split at the two places new content goes. - renderItemGutter runs BEFORE the focus line and the depth indent, so what it draws stays pinned to the row's left edge instead of travelling with the tree. renderItemIcon runs between the triangle and the text. Both take contentRect by reference and may carve space off its left; every step after them uses what is left. The base draws nothing in either, so every existing tree renders exactly as it did. - A row can wear one frame of a sheet, set by IconImage/IconSize. Which frame is script's answer to onGetItemIcon, asked ONCE as the tree builds and cached on the TreeItem -- onRenderItem runs for every visible row of every frame, so a callback there would be a console call per row per frame. Modelled on getObjectText, which already works that way. refreshItem re-asks both text and icon for one row, because a control's picture can change without the 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. - The icons are drawn with dglDrawBitmapStretchSR rather than renderStretchedImageAsset. That one reads the asset off the PROFILE, and its first statement is dglClearBitmapModulation -- which here would throw away the row's font colour. Drawn directly, a white sheet inherits the row's normal/highlight/selected/disabled ink for nothing. - mIndentSize is wired up at last. It was declared, initialised to 10 and read nowhere; wiring it without resetting the constructor to 0 would have silently re-indented every tree in the engine. 0 now means "one row height", which is the step the tree has always used. GuiEditorExplorerTree is the first thing to use the gutter seam: two columns of editor state, an eye and a padlock, in the manner of a layers panel. It is editor-only and the palette refuses it for free, by the GuiEdit prefix, in both copies of that rule. hidden and locked are not properties of the Gui. Neither is ever written to a file -- _writeHidden and _writeLocked both refuse -- because saving them would put a working state into the document. So a click in a column writes the flag straight, with no undo record: undoing one would restore something that can never reach a file, and would leave Ctrl+Z after "hide three things, then move a button" un-hiding something instead of putting the button back. It acts on the row clicked and not on the selection, which is what a layers panel does and what someone reaching for one row's eye means. Two things the columns must not do, and how: - Not change the selection. onTouchDown handles the gutter above everything, including the base's row-0 case -- which never calls Parent::onTouchDown at all, so the root row would otherwise be the one row whose columns did nothing. Returning before Parent::onTouchDown buys the rest: no setFirstResponder, no handleItemClick, therefore no selection change and no onClick or onDoubleClick, and mLastClickItem is left alone so a press in a column followed by a press on the row is not read as a double click. - Not arm a reorder. An mGutterPress latch makes onTouchDragged return early, which also skips getHitIndex -- whose side effects are the drag state the drop indicator and reorderFromDrag read. onTouchUp swallows rather than chains, because GuiControl::onTouchUp bubbles an event the script did not consume. The columns are drawn in ONE ink whatever state the row is in. Inheriting the row's font colour is what the icons did at first and it read correctly nowhere: the box behind them does not change with selection, so a selected row put selected-TEXT ink on an unselected background and the eye all but vanished, by varying amounts in all four themes. An icon on a state-independent background needs a state-independent colour. The geometry is all statics taking everything they use -- resolveIndent, iconSlot, focusLineOffset, getGutterCells, columnAt, getBoxRect -- because a unit test cannot reach it any other way. Adding a row to a tree calls updateSize, which asks the profile for a font, which LOADS one, which registers a texture, which asserts with no GL context; in a debug build that is a modal box, so it arrives as a hang. guiTreeRowLayoutTests.cc, 18 tests, and treeIcons.cs for the plumbing that does need a canvas. focusLineOffset is there because making the indent settable broke the focus line four pixels, silently. The rule that hangs from a container's triangle was centred in the INDENT slot, and the triangle is drawn in a square of the ROW HEIGHT. Those were the same number, so nobody had to know which one they were centring on. Now the line takes the row height and nothing else, and the test sweeps every height from 4 to 64 asserting it still covers the triangle's point. Two things fixed while in here. getObjectText declared char buffer[1024], filled it only inside if(obj), and returned StringTable->insert(buffer) unconditionally -- a null object read uninitialised stack. calculateHeaderExtent was dead: it computed two locals, used neither, and nothing called it. It was the only trace of an earlier attempt at columns. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qi2pZ9UzF5rsLCNVErpzwt --- cmake/EngineSources.cmake | 2 + .../GuiEditor/scripts/GuiEditorControlSpec.cs | 7 +- .../gui/editor/guiEditorExplorerTree.cc | 434 ++++++++++++++++++ .../source/gui/editor/guiEditorExplorerTree.h | 167 +++++++ .../guiEditorExplorerTree_ScriptBinding.h | 79 ++++ engine/source/gui/guiTreeViewCtrl.cc | 191 +++++++- engine/source/gui/guiTreeViewCtrl.h | 92 +++- .../gui/guiTreeViewCtrl_ScriptBinding.h | 34 ++ .../testing/tests/guiTreeRowLayoutTests.cc | 354 ++++++++++++++ tests/smoke/treeIcons.cs | 114 +++++ 10 files changed, 1445 insertions(+), 29 deletions(-) create mode 100644 engine/source/gui/editor/guiEditorExplorerTree.cc create mode 100644 engine/source/gui/editor/guiEditorExplorerTree.h create mode 100644 engine/source/gui/editor/guiEditorExplorerTree_ScriptBinding.h create mode 100644 engine/source/testing/tests/guiTreeRowLayoutTests.cc create mode 100644 tests/smoke/treeIcons.cs diff --git a/cmake/EngineSources.cmake b/cmake/EngineSources.cmake index f4caefbdc..40ab45e53 100644 --- a/cmake/EngineSources.cmake +++ b/cmake/EngineSources.cmake @@ -202,6 +202,7 @@ set(TORQUE_ENGINE_SOURCES # ---- gui/editor ---- ${TORQUE_SRC}/gui/editor/guiDebugger.cc ${TORQUE_SRC}/gui/editor/guiEditCtrl.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 @@ -341,6 +342,7 @@ set(TORQUE_ENGINE_SOURCES ${TORQUE_SRC}/testing/tests/guiProfileThemeTests.cc ${TORQUE_SRC}/testing/tests/guiScrollLayoutTests.cc ${TORQUE_SRC}/testing/tests/guiTextEditTests.cc + ${TORQUE_SRC}/testing/tests/guiTreeRowLayoutTests.cc ${TORQUE_SRC}/testing/tests/platformFileIoTests.cc ${TORQUE_SRC}/testing/tests/platformMemoryTests.cc ${TORQUE_SRC}/testing/tests/platformStringTests.cc diff --git a/editor/GuiEditor/scripts/GuiEditorControlSpec.cs b/editor/GuiEditor/scripts/GuiEditorControlSpec.cs index 907fa5eb8..951966295 100644 --- a/editor/GuiEditor/scripts/GuiEditorControlSpec.cs +++ b/editor/GuiEditor/scripts/GuiEditorControlSpec.cs @@ -192,7 +192,12 @@ %this.addSection("GuiListBoxCtrl", "List", "List", "AllowMultipleSelections FitParentWidth"); - %this.addSection("GuiTreeViewCtrl", "Tree", "Tree", "AllowReorder"); + // 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"); diff --git a/engine/source/gui/editor/guiEditorExplorerTree.cc b/engine/source/gui/editor/guiEditorExplorerTree.cc new file mode 100644 index 000000000..1cc51b9f3 --- /dev/null +++ b/engine/source/gui/editor/guiEditorExplorerTree.cc @@ -0,0 +1,434 @@ +//----------------------------------------------------------------------------- +// 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. +//----------------------------------------------------------------------------- + +#include "gui/editor/guiEditorExplorerTree.h" +#include "graphics/dgl.h" +#include "gui/guiCanvas.h" + +#include "guiEditorExplorerTree_ScriptBinding.h" + +IMPLEMENT_CONOBJECT(GuiEditorExplorerTree); + +GuiEditorExplorerTree::GuiEditorExplorerTree() +{ + mStateImageAssetID = StringTable->EmptyString; + mStateImageAsset = NULL; + // -1 is "no art", so a tree given no sheet draws empty boxes rather than + // whatever frame 0 happens to be. + mEyeFrame = -1; + mLockFrame = -1; + mShownTip = StringTable->EmptyString; + mHiddenTip = StringTable->EmptyString; + mLockedTip = StringTable->EmptyString; + mUnlockedTip = StringTable->EmptyString; + mGutterPress = false; +} + +void GuiEditorExplorerTree::initPersistFields() +{ + Parent::initPersistFields(); + + // The frame numbers come from script, never from here: EditorIcons.cs is + // generated and alphabetical, so an index baked into the engine would + // silently become a different picture the next time the sheet is rebuilt. + addProtectedField("StateIcons", TypeAssetId, Offset(mStateImageAssetID, GuiEditorExplorerTree), &setStateImage, &getStateImage, "The image asset the eye and padlock are frames of."); + addField("EyeFrame", TypeS32, Offset(mEyeFrame, GuiEditorExplorerTree), "Frame of StateIcons drawn when a control is not hidden."); + addField("LockFrame", TypeS32, Offset(mLockFrame, GuiEditorExplorerTree), "Frame of StateIcons drawn when a control is locked."); + + addField("ShownTip", TypeString, Offset(mShownTip, GuiEditorExplorerTree), "Tooltip for the eye column when the control is not hidden."); + addField("HiddenTip", TypeString, Offset(mHiddenTip, GuiEditorExplorerTree), "Tooltip for the eye column when the control is hidden."); + addField("LockedTip", TypeString, Offset(mLockedTip, GuiEditorExplorerTree), "Tooltip for the padlock column when the control is locked."); + addField("UnlockedTip", TypeString, Offset(mUnlockedTip, GuiEditorExplorerTree), "Tooltip for the padlock column when the control is not locked."); +} + +void GuiEditorExplorerTree::setStateImageAsset(const char* pImageAssetID) +{ + // Sanity! + AssertFatal(pImageAssetID != NULL, "Cannot use a NULL asset ID."); + + mStateImageAssetID = StringTable->insert(pImageAssetID); + + if (mStateImageAssetID != StringTable->EmptyString) + { + mStateImageAsset = pImageAssetID; + } + else + { + mStateImageAsset.clear(); + } +} + +//----------------------------------------------------------------------------- +// Geometry. All of it static and taking everything it uses, so it can be tested +// away from a canvas, a Sim and a GL context -- adding a row to a tree loads a +// font, which registers a texture, which is the one thing a unit test here +// cannot do. +//----------------------------------------------------------------------------- + +S32 GuiEditorExplorerTree::getGutterWidth() +{ + return 2 * smColumnWidth; +} + +void GuiEditorExplorerTree::getGutterCells(S32 left, S32 top, S32 height, RectI& hiddenCell, RectI& lockedCell) +{ + hiddenCell.set(Point2I(left, top), Point2I(smColumnWidth, height)); + lockedCell.set(Point2I(left + smColumnWidth, top), Point2I(smColumnWidth, height)); +} + +GuiEditorExplorerTree::GutterColumn GuiEditorExplorerTree::columnAt(S32 x, S32 left) +{ + const S32 offset = x - left; + if (offset < 0) + { + return GutterNone; + } + if (offset < smColumnWidth) + { + return GutterHidden; + } + if (offset < (2 * smColumnWidth)) + { + return GutterLocked; + } + return GutterNone; +} + +RectI GuiEditorExplorerTree::getBoxRect(const RectI& cell) +{ + // Square, and never taller than the row: the art is square and stretching it + // to a short row would be the one thing that makes 16px work look wrong. + const S32 size = getMin(smBoxSize, cell.extent.y); + // The divider owns the cell's last pixel, so center in what is left of it. + const S32 usable = cell.extent.x - 1; + return RectI(cell.point.x + ((usable - size) / 2), + cell.point.y + ((cell.extent.y - size) / 2), + size, size); +} + +S32 GuiEditorExplorerTree::gutterInset() +{ + if (!mProfile) + { + return 0; + } + + GuiBorderProfile* leftProfile = mProfile->getLeftBorder(); + if (!leftProfile) + { + return 0; + } + + return leftProfile->getMargin(NormalState) + leftProfile->getBorder(NormalState) + leftProfile->getPadding(NormalState); +} + +//----------------------------------------------------------------------------- +// Drawing +//----------------------------------------------------------------------------- + +void GuiEditorExplorerTree::renderItemGutter(const RectI& itemRect, RectI& contentRect, TreeItem* treeItem, GuiControlState currentState) +{ + if (!mProfile || !treeItem) + { + return; + } + + const S32 left = itemRect.point.x + gutterInset(); + const S32 want = (left + getGutterWidth()) - contentRect.point.x; + if (want <= 0 || contentRect.extent.x <= want) + { + // No room for the rail and something after it. Draw nothing and carve + // nothing: a cramped tree falls back to plain rows, which is legible, + // rather than to text written over the columns, which is not. + return; + } + + // Reserve before anything below can return early, so a row that draws no + // boxes still keeps its text off the strip and lined up with every other row. + contentRect.point.x += want; + contentRect.extent.x -= want; + + RectI hiddenCell, lockedCell; + getGutterCells(left, itemRect.point.y, itemRect.extent.y, hiddenCell, lockedCell); + + // Hover is polled, not evented: there is no per-row enter or leave to hook, + // and the row above has already read the cursor once to pick its own state. + Point2I cursorPt = Point2I(0, 0); + GuiCanvas* root = getRoot(); + if (root) + { + cursorPt = root->getCursorPos(); + } + + // The root row is the simulated canvas: stage furniture, not a control in the + // document. Hiding it would blank the canvas and locking it would do nothing, + // so it gets no boxes -- but it still draws its dividers, or the rail would + // start one row down and read as a column that had failed to paint. + SimObject* obj = getItemObject(treeItem); + const bool hasBoxes = (obj != NULL && treeItem->trunk != NULL); + + // The rail is drawn in ONE ink, on every row, whatever state the row is in. + // + // Inheriting the row's font colour is what the icons did at first, and it read + // correctly nowhere: the box behind them does not change with selection, so a + // selected row put selected-TEXT ink on an unselected background and the eye + // all but vanished. Checked against all four themes. The boxes and dividers + // are already state-independent -- the icons have to match them, not the text. + dglSetBitmapModulation(getFontColor(mProfile, NormalState)); + + // Photoshop's order and Photoshop's reading: the eye is shown when the + // control is NOT hidden, the padlock when it IS locked. So a tidy Gui is a + // column of eyes and a column of nothing. + renderGutterCell(hiddenCell, cursorPt, hasBoxes, hasBoxes && !obj->isHidden(), mEyeFrame); + renderGutterCell(lockedCell, cursorPt, hasBoxes, hasBoxes && obj->isLocked(), mLockFrame); + + // Put back what the row was drawing with. onRenderItem sets the modulation + // once, before the gutter, and everything after this -- the triangle, the + // class icon, renderText -- is still relying on it. + dglSetBitmapModulation(getFontColor(mProfile, currentState)); +} + +void GuiEditorExplorerTree::renderGutterCell(const RectI& cell, const Point2I& cursorPt, bool showBox, bool showIcon, S32 frame) +{ + const ColorI& quiet = mProfile->getFillColor(HighlightState); + + // One pixel down the cell's right, over the row's WHOLE height rather than + // its content's, so the two columns read as continuous rules down the tree + // even under a profile that puts air between rows. + RectI divider(cell.point.x + cell.extent.x - 1, cell.point.y, 1, cell.extent.y); + dglDrawRectFill(divider, quiet); + + RectI box = getBoxRect(cell); + if (!showBox || !box.isValidRect()) + { + return; + } + + // The hover fill is deliberately barely there against the row fill. The cell + // under the pointer steps up to the selected fill, which is the only thing + // saying the rail can be clicked at all -- on an unlocked, visible control + // both boxes are empty and there is otherwise nothing to find. + dglDrawRectFill(box, cell.pointInRect(cursorPt) ? mProfile->getFillColor(SelectedState) : quiet); + + // The caller has set the modulation to the normal-state font colour and will + // put the row's own back afterwards, so the icon is the same ink on every row. + if (showIcon && frame >= 0 && !mStateImageAsset.isNull()) + { + drawIconFrame(box, mStateImageAsset, (U32)frame); + } +} + +//----------------------------------------------------------------------------- +// Hit testing +//----------------------------------------------------------------------------- + +S32 GuiEditorExplorerTree::visibleRowAt(const Point2I& globalPoint) +{ + const Point2I local = globalToLocalCoord(globalPoint); + if (local.y < 0 || mItemSize.y <= 0) + { + return -1; + } + + const S32 slot = (S32)mFloor((F32)local.y / (F32)mItemSize.y); + for (S32 i = 0, j = 0; i < mItems.size(); i++) + { + TreeItem* treeItem = dynamic_cast(mItems[i]); + if (treeItem && treeItem->isVisible) + { + if (j == slot) + { + return i; + } + j++; + } + } + return -1; +} + +GuiEditorExplorerTree::GutterColumn GuiEditorExplorerTree::hitGutter(const Point2I& globalPoint, SimObject** objOut) +{ + const GutterColumn column = columnAt(globalToLocalCoord(globalPoint).x, gutterInset()); + if (column == GutterNone) + { + return GutterNone; + } + + const S32 index = visibleRowAt(globalPoint); + if (index < 0) + { + return GutterNone; + } + + TreeItem* treeItem = dynamic_cast(mItems[index]); + // The root keeps its columns' width but has no boxes, so it has none to hit. + if (!treeItem || !treeItem->isActive || treeItem->trunk == NULL) + { + return GutterNone; + } + + SimObject* obj = getItemObject(treeItem); + if (!obj) + { + return GutterNone; + } + + if (objOut) + { + *objOut = obj; + } + return column; +} + +Point2I GuiEditorExplorerTree::getGutterPoint(S32 index, GutterColumn column) +{ + // Visible-row space, which is what the rows are drawn and hit tested in. + S32 visible = 0; + for (S32 i = 0; i < mItems.size() && i < index; i++) + { + TreeItem* treeItem = dynamic_cast(mItems[i]); + if (treeItem && treeItem->isVisible) + { + visible++; + } + } + + RectI hiddenCell, lockedCell; + getGutterCells(gutterInset(), visible * mItemSize.y, mItemSize.y, hiddenCell, lockedCell); + + const RectI& cell = (column == GutterLocked) ? lockedCell : hiddenCell; + const RectI box = getBoxRect(cell); + Point2I local(box.point.x + (box.extent.x / 2), box.point.y + (box.extent.y / 2)); + return localToGlobalCoord(local); +} + +void GuiEditorExplorerTree::onTouchDown(const GuiEvent& event) +{ + mGutterPress = false; + + // Above everything the base does, including its row-0 case -- which never + // calls Parent::onTouchDown at all, so the root row would otherwise be the + // one row whose columns did nothing. + // + // Returning before Parent::onTouchDown is what buys every requirement at + // once. GuiListBoxCtrl::onTouchDown never runs, so there is no + // setFirstResponder and no handleItemClick; no handleItemClick means no + // selection change and no onClick or onDoubleClick; and mLastClickItem is + // left alone, so a press in a column followed by a press on the row is not + // read as a double click. + SimObject* obj = NULL; + const GutterColumn column = hitGutter(event.mousePoint, &obj); + if (column != GutterNone && obj) + { + // Written straight, with no undo record, and that is deliberate. Neither + // flag is ever saved -- SimObject::_writeHidden and _writeLocked both + // refuse, see the comment there -- so an undo step would restore + // something that can never reach a file, and would leave Ctrl+Z after + // "hide three things, then move a button" un-hiding something instead of + // putting the button back. + // + // It acts on the row that was clicked and not on the selection, which is + // what a layers panel does and what someone reaching for one row's eye + // means. If this ever should be undoable, it is this branch that changes + // and nothing else. + if (column == GutterHidden) + { + obj->setHidden(!obj->isHidden()); + } + else + { + obj->setLocked(!obj->isLocked()); + } + + mGutterPress = true; + setUpdate(); + return; + } + + Parent::onTouchDown(event); +} + +void GuiEditorExplorerTree::onTouchDragged(const GuiEvent& event) +{ + // A press that landed in a column never becomes a reorder. Returning here + // also skips getHitIndex, which matters: its side effects are the drag state + // the drop indicator and reorderFromDrag read, and letting them update during + // a gutter press would leave the tree half-primed to move something. + if (mGutterPress) + { + return; + } + + Parent::onTouchDragged(event); +} + +void GuiEditorExplorerTree::onTouchUp(const GuiEvent& event) +{ + if (mGutterPress) + { + mGutterPress = false; + mDragActive = false; + // Swallowed rather than chained: GuiControl::onTouchUp hands an event the + // script did not consume to the parent, and the toggle already happened. + return; + } + + Parent::onTouchUp(event); +} + +//----------------------------------------------------------------------------- +// Tooltips +//----------------------------------------------------------------------------- + +const char* GuiEditorExplorerTree::tipForPoint(const Point2I& globalPoint) +{ + SimObject* obj = NULL; + const GutterColumn column = hitGutter(globalPoint, &obj); + if (column == GutterNone || !obj) + { + return NULL; + } + + StringTableEntry tip = (column == GutterHidden) + ? (obj->isHidden() ? mHiddenTip : mShownTip) + : (obj->isLocked() ? mLockedTip : mUnlockedTip); + + return (tip != StringTable->EmptyString) ? tip : NULL; +} + +bool GuiEditorExplorerTree::renderTooltip(Point2I& cursorPos, const char* tipText) +{ + // The canvas re-calls this every frame once the pointer has settled, and it + // decides whether to at all by comparing whole controls rather than points. + // So one control can serve different text per region, and the text follows + // the cursor from column to column without anything having to invalidate it. + if (tipText == NULL) + { + const char* gutterTip = tipForPoint(cursorPos); + if (gutterTip != NULL) + { + tipText = gutterTip; + } + } + + return Parent::renderTooltip(cursorPos, tipText); +} diff --git a/engine/source/gui/editor/guiEditorExplorerTree.h b/engine/source/gui/editor/guiEditorExplorerTree.h new file mode 100644 index 000000000..15a118aa3 --- /dev/null +++ b/engine/source/gui/editor/guiEditorExplorerTree.h @@ -0,0 +1,167 @@ +//----------------------------------------------------------------------------- +// 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. +//----------------------------------------------------------------------------- + +#ifndef _GUI_EDITOR_EXPLORERTREE_H_ +#define _GUI_EDITOR_EXPLORERTREE_H_ + +#ifndef _GUI_TREEVIEWCTRL_H +#include "gui/guiTreeViewCtrl.h" +#endif + +//----------------------------------------------------------------------------- +// The Gui Editor's Explorer tree: a tree with two columns of editor state down +// its left edge. +// +// hidden and locked are not properties of the Gui being authored. Neither is +// ever written to a file -- SimObject::_writeHidden and _writeLocked both refuse +// -- because saving them would put a working state into the document. They lived +// in the properties pane beside Visible, Active and useInput, which are the real +// thing, and being next to them read as a promise that they were too. +// +// So they moved here, as the eye and the padlock of a layers panel. The columns +// do NOT indent with the tree: they are a fixed rail at the left, in row with +// each item, which is what makes a whole branch's state scannable at a glance. +// +// Editor-only, and not offered to anyone building a Gui: the palette refuses +// every class whose name begins "GuiEdit", in both copies of that rule +// (GuiEditorControlIcons::isPlaceableClass, generated, and +// GuiEditorControlSpec::isPlaceableClass, hand-typed for its drift guard). +//----------------------------------------------------------------------------- + +class GuiEditorExplorerTree : public GuiTreeViewCtrl +{ +private: + typedef GuiTreeViewCtrl Parent; + +public: + enum GutterColumn + { + GutterNone = 0, + GutterHidden, + GutterLocked + }; + + // The box is the icon, drawn at its own size: 16px art in a 16px box is the + // only scale that is sharp. Fixed rather than derived from the row height, + // which moves with the theme's font size -- a rail that changes width when + // someone bumps the font has stopped being a rail. The last pixel of a + // column is its divider. + // + // constexpr rather than const: a test asserting against one binds it to a + // const reference, which would odr-use a plain static const and fail to link. + static constexpr S32 smBoxSize = 16; + static constexpr S32 smBoxPad = 2; + static constexpr S32 smColumnWidth = smBoxSize + (2 * smBoxPad) + 1; + + /// What the two columns cost a row, together. + static S32 getGutterWidth(); + + /// The two cells, given the row's left content edge and its vertical span. + /// Deliberately a pure function of three numbers: the renderer and the hit + /// test call it with the same numbers, so the two cannot drift apart. The + /// eye is leftmost, as it is in every layers panel anyone has used. + static void getGutterCells(S32 left, S32 top, S32 height, RectI& hiddenCell, RectI& lockedCell); + + /// Which column an x falls in, measured from the same left edge. The row was + /// already settled by y, so this is the whole hit test. + static GutterColumn columnAt(S32 x, S32 left); + + /// The square inside a cell that the icon draws in and the eye reads as a + /// box. Centered in what the divider leaves. + static RectI getBoxRect(const RectI& cell); + + GuiEditorExplorerTree(); + static void initPersistFields(); + + void onTouchDown(const GuiEvent& event); + void onTouchDragged(const GuiEvent& event); + void onTouchUp(const GuiEvent& event); + bool renderTooltip(Point2I& cursorPos, const char* tipText = NULL); + + /// The row a global point lands on, as a raw mItems index, or -1. + /// + /// getHitIndex would answer this, but it also recomputes the drag state as a + /// side effect -- mDragIndex, mReorderMethod, mIsDragLegal -- and the tooltip + /// asks this question every frame the pointer sits still. Asking must not arm + /// anything. + S32 visibleRowAt(const Point2I& globalPoint); + + /// The row's left content edge, in the control's own coordinates. + /// + /// Measured with NORMAL-state insets in both the paint and the hit test, so a + /// profile that pads a highlighted row differently cannot make the columns + /// jitter under the pointer or make a click miss by the width of the change. + S32 gutterInset(); + + /// The center of one column's box on one row, in global coordinates. For + /// tests, which must not hard-code a point: a stale coordinate reports a + /// missing item, which is exactly what a broken hit test reports, and the + /// test would be lying either way. + Point2I getGutterPoint(S32 index, GutterColumn column); + +protected: + void renderItemGutter(const RectI& itemRect, RectI& contentRect, TreeItem* treeItem, GuiControlState currentState); + /// One cell: its divider always, then its box if the row has one, then the + /// icon if the flag says so. The root row draws dividers and no box, so the + /// rail runs the whole height of the tree. + void renderGutterCell(const RectI& cell, const Point2I& cursorPt, bool showBox, bool showIcon, S32 frame); + + /// Which column a press landed in, and on what. GutterNone for the root row, + /// for an inactive row, and for anything with no object behind it. + GutterColumn hitGutter(const Point2I& globalPoint, SimObject** objOut); + + /// The tip for whatever the cursor is over, or NULL to leave the control's + /// own tooltip alone. + const char* tipForPoint(const Point2I& globalPoint); + + StringTableEntry mStateImageAssetID; + AssetPtr mStateImageAsset; + S32 mEyeFrame; + S32 mLockFrame; + + // Four strings rather than a format, and named for the state each describes + // rather than for an on and an off. The eye reads inverted -- it is present + // when the control is NOT hidden -- so "the tip for the on state" would have + // to be read twice by everyone who ever touched it. + // + // The text itself is script's: the "Locked - On" heading style belongs with + // the other toggles that use it, and nothing in the engine should be + // inventing user-facing prose. + StringTableEntry mShownTip; + StringTableEntry mHiddenTip; + StringTableEntry mLockedTip; + StringTableEntry mUnlockedTip; + + /// Latched by a press that landed in a column, so the drag and the release + /// that follow it know to do nothing. + bool mGutterPress; + + void setStateImageAsset(const char* pImageAssetID); + inline StringTableEntry getStateImageAsset(void) const { return mStateImageAssetID; } + static bool setStateImage(void* obj, const char* data) { static_cast(obj)->setStateImageAsset(data); return false; } + static const char* getStateImage(void* obj, const char* data) { return static_cast(obj)->getStateImageAsset(); } + +public: + DECLARE_CONOBJECT(GuiEditorExplorerTree); +}; + +#endif //_GUI_EDITOR_EXPLORERTREE_H_ diff --git a/engine/source/gui/editor/guiEditorExplorerTree_ScriptBinding.h b/engine/source/gui/editor/guiEditorExplorerTree_ScriptBinding.h new file mode 100644 index 000000000..73035f530 --- /dev/null +++ b/engine/source/gui/editor/guiEditorExplorerTree_ScriptBinding.h @@ -0,0 +1,79 @@ +//----------------------------------------------------------------------------- +// 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. +//----------------------------------------------------------------------------- + +ConsoleMethodGroupBeginWithDocs(GuiEditorExplorerTree, GuiTreeViewCtrl) + +/*! The width of one gutter column, in pixels. + @return The column width, dividers included. +*/ +ConsoleMethodWithDocs(GuiEditorExplorerTree, getGutterColumnWidth, ConsoleInt, 2, 2, ()) +{ + return GuiEditorExplorerTree::smColumnWidth; +} + +/*! Which gutter column a point in the tree's own coordinates falls in. + @param localX The x offset into the control. + @return "hidden", "locked", or "" for neither. +*/ +ConsoleMethodWithDocs(GuiEditorExplorerTree, gutterColumnAt, ConsoleString, 3, 3, "(S32 localX)") +{ + switch (GuiEditorExplorerTree::columnAt(dAtoi(argv[2]), object->gutterInset())) + { + case GuiEditorExplorerTree::GutterHidden: + return "hidden"; + case GuiEditorExplorerTree::GutterLocked: + return "locked"; + default: + return ""; + } +} + +/*! The center of one column's box on one row, in canvas coordinates. + Exists so a test can find its own target rather than carry a hard-coded + point: a coordinate that drifted off the box would report a missing item, + which is exactly what a broken hit test reports, and the test would be lying + either way. + @param index The zero-based raw item index. + @param column "hidden" or "locked". + @return The point, as "x y". +*/ +ConsoleMethodWithDocs(GuiEditorExplorerTree, getGutterPoint, ConsoleString, 4, 4, "(S32 index, string column)") +{ + S32 index = dAtoi(argv[2]); + if (index < 0 || index >= object->mItems.size()) + { + Con::warnf("GuiEditorExplorerTree::getGutterPoint() - Invalid index given."); + return "0 0"; + } + + GuiEditorExplorerTree::GutterColumn column = (dStricmp(argv[3], "locked") == 0) + ? GuiEditorExplorerTree::GutterLocked + : GuiEditorExplorerTree::GutterHidden; + + Point2I point = object->getGutterPoint(index, column); + + char* buffer = Con::getReturnBuffer(32); + dSprintf(buffer, 32, "%d %d", point.x, point.y); + return buffer; +} + +ConsoleMethodGroupEndWithDocs(GuiEditorExplorerTree) diff --git a/engine/source/gui/guiTreeViewCtrl.cc b/engine/source/gui/guiTreeViewCtrl.cc index b4167b1d9..08f36ef4f 100755 --- a/engine/source/gui/guiTreeViewCtrl.cc +++ b/engine/source/gui/guiTreeViewCtrl.cc @@ -33,7 +33,13 @@ IMPLEMENT_CONOBJECT(GuiTreeViewCtrl); GuiTreeViewCtrl::GuiTreeViewCtrl() { mActive = true; - mIndentSize = 10; + // Zero is "one row height", which is the step the tree has always used. The + // field sat here unread for years holding 10; wiring it up without resetting + // it would have re-indented every tree in the engine as a side effect. + mIndentSize = 0; + mIconImageAssetID = StringTable->EmptyString; + mIconImageAsset = NULL; + mIconSize = 16; mMultipleSelections = true; mTouchPoint = Point2I::Zero; mDragActive = false; @@ -48,6 +54,118 @@ GuiTreeViewCtrl::~GuiTreeViewCtrl() { } +S32 GuiTreeViewCtrl::resolveIndent(S32 indentSize, S32 rowInnerHeight) +{ + // A row height is the historical step and stays the default, so a tree that + // says nothing indents exactly as it always did. Neither answer may go + // negative: a row too short to have an inside would otherwise walk the tree + // backwards, one level at a time. + const S32 indent = (indentSize > 0) ? indentSize : rowInnerHeight; + return (indent > 0) ? indent : 0; +} + +S32 GuiTreeViewCtrl::focusLineOffset(S32 rowInnerHeight) +{ + const S32 offset = (rowInnerHeight - smFocusLineWidth) / 2; + return (offset > 0) ? offset : 0; +} + +bool GuiTreeViewCtrl::iconSlot(const RectI& contentRect, S32 iconSize, RectI& dstOut, S32& advanceOut) +{ + advanceOut = 0; + if (iconSize <= 0 || contentRect.extent.y <= 0) + { + return false; + } + + // Never enlarge. The art is drawn for one size and blowing it up is what + // looks soft, so a row shorter than the icon gets the icon shrunk to it. + const S32 size = getMin(iconSize, contentRect.extent.y); + const S32 advance = size + smIconGap; + if (size <= 0 || contentRect.extent.x < advance) + { + // No room for the icon and a space after it. Consume nothing, so the row + // falls back to plain text rather than to text drawn over an icon. + return false; + } + + dstOut.set(Point2I(contentRect.point.x, contentRect.point.y + ((contentRect.extent.y - size) / 2)), + Point2I(size, size)); + advanceOut = advance; + return true; +} + +void GuiTreeViewCtrl::setIconImageAsset(const char* pImageAssetID) +{ + // Sanity! + AssertFatal(pImageAssetID != NULL, "Cannot use a NULL asset ID."); + + mIconImageAssetID = StringTable->insert(pImageAssetID); + + // Unlike a profile's sheet there is no refcount to wait on: a tree draws its + // own icons rather than lending them to whatever wears it, so resolve now. + // An empty id clears, which is how a tree turns icons back off. + if (mIconImageAssetID != StringTable->EmptyString) + { + mIconImageAsset = pImageAssetID; + } + else + { + mIconImageAsset.clear(); + } +} + +void GuiTreeViewCtrl::drawIconFrame(const RectI& dst, ImageAsset* sheet, U32 frame) +{ + if (sheet == NULL || !sheet->isAssetValid() || frame >= sheet->getFrameCount()) + { + return; + } + + const ImageAsset::FrameArea::PixelArea& pixelArea = sheet->getImageFrameArea(frame).mPixelArea; + RectI srcRect(pixelArea.mPixelOffset, Point2I(pixelArea.mPixelWidth, pixelArea.mPixelHeight)); + + dglDrawBitmapStretchSR(sheet->getImageTexture(), dst, srcRect); +} + +void GuiTreeViewCtrl::renderItemIcon(RectI& contentRect, TreeItem* treeItem, GuiControlState currentState) +{ + if (!treeItem || treeItem->iconFrame < 0 || mIconImageAsset.isNull()) + { + return; + } + + RectI dst; + S32 advance = 0; + if (!iconSlot(contentRect, mIconSize, dst, advance)) + { + return; + } + + // The modulation is still the row's font color, set before any of this drew. + drawIconFrame(dst, mIconImageAsset, (U32)treeItem->iconFrame); + + contentRect.point.x += advance; + contentRect.extent.x -= advance; +} + +S32 GuiTreeViewCtrl::getObjectIconFrame(SimObject* obj) +{ + // No sheet means no icons at all, so do not trouble script for an answer we + // would only throw away. + if (!obj || mIconImageAsset.isNull() || !isMethod("onGetItemIcon")) + { + return -1; + } + + const char* frame = Con::executef(this, 2, "onGetItemIcon", Con::getIntArg(obj->getId())); + if (!frame || !frame[0]) + { + return -1; + } + return dAtoi(frame); +} + GuiTreeViewCtrl::TreeItem* GuiTreeViewCtrl::grabItemPtr(S32 index) { // Range Check @@ -70,6 +188,15 @@ void GuiTreeViewCtrl::initPersistFields() // It defaults off so trees holding non-GuiControl items (e.g. the Profile // Editor's proxy tree) can never enter the reorder path; opt in explicitly. addField("AllowReorder", TypeBool, Offset(mAllowReorder, GuiTreeViewCtrl)); + // How far one level of depth steps a row in. Zero - the default - is one row + // height, which is what the tree has always done; a narrow tree carrying + // other things in the row can buy the width back by naming a smaller step. + addField("IndentSize", TypeS32, Offset(mIndentSize, GuiTreeViewCtrl), "Pixels per level of depth. 0 uses the row height."); + // A sheet of small pictures, one per row, drawn between the triangle and the + // text. Which frame a row wears is script's answer to onGetItemIcon; with no + // sheet set the question is never asked and no width is spent. + addProtectedField("IconImage", TypeAssetId, Offset(mIconImageAssetID, GuiTreeViewCtrl), &setIconImage, &getIconImage, "The image asset a row's icon is a frame of."); + addField("IconSize", TypeS32, Offset(mIconSize, GuiTreeViewCtrl), "How big to draw a row's icon. The art is never enlarged past this."); } S32 GuiTreeViewCtrl::getAdjacentVisibleIndex(S32 fromIndex, S32 direction) @@ -514,26 +641,37 @@ void GuiTreeViewCtrl::onRenderItem(RectI& itemRect, LBItem* item) RectI fillRect = applyBorders(ctrlRect.point, ctrlRect.extent, currentState, mProfile); RectI contentRect = applyPadding(fillRect.point, fillRect.extent, currentState, mProfile); + // Anything pinned to the row's left edge goes in here: before the focus line + // and before the depth indent, so it stays put instead of travelling with the + // tree. The base draws nothing and carves nothing. + renderItemGutter(itemRect, contentRect, treeItem, currentState); + if (contentRect.extent.x <= 0) + { + return; + } + + const S32 indent = resolveIndent(mIndentSize, contentRect.extent.y); + //indent to the focus level if(mFocusLevel >= 0) { - contentRect.point.x += (mFocusLevel * contentRect.extent.y); - contentRect.extent.x -= (mFocusLevel * contentRect.extent.y); + contentRect.point.x += (mFocusLevel * indent); + contentRect.extent.x -= (mFocusLevel * indent); - //convert this space to a line by crushing down the sides - S32 crush = mRound((contentRect.extent.y - 2) / 2); - RectI line = RectI(contentRect.point.x + crush, contentRect.point.y, 2, contentRect.extent.y); + // Crushed down to a line, hanging from the triangle's point. + S32 crush = focusLineOffset(contentRect.extent.y); + RectI line = RectI(contentRect.point.x + crush, contentRect.point.y, smFocusLineWidth, contentRect.extent.y); ColorI lineColor = currentState == SelectedState ? mProfile->getFillColor(NormalState) : mProfile->getFillColor(SelectedState); dglDrawRectFill(line, lineColor); //Remove indent - contentRect.point.x -= (mFocusLevel * contentRect.extent.y); - contentRect.extent.x += (mFocusLevel * contentRect.extent.y); + contentRect.point.x -= (mFocusLevel * indent); + contentRect.extent.x += (mFocusLevel * indent); } // Indent by level - contentRect.point.x += (treeItem->level * contentRect.extent.y); - contentRect.extent.x -= (treeItem->level * contentRect.extent.y); + contentRect.point.x += (treeItem->level * indent); + contentRect.extent.x -= (treeItem->level * indent); // Render open/close triangle if(obj) @@ -556,6 +694,10 @@ void GuiTreeViewCtrl::onRenderItem(RectI& itemRect, LBItem* item) contentRect.point.x += contentRect.extent.y; contentRect.extent.x -= contentRect.extent.y; + // The row's own picture, between the triangle and the words. The base draws + // nothing and carves nothing. + renderItemIcon(contentRect, treeItem, currentState); + renderText(contentRect.point, contentRect.extent, item->itemText, mProfile); } @@ -701,6 +843,7 @@ void GuiTreeViewCtrl::inspectObject(SimObject* obj) S32 id = addItemWithID(text, obj->getId(), obj); TreeItem* treeItem = grabItemPtr(id); treeItem->level = 0; + treeItem->iconFrame = getObjectIconFrame(obj); addBranches(treeItem, obj, 1); } @@ -722,6 +865,7 @@ void GuiTreeViewCtrl::addBranches(TreeItem* treeItem, SimObject* obj, U16 level) TreeItem* branch = grabItemPtr(index); branch->level = level; branch->trunk = treeItem; + branch->iconFrame = getObjectIconFrame(sub); treeItem->branchList.push_back(branch); addBranches(branch, sub, level + 1); @@ -800,7 +944,10 @@ void GuiTreeViewCtrl::refreshTree() StringTableEntry GuiTreeViewCtrl::getObjectText(SimObject* obj) { - char buffer[1024]; + // Empty rather than uninitialised: the fall-through at the bottom returns + // this buffer whether or not the block below filled it, and a null object + // used to hand the string table whatever was on the stack. + char buffer[1024] = { 0 }; if (obj) { if (isMethod("onGetObjectText")) @@ -840,23 +987,17 @@ StringTableEntry GuiTreeViewCtrl::getObjectText(SimObject* obj) return StringTable->insert(buffer, true); } -void GuiTreeViewCtrl::calculateHeaderExtent() +void GuiTreeViewCtrl::refreshItem(S32 index) { - if(mProfile) + TreeItem* treeItem = grabItemPtr(index); + if (!treeItem) { - GuiBorderProfile* topProfile = mProfile->getTopBorder(); - GuiBorderProfile* bottomProfile = mProfile->getBottomBorder(); - - S32 topSize = (topProfile) ? topProfile->getMargin(NormalState) + topProfile->getBorder(NormalState) + topProfile->getPadding(NormalState) : 0; - S32 bottomSize = (bottomProfile) ? bottomProfile->getMargin(NormalState) + bottomProfile->getBorder(NormalState) + bottomProfile->getPadding(NormalState) : 0; - - GFont* font = mProfile->getFont(); - S32 fontSize = (font) ? font->getHeight() : 0; - - S32 height = topSize + bottomSize + fontSize; - S32 width = mBounds.extent.x; - + return; } + + SimObject* obj = getItemObject(treeItem); + setItemText(index, getObjectText(obj)); + treeItem->iconFrame = getObjectIconFrame(obj); } void GuiTreeViewCtrl::updateSize() diff --git a/engine/source/gui/guiTreeViewCtrl.h b/engine/source/gui/guiTreeViewCtrl.h index f32b0faa2..ca6c48194 100755 --- a/engine/source/gui/guiTreeViewCtrl.h +++ b/engine/source/gui/guiTreeViewCtrl.h @@ -39,6 +39,12 @@ class GuiTreeViewCtrl : public GuiListBoxCtrl protected: SimObjectPtr mRootObject; S32 mIndentSize; + /// The sheet a row's icon is a frame of, and how big to draw it. Empty by + /// default: with no sheet a row asks script for nothing, draws nothing and + /// costs nothing, so a tree that wants no icons is exactly as it was. + StringTableEntry mIconImageAssetID; + AssetPtr mIconImageAsset; + S32 mIconSize; Point2I mTouchPoint; bool mDragActive; S32 mDragIndex; @@ -56,7 +62,7 @@ class GuiTreeViewCtrl : public GuiListBoxCtrl class TreeItem : public GuiListBoxCtrl::LBItem { public: - TreeItem() : isOpen(1), level(0), triangleArea(RectI()), isVisible(1), branchList(vector()), trunk(nullptr) { } + TreeItem() : isOpen(1), level(0), triangleArea(RectI()), isVisible(1), branchList(vector()), trunk(nullptr), iconFrame(-1) { } virtual ~TreeItem() { } bool isOpen; @@ -65,13 +71,89 @@ class GuiTreeViewCtrl : public GuiListBoxCtrl bool isVisible; vector branchList; TreeItem* trunk; + /// Which frame of the tree's icon sheet this row draws, or -1 for none. + /// Asked of script once when the row is built rather than every frame - + /// onRenderItem runs for every visible row of every frame, so a callback + /// here would be a console call per row per frame. + S32 iconFrame; }; -private: + /// The per-level indent step. Zero means "one row height", which is what the + /// tree has always done and so is the default; a positive value is used as + /// given. Static, and taking both numbers, so it can be tested away from a + /// canvas, a Sim and a GL context - the same reason GuiScrollCtrl's bar + /// arithmetic is two statics. + static S32 resolveIndent(S32 indentSize, S32 rowInnerHeight); + + /// Where an icon of iconSize draws inside contentRect, and what it costs in + /// width. Never enlarges: a row shorter than the art shrinks the art rather + /// than promising a slot it cannot hold. Answers false - consuming nothing - + /// when there is no room at all, so a cramped tree degrades to plain rows + /// instead of to text drawn over an icon. + static bool iconSlot(const RectI& contentRect, S32 iconSize, RectI& dstOut, S32& advanceOut); + + /// Where the focus line's 2px rule sits, measured from the left of the focus + /// level's slot. + /// + /// Centred on the TRIANGLE's square rather than on the indent step, because + /// the line hangs down from a container's triangle and should line up with + /// its point. The two were the same number until IndentSize became settable, + /// which is exactly how they silently stopped agreeing. + static S32 focusLineOffset(S32 rowInnerHeight); + + /// Width of the focus line itself. + static constexpr S32 smFocusLineWidth = 2; + + /// Air between the icon and the text it labels. Four rather than two: the art + /// on these sheets bleeds to the tile edge on purpose, so two pixels of gap + /// reads as none and the picture runs into the first letter. + /// + /// constexpr rather than const: a test asserting against it binds it to a + /// const reference, which would odr-use a plain static const and fail to link + /// for want of a definition. + static constexpr S32 smIconGap = 4; + +protected: + // Reachable by a subclass: a render hook is handed a TreeItem and needs the + // SimObject behind it. Neither is something a caller outside the hierarchy + // should be doing, so neither is public. TreeItem* grabItemPtr(S32 index); // The tree always stores a SimObject* in LBItem::itemData; recover it // safely (item may be null) so callers can dynamic_cast to the real type. SimObject* getItemObject(TreeItem* item); + + /// Two seams in a row, so a subclass can add to one without copying the whole + /// of onRenderItem - which could not be copied faithfully anyway, the focus + /// line being driven by private state. Both are handed contentRect by + /// reference and may carve space off its left; every step after them uses + /// what is left. + /// + /// renderItemGutter runs BEFORE the focus line and the depth indent, so what + /// it draws stays pinned to the row's left edge instead of travelling with + /// the tree. renderItemIcon runs between the triangle and the text. + virtual void renderItemGutter(const RectI& itemRect, RectI& contentRect, TreeItem* treeItem, GuiControlState currentState) { } + virtual void renderItemIcon(RectI& contentRect, TreeItem* treeItem, GuiControlState currentState); + + /// One frame of a sheet, drawn with whatever bitmap modulation is current. + /// + /// Deliberately not renderStretchedImageAsset: that one reads the asset off + /// the PROFILE, and its first act is dglClearBitmapModulation - which here + /// would throw away the row's font color. The sheets are white masks, the + /// modulation is already set for the row's state, so an icon inherits the + /// row's normal / highlight / selected / disabled ink for nothing. + void drawIconFrame(const RectI& dst, ImageAsset* sheet, U32 frame); + + /// Which frame of the sheet a row should wear, asked of script once while the + /// tree is being built. -1 - the answer when no sheet is set, no handler + /// exists, or the handler declines - means no icon and no width spent. + S32 getObjectIconFrame(SimObject* obj); + + void setIconImageAsset(const char* pImageAssetID); + inline StringTableEntry getIconImageAsset(void) const { return mIconImageAssetID; } + static bool setIconImage(void* obj, const char* data) { static_cast(obj)->setIconImageAsset(data); return false; } + static const char* getIconImage(void* obj, const char* data) { return static_cast(obj)->getIconImageAsset(); } + +private: // Commits a completed drag-reorder. Fully guarded: bails on any missing or // wrong-typed item rather than trusting itemData is a GuiControl/SimGroup. void reorderFromDrag(); @@ -140,7 +222,11 @@ class GuiTreeViewCtrl : public GuiListBoxCtrl void addBranches(TreeItem* treeItem, SimObject* obj, U16 level); void refreshTree(); StringTableEntry getObjectText(SimObject* obj); - void calculateHeaderExtent(); + /// Re-asks the inspected object for one row's text and icon. A row's picture + /// can change without the row moving - a bare GuiControl re-profiled from a + /// panel to a label is still the same object in the same place - so the two + /// have to be refreshable together. + void refreshItem(S32 index); virtual GuiListBoxCtrl::LBItem* createItem(); void setBranchesVisible(TreeItem* treeItem, bool isVisible); void setItemOpen(S32 index, bool isOpen); diff --git a/engine/source/gui/guiTreeViewCtrl_ScriptBinding.h b/engine/source/gui/guiTreeViewCtrl_ScriptBinding.h index 54d1a198c..5001d0bdc 100644 --- a/engine/source/gui/guiTreeViewCtrl_ScriptBinding.h +++ b/engine/source/gui/guiTreeViewCtrl_ScriptBinding.h @@ -115,4 +115,38 @@ ConsoleMethodWithDocs(GuiTreeViewCtrl, refreshItemText, ConsoleVoid, 3, 3, "(S32 object->setItemText(index, object->getObjectText(sub)); } +/*! Refreshes both the text and the icon of the item based on the inspected + object. A control's picture can change without the row moving - re-profiling + a bare GuiControl from a panel to a label is still the same object in the + same place - so prefer this over refreshItemText. + @param index The zero-based index of the item that will be updated. + @return No return value. +*/ +ConsoleMethodWithDocs(GuiTreeViewCtrl, refreshItem, ConsoleVoid, 3, 3, "(S32 index)") +{ + S32 index = dAtoi(argv[2]); + if (index < 0 || index >= object->mItems.size()) + { + Con::warnf("GuiTreeViewCtrl::refreshItem() - Invalid index given."); + return; + } + object->refreshItem(index); +} + +/*! Gets the icon frame the given item is wearing, or -1 if it has none. + @param index The zero-based index of the item. + @return The frame index into the tree's IconImage sheet. +*/ +ConsoleMethodWithDocs(GuiTreeViewCtrl, getItemIcon, ConsoleInt, 3, 3, "(S32 index)") +{ + S32 index = dAtoi(argv[2]); + if (index < 0 || index >= object->mItems.size()) + { + Con::warnf("GuiTreeViewCtrl::getItemIcon() - Invalid index given."); + return -1; + } + GuiTreeViewCtrl::TreeItem* treeItem = dynamic_cast(object->mItems[index]); + return treeItem ? treeItem->iconFrame : -1; +} + ConsoleMethodGroupEndWithDocs(GuiTreeViewCtrl) \ No newline at end of file diff --git a/engine/source/testing/tests/guiTreeRowLayoutTests.cc b/engine/source/testing/tests/guiTreeRowLayoutTests.cc new file mode 100644 index 000000000..d3a93265e --- /dev/null +++ b/engine/source/testing/tests/guiTreeRowLayoutTests.cc @@ -0,0 +1,354 @@ +//----------------------------------------------------------------------------- +// 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. +//----------------------------------------------------------------------------- + +// We don't want tests in a shipping version. +#ifndef TORQUE_SHIPPING + +#ifndef _UNIT_TESTING_H_ +#include "testing/unitTesting.h" +#endif + +#ifndef _GUICONTROL_H_ +#include "gui/guiControl.h" +#endif + +#ifndef _GUI_TREEVIEWCTRL_H +#include "gui/guiTreeViewCtrl.h" +#endif + +#ifndef _GUI_EDITOR_EXPLORERTREE_H_ +#include "gui/editor/guiEditorExplorerTree.h" +#endif + +//----------------------------------------------------------------------------- +// How a tree row spends its width, left to right. +// +// A row is a budget: two gutter columns, an indent per level, a triangle, an +// icon, and whatever is left is the text. Every one of those is arithmetic, and +// all of it used to be inline in onRenderItem where nothing could reach it. +// +// It cannot be tested there. Adding a row to a tree calls updateSize, which asks +// the profile for a font, which loads one, which registers a texture -- and this +// suite runs with no canvas and so no GL context to make one in. TextureManager +// asserts, and in a debug build an assert is a modal box, so the whole run hangs +// rather than fails. So the arithmetic is pulled out into statics that take +// everything they use, exactly as GuiScrollCtrl's bar arithmetic was, and the +// statics are what these tests call. They construct nothing. +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// resolveIndent -- how far one level of depth steps the row in. +// +// The step was always the row's inner height, hard-coded at three sites. It is +// now a field, and the danger in that is the default: mIndentSize sat unread for +// years holding 10, so wiring it up without resetting it would have silently +// re-indented every tree in the engine. Zero means "as before". +//----------------------------------------------------------------------------- + +TEST( GuiTreeRowLayoutTests, ZeroIndentMeansOneRowHeight ) +{ + ASSERT_EQ( GuiTreeViewCtrl::resolveIndent( 0, 22 ), 22 ) + << "Zero is the default, and the default must be the step the tree always used."; + + SUCCEED(); +} + +TEST( GuiTreeRowLayoutTests, APositiveIndentWins ) +{ + ASSERT_EQ( GuiTreeViewCtrl::resolveIndent( 12, 22 ), 12 ) + << "A tree that asks for a narrower step gets it, whatever the row height is."; + + SUCCEED(); +} + +TEST( GuiTreeRowLayoutTests, ANegativeIndentFallsBackToTheRowHeight ) +{ + ASSERT_EQ( GuiTreeViewCtrl::resolveIndent( -6, 22 ), 22 ) + << "Only a positive value is an instruction; anything else means 'as before'."; + + SUCCEED(); +} + +TEST( GuiTreeRowLayoutTests, IndentIsNeverNegative ) +{ + ASSERT_EQ( GuiTreeViewCtrl::resolveIndent( 0, 0 ), 0 ) + << "A row with no inside indents by nothing."; + ASSERT_EQ( GuiTreeViewCtrl::resolveIndent( 0, -4 ), 0 ) + << "A negative step would walk the tree backwards, one level at a time."; + + SUCCEED(); +} + +//----------------------------------------------------------------------------- +// focusLineOffset -- the rule that hangs from a container's triangle. +// +// It marks the branch the editor will drop into, and it runs down from that +// container's triangle, so it has to line up with the triangle's point. It did +// for free while the indent step and the row height were the same number: the +// line was centred in the indent slot and the triangle was drawn in a square of +// the row height, and nobody had to know which one they were centring on. +// +// Making IndentSize settable broke that silently. The line moved four pixels +// left of the triangle it belonged to and everything still "worked". Hence a +// static, and hence this: the invariant is that the line covers the centre of +// the triangle's square, whatever the indent happens to be. +//----------------------------------------------------------------------------- + +// The triangle is drawn in a square of the row's inner height, so its point is +// at half that. The line must cover it. +static bool lineCoversTriangleTip( const S32 rowInnerHeight ) +{ + const S32 start = GuiTreeViewCtrl::focusLineOffset( rowInnerHeight ); + const S32 end = start + GuiTreeViewCtrl::smFocusLineWidth; + const S32 tip = rowInnerHeight / 2; + return tip >= start && tip < end; +} + +TEST( GuiTreeRowLayoutTests, TheFocusLineHangsFromTheTrianglesPoint ) +{ + for( S32 rowInnerHeight = 4; rowInnerHeight <= 64; rowInnerHeight++ ) + { + ASSERT_TRUE( lineCoversTriangleTip( rowInnerHeight ) ) + << "row height " << rowInnerHeight << ": the line missed the triangle it hangs from."; + } + + SUCCEED(); +} + +TEST( GuiTreeRowLayoutTests, TheFocusLineDoesNotDependOnTheIndent ) +{ + // The point of the static. focusLineOffset takes the row height and nothing + // else, so no value of IndentSize can move the line off its triangle -- which + // is precisely what happened when the offset was computed from the indent. + ASSERT_EQ( GuiTreeViewCtrl::focusLineOffset( 20 ), 9 ); + ASSERT_EQ( GuiTreeViewCtrl::resolveIndent( 12, 20 ), 12 ) + << "A narrow indent is still honoured..."; + ASSERT_EQ( GuiTreeViewCtrl::focusLineOffset( 20 ), 9 ) + << "...and the line does not follow it."; + + SUCCEED(); +} + +TEST( GuiTreeRowLayoutTests, TheFocusLineNeverStartsLeftOfItsSlot ) +{ + ASSERT_EQ( GuiTreeViewCtrl::focusLineOffset( 0 ), 0 ); + ASSERT_EQ( GuiTreeViewCtrl::focusLineOffset( 1 ), 0 ) + << "A row too short to centre a 2px rule in must not push it into the slot before."; + ASSERT_EQ( GuiTreeViewCtrl::focusLineOffset( -8 ), 0 ); + + SUCCEED(); +} + +//----------------------------------------------------------------------------- +// iconSlot -- where the row's picture goes, and what it costs. +// +// Two promises: it never enlarges the art, and when it cannot fit it consumes +// nothing at all. The second is what stops a cramped tree drawing its text on +// top of its icons -- degrading to a plain row is legible, overlapping is not. +//----------------------------------------------------------------------------- + +TEST( GuiTreeRowLayoutTests, TheIconCentersInATallerRow ) +{ + RectI dst; + S32 advance = 0; + const bool fits = GuiTreeViewCtrl::iconSlot( RectI( 100, 50, 200, 22 ), 16, dst, advance ); + + ASSERT_TRUE( fits ); + ASSERT_EQ( dst.point.x, 100 ) << "It draws at the left edge of what is left."; + ASSERT_EQ( dst.point.y, 53 ) << "Six spare pixels, three above and three below."; + ASSERT_EQ( dst.extent.x, 16 ); + ASSERT_EQ( dst.extent.y, 16 ) << "A square: the art is square and must not be stretched."; + ASSERT_EQ( advance, 16 + GuiTreeViewCtrl::smIconGap ) << "The icon, plus one breath before the text."; + + SUCCEED(); +} + +TEST( GuiTreeRowLayoutTests, TheIconNeverEnlarges ) +{ + RectI dst; + S32 advance = 0; + const bool fits = GuiTreeViewCtrl::iconSlot( RectI( 0, 0, 200, 12 ), 16, dst, advance ); + + ASSERT_TRUE( fits ); + ASSERT_EQ( dst.extent.y, 12 ) << "A row shorter than the art shrinks the art, rather than blowing it up."; + ASSERT_EQ( dst.extent.x, 12 ) << "Still square."; + ASSERT_EQ( dst.point.y, 0 ) << "Nothing spare, so nothing to center."; + ASSERT_EQ( advance, 12 + GuiTreeViewCtrl::smIconGap ); + + SUCCEED(); +} + +TEST( GuiTreeRowLayoutTests, ANarrowRowGetsNoSlotAndPaysNothing ) +{ + RectI dst; + S32 advance = 0; + const bool fits = GuiTreeViewCtrl::iconSlot( RectI( 0, 0, 10, 22 ), 16, dst, advance ); + + ASSERT_FALSE( fits ) << "There is no room for the icon and a space after it."; + ASSERT_EQ( advance, 0 ) << "Consuming width it did not draw in would put the text under nothing."; + + SUCCEED(); +} + +TEST( GuiTreeRowLayoutTests, TheSlotFitsInExactlyItsOwnWidth ) +{ + RectI dst; + S32 advance = 0; + + ASSERT_TRUE( GuiTreeViewCtrl::iconSlot( RectI( 0, 0, 16 + GuiTreeViewCtrl::smIconGap, 22 ), 16, dst, advance ) ) + << "Exactly enough is enough."; + + ASSERT_FALSE( GuiTreeViewCtrl::iconSlot( RectI( 0, 0, 15 + GuiTreeViewCtrl::smIconGap, 22 ), 16, dst, advance ) ) + << "One pixel short is short. The gap is part of the cost, not a nicety to drop."; + + SUCCEED(); +} + +TEST( GuiTreeRowLayoutTests, NoIconSizeIsNoSlot ) +{ + RectI dst; + S32 advance = 0; + + ASSERT_FALSE( GuiTreeViewCtrl::iconSlot( RectI( 0, 0, 200, 22 ), 0, dst, advance ) ) + << "A tree with no sheet set asks for nothing and must be charged nothing."; + ASSERT_EQ( advance, 0 ); + + ASSERT_FALSE( GuiTreeViewCtrl::iconSlot( RectI( 0, 0, 200, 0 ), 16, dst, advance ) ) + << "A row with no height has nowhere to put it."; + ASSERT_EQ( advance, 0 ); + + SUCCEED(); +} + +//----------------------------------------------------------------------------- +// The Explorer's two gutter columns. +// +// The eye and the padlock are a fixed rail at the row's left edge, and the only +// thing keeping the picture and the click in agreement is that both ask the same +// function the same question. Nothing on screen would report them disagreeing: +// the boxes would draw where they always did and the clicks would land a few +// pixels off, toggling the wrong control or nothing at all. +// +// So the geometry is checked here, from both sides of every boundary. An +// off-by-one in columnAt reads as "the eye is fussy about where you click", +// which is the kind of thing that gets lived with rather than reported. +//----------------------------------------------------------------------------- + +// Where the rail starts. Nonzero on purpose: with the editor's treeViewProfile +// the inset is 0, so testing only at 0 would pass on arithmetic that had dropped +// the left edge entirely. +static const S32 GutterLeft = 7; + +TEST( GuiTreeRowLayoutTests, TheEyeColumnIsLeftmost ) +{ + RectI eye, lock; + GuiEditorExplorerTree::getGutterCells( GutterLeft, 40, 22, eye, lock ); + + ASSERT_EQ( eye.point.x, GutterLeft ) + << "Every layers panel anyone has used puts visibility first. This is that order."; + ASSERT_LT( eye.point.x, lock.point.x ); + + SUCCEED(); +} + +TEST( GuiTreeRowLayoutTests, TheTwoColumnsAbutAndSpanTheRow ) +{ + RectI eye, lock; + GuiEditorExplorerTree::getGutterCells( GutterLeft, 40, 22, eye, lock ); + + ASSERT_EQ( eye.point.x + eye.extent.x, lock.point.x ) << "No seam between them."; + ASSERT_EQ( eye.extent.x, GuiEditorExplorerTree::smColumnWidth ); + ASSERT_EQ( lock.extent.x, GuiEditorExplorerTree::smColumnWidth ); + ASSERT_EQ( eye.extent.x + lock.extent.x, GuiEditorExplorerTree::getGutterWidth() ) + << "What the renderer carves off the row must be what the two cells actually occupy."; + + ASSERT_EQ( eye.point.y, 40 ); + ASSERT_EQ( eye.extent.y, 22 ) << "The full row height, so the dividers read as continuous rules."; + ASSERT_EQ( lock.point.y, 40 ); + ASSERT_EQ( lock.extent.y, 22 ); + + SUCCEED(); +} + +TEST( GuiTreeRowLayoutTests, EveryColumnBoundaryIsWhereItLooks ) +{ + const S32 w = GuiEditorExplorerTree::smColumnWidth; + + ASSERT_EQ( GuiEditorExplorerTree::columnAt( GutterLeft - 1, GutterLeft ), + GuiEditorExplorerTree::GutterNone ) << "One pixel left of the rail is not the rail."; + ASSERT_EQ( GuiEditorExplorerTree::columnAt( GutterLeft, GutterLeft ), + GuiEditorExplorerTree::GutterHidden ) << "Its first pixel is the eye."; + ASSERT_EQ( GuiEditorExplorerTree::columnAt( GutterLeft + w - 1, GutterLeft ), + GuiEditorExplorerTree::GutterHidden ) + << "Including its divider: the whole cell is clickable, not just the box."; + ASSERT_EQ( GuiEditorExplorerTree::columnAt( GutterLeft + w, GutterLeft ), + GuiEditorExplorerTree::GutterLocked ) << "And the next pixel is the padlock."; + ASSERT_EQ( GuiEditorExplorerTree::columnAt( GutterLeft + ( 2 * w ) - 1, GutterLeft ), + GuiEditorExplorerTree::GutterLocked ); + ASSERT_EQ( GuiEditorExplorerTree::columnAt( GutterLeft + ( 2 * w ), GutterLeft ), + GuiEditorExplorerTree::GutterNone ) + << "Past the rail is the tree, where a click selects rather than toggles."; + + SUCCEED(); +} + +TEST( GuiTreeRowLayoutTests, AFarawayPointIsInNoColumn ) +{ + ASSERT_EQ( GuiEditorExplorerTree::columnAt( -400, GutterLeft ), + GuiEditorExplorerTree::GutterNone ); + ASSERT_EQ( GuiEditorExplorerTree::columnAt( 4000, GutterLeft ), + GuiEditorExplorerTree::GutterNone ); + + SUCCEED(); +} + +TEST( GuiTreeRowLayoutTests, TheBoxSitsInsideItsCellClearOfTheDivider ) +{ + RectI eye, lock; + GuiEditorExplorerTree::getGutterCells( GutterLeft, 40, 22, eye, lock ); + const RectI box = GuiEditorExplorerTree::getBoxRect( eye ); + + ASSERT_EQ( box.extent.x, GuiEditorExplorerTree::smBoxSize ); + ASSERT_EQ( box.extent.y, GuiEditorExplorerTree::smBoxSize ) << "Square: the art is square."; + ASSERT_GE( box.point.x, eye.point.x ) << "Inside its own cell."; + ASSERT_LT( box.point.x + box.extent.x, eye.point.x + eye.extent.x ) + << "Clear of the divider, which owns the cell's last pixel."; + ASSERT_EQ( box.point.y, 40 + 3 ) << "Six spare pixels of row, three above and three below."; + + SUCCEED(); +} + +TEST( GuiTreeRowLayoutTests, TheBoxShrinksIntoAShortRowRatherThanOverflowing ) +{ + RectI eye, lock; + GuiEditorExplorerTree::getGutterCells( GutterLeft, 0, 10, eye, lock ); + const RectI box = GuiEditorExplorerTree::getBoxRect( eye ); + + ASSERT_EQ( box.extent.y, 10 ) << "A row too short for the art gets the art shrunk to it."; + ASSERT_EQ( box.extent.x, 10 ) << "Still square, so the icon is not distorted."; + ASSERT_GE( box.point.y, 0 ); + ASSERT_LE( box.point.y + box.extent.y, 10 ) << "And it stays within the row it is in."; + + SUCCEED(); +} + +#endif // TORQUE_SHIPPING diff --git a/tests/smoke/treeIcons.cs b/tests/smoke/treeIcons.cs new file mode 100644 index 000000000..46ee62abf --- /dev/null +++ b/tests/smoke/treeIcons.cs @@ -0,0 +1,114 @@ +//----------------------------------------------------------------------------- +// A tree row can wear a small picture, drawn between the triangle and the text. +// +// The frame is pulled from script ONCE, while the tree is building itself, and +// cached on the row -- onRenderItem runs for every visible row of every frame, +// so asking per draw would be a console call per row per frame. That caching is +// the whole risk in the feature: a row whose picture should have changed but +// whose cache was never invalidated is wrong in a way nothing else reports. +// +// This has to run with a canvas. Adding a row calls updateSize, which asks the +// profile for a font, which loads one, which registers a texture -- so the row +// arithmetic is unit tested (guiTreeRowLayoutTests.cc) and the plumbing is here. +//----------------------------------------------------------------------------- + +setLogMode(2); +setScriptExecEcho(false); +trace(false); +$Scripts::ignoreDSOs = true; +setCompanyAndProduct("Torque Game Engines", "Torque2D"); +ModuleDatabase.EchoInfo = false; +AssetDatabase.EchoInfo = false; +AssetDatabase.IgnoreAutoUnload = true; + +testExec("editor/main.cs"); + +$Pass = 0; +$Fail = 0; + +function iconCheck(%label, %condition) +{ + if(%condition) + { + $Pass++; + echo("TICO PASS: " @ %label); + } + else + { + $Fail++; + echo("TICO FAIL: " @ %label); + } +} + +// The tree under test answers from here. Returning "" is how a handler declines +// a row, which must read the same as having no handler at all. +function IconTestTree::onGetItemIcon(%this, %obj) +{ + if(%obj.iconDeclines) + { + return ""; + } + return %obj.wantIcon; +} + +schedule(2500, 0, "icoRun"); + +function icoRun() +{ + // A little tree of our own rather than the editor's, so this tests the base + // class and not the Gui Editor's use of it. + %root = new SimGroup(); + %a = new SimObject(); %a.wantIcon = 7; + %b = new SimObject(); %b.wantIcon = 3; + %c = new SimObject(); %c.iconDeclines = true; + %root.add(%a); + %root.add(%b); + %root.add(%c); + + %tree = new GuiTreeViewCtrl() + { + class = "IconTestTree"; + Position = "0 0"; + Extent = "200 200"; + }; + ThemeManager.setProfile(%tree, "treeViewProfile"); + Canvas.getContent().add(%tree); + + // --- With no sheet set, the question is never asked. + %tree.inspect(%root); + iconCheck("no sheet means no icon on the root", %tree.getItemIcon(0) == -1); + iconCheck("no sheet means no icon on a branch", %tree.getItemIcon(1) == -1); + + // --- Set one, and the rows pick their frames up on the next build. + %tree.IconImage = "EditorCore:editorIcons16"; + iconCheck("the sheet round-trips through the field", + %tree.IconImage $= "EditorCore:editorIcons16"); + + %tree.refresh(); + iconCheck("a branch wears the frame script named", %tree.getItemIcon(1) == 7); + iconCheck("each branch answers for itself", %tree.getItemIcon(2) == 3); + iconCheck("a declined row wears none", %tree.getItemIcon(3) == -1); + + // --- The cache is the risk. A row whose answer changes must be refreshable + // without rebuilding the whole tree, because that is what the properties + // pane does when a control is re-profiled under a selection. + %b.wantIcon = 21; + iconCheck("the cache does not follow the object on its own", %tree.getItemIcon(2) == 3); + %tree.refreshItem(2); + iconCheck("refreshItem re-asks", %tree.getItemIcon(2) == 21); + iconCheck("refreshItem left its neighbours alone", %tree.getItemIcon(1) == 7); + + // --- And clearing the sheet turns the feature back off. + %tree.IconImage = ""; + %tree.refresh(); + iconCheck("clearing the sheet stops the asking", %tree.getItemIcon(1) == -1); + + // --- Out-of-range indices report rather than crash. + iconCheck("an index past the end answers -1", %tree.getItemIcon(999) == -1); + + %tree.deleteObject(); + %root.deleteObject(); + + echo("TICO RESULT: " @ $Pass @ " passed, " @ $Fail @ " failed"); + quit(); +} From e638819820ebd5ebf193d2754aa96c739bab4f85 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Sun, 2 Aug 2026 21:33:08 -0400 Subject: [PATCH 28/55] Gui Editor: the control icons at 16px, for the Explorer tree controlIcons16.png, 128x64, the same 31 drawings in the same 8x4 grid as the 64 and 128 sheets, so one index still names the same icon in all three. sheetFor gains a tier on the same rule as before -- each threshold is that sheet's own resolution, because past it the sheet would have to be enlarged and enlarging is what looks soft. The art was designed for this. icons.py places everything in a 64-unit square with primary structure on multiples of four, which is a whole pixel at 16, and nothing is ever stroked -- a frame is a filled rect with a smaller rect knocked out. All 31 pass the build script's own bbox and ink-ramp assertions at 16. What did NOT survive the trip down was the resample. render_alpha finished with Image.LANCZOS, which is a sharpening kernel: it has negative lobes, so it rings. At 64 and 128 the thinnest primary feature is 4 units, which is 4px or 8px, broad enough that its middle still reaches full alpha. At 16 a 4-unit bar IS one pixel, and Lanczos resolved it at alpha 221 with 22-alpha ghosts either side. Measured on the sheet: GuiTextEditCtrl at 16px had ZERO fully-opaque pixels and 202 mid-alpha ones, against 64 and 4 under Image.BOX, which over an 8x supersample is exact area averaging. The generator now picks the filter by size, so the shipped 64 and 128 PNGs are byte-identical. Worth writing down because the two are easy to conflate: landing on a pixel boundary and surviving the resample are separate problems, and the 4-unit grid rule only ever promised the first. The generator had also drifted from what shipped -- commit d2845d78's GuiTabPageCtrl refusal and the refused-class guard around groupKeys existed only in the generated file on disk, so the next regeneration would have silently reverted them. Fixed there first and proved a no-op before the new size was added. The refusal needed a set of its own rather than joining NOT_PLACED_BY_HAND: the build reports stale = drawn - placeable, so a class that is refused but still DRAWN put in that set reads as a dead tile. palette.cs checks both new thresholds from either side, and that every sheet sheetFor can name actually resolves with all 32 cells -- a missing image renders as nothing rather than throwing, so an unregistered asset would show up as blank space in the tree and nowhere else at all. controlIcons.cs gains a third page for the same reason. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qi2pZ9UzF5rsLCNVErpzwt --- .../images/controlIcons16.asset.taml | 8 +++++ editor/GuiEditor/images/controlIcons16.png | Bin 0 -> 2775 bytes .../scripts/GuiEditorControlIcons.cs | 28 +++++++++++------- tests/shots/controlIcons.cs | 18 ++++++++--- tests/smoke/palette.cs | 24 +++++++++++++-- 5 files changed, 62 insertions(+), 16 deletions(-) create mode 100644 editor/GuiEditor/images/controlIcons16.asset.taml create mode 100644 editor/GuiEditor/images/controlIcons16.png 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 0000000000000000000000000000000000000000..fb7593ee0dbf61fff1aa535567732da4c47f8c69 GIT binary patch literal 2775 zcmV;|3Mlo7P)0y)}_P`SkZ+EtT%rpAldo#21&O7_- zZXZ~7cV^ys=Y8MjeSUnO=Y7YiR;zSq<z^z@&D51Ms>0&S>Wf zKwdkR2Xfl6vU~^CUyJzuu2c1gV_3Id_L_8R94Y~i$vqs)KDZWhpu^4;fHwjrK`kl+M*=2FV;SK7 z{rh?+c1fqqXbs;WojzG71~4^7l9UOeZ)pQv2KLH89|4X7Yk^+_JpoM6m0<8yCMW{^ zA?L?{-7)Ud1snyQkpCZwRGX|-Fyp$eer%64=rC|bIX)`qh62QW67tLCnk)rJmBIWr zFalhZb1wiN%kO^RlJv{4QtlIrx-)X$pvjY!iKwd3AUX|Pk^X#6dG0H~;|dc5mUm`n zXXA*`ebV4TjRDdS!BtBMP_V2&r_AVi8PL@s*Ukg$fS)RoAe#acIsudL`!bMDfW%bk zyfExB;D8)E1sno;fv2O&J9qEitu=`IYN&u_Gk|UVhm^tmF7V$P*Y5(l71gNI4&ZV~ z+3mnTLfVp*XXpXOf!_hIEA0!D0IvaG27Uyblw;2W=cFxPiYlL}R4P%8^(}Yb2%Mz# zD4)W0zF6ND^IpSJ=5PoTB%yH;;qL(xVt>3Iqdl+L^qX_%&Yh@PUrBH*emIqXG&cGj_QEK_YNn3zt6WRc$1vpvB zlr_9gy*}Ft65xXPu}NsZFhGC8@?$)T^d&97JD@%N20nbFX-cq%hxq^(n%seE+1tRo zHOjn%2bp4lLFjI-V+dO#9>D(s%2rc&uB3QM9Pn(5d>+~W{Rulj??B)0F9LlbzsEz$ z`}A%IFp>pc1>Q-JLr{+a>Bh^L0x!C^Jp^ve*Lc>;coE1U;2>V&NgSF3P5{3MxZj|t z)3Ug?I;?#_eWo;RfLx5b)m}e>BE3`b;o`7xTZNoI5KMWke*yko-T37u!IQ#MnrH(~2HB$kJ$2 zF{HAhbWT`#m3GQNcZ;3ir>M=hfmJ@H7=dkt1DMQ~w`5QbCR71I<<&g7UDrXMDe>1h|#29gJC@0M0^(l@RLFarV`f> zoySW#2l$P+lCzmgrP6QdM)e636cbpZKI7Qbe-RIxKdl6wNI8~XuY06Xf86oQ;~}j6 zICH-h@+vNo26R3|q zt?~JUd2$6&A+sUZtY&G%FHHtIW6&pg3p6utvO0LZe5SM@hpz(Crd*2Fe8DhJPF?H~ zgi)Mqf&?%z0+Xl!KVP66CIQ|Cz6LzWV%YWV7>(+|!|Oi)t~S!8O$d%;BXoQr<_;sR zKqiyOXZXEA5};4?yzc^?GPrRMUW!ykEc_k91iFFeq_oXUF6Gs^K@y<1T`(O36)+)E zVXrO$RlY zp*BEXd!P+?$&Uld?EW!c$To;RP<;Ec{I9nckOn?(BTXJ_pkW+k1SWcZb$@_vyp0Be z5WVMsH^mK{#oJnKN~!#!GAMK6mdz>d*@TwlF@d+yLT?K3eA}eYygYj`V(XS&G!}|B zH3A(&hVt7x?n{6S@P`OE_dU%+_)~;yv{w5jVDFu65&IFD0zp_VmpU6^1dfp2(yUq% zh-T&lcFU}sF!aT3Q^U#%)7hwif`uDMn?u3Y`vZ(Ad(4>70Tu^?dM$!M)r8jnSV;P{ zsSPk30tD@kNKd1MvXHBW!JwmlFsPFcU{AI{&>2>EK0ufby`-s^odGsVsg71S4TQm? z!s(HF2|12kI)YQFRH`w+g{nbjuT564N>|0@oD(DTiJ15BMctS|kgGpLT#ltMfVEVh zU-qiBRX3=6?|HRvYYYzCgW-#jY z>tNPS!7sAfa;{v!vHp+%(9W0)5NGiH{_%k{|N9wb@E;E;dqtR_rFRG!i2yEg0V{uA z#M{|2s^fkrfYXfJBV%b{XG4xz_n|@m_?UwK?f1_d(JO)L{=NVk6@Z}x(3g!5)EJ0z zL?@TZaz1{sPS?h92YbZYpVJn~ekM%trr0Z`W=ViXphX3mOU0B2 zHZJ*CKu^HJ*UuD5;Cl|KGJ$jJ)~z(WfMy$EX_QZ*0;(y6v*W6z+d30$+O%oe>=e3E zRX|*t7Mp25-Ye|Nw1^F`TvR}-(wwM?9PrDKw}~tZ36Q7x8zT}*lbb0B%-mAJA|OTKf%w3Sg6zc4vr~IQsB|+e9Ls+7qB8}$>49@ dy4C82{|7{|P~g&?x- 64) ? "GuiEditor:controlIcons128" : "GuiEditor:controlIcons64"; + if(%tileSize > 64) + { + return "GuiEditor:controlIcons128"; + } + if(%tileSize > 16) + { + return "GuiEditor:controlIcons64"; + } + return "GuiEditor:controlIcons16"; } diff --git a/tests/shots/controlIcons.cs b/tests/shots/controlIcons.cs index 1260f7ad3..63003fe3b 100644 --- a/tests/shots/controlIcons.cs +++ b/tests/shots/controlIcons.cs @@ -13,7 +13,16 @@ // shot shows, and nothing else would. // // Page 0 is the 128 sheet at 96px, which is what the grid view will draw. Page 1 -// is the 64 sheet at 48px, which is the row view. +// is the 64 sheet at 48px, which is the row view. Page 2 is the 16 sheet, which +// the Explorer tree draws. +// +// The tiles keep constrainProportions, so page 2 draws at 16px rather than being +// stretched to fill -- which is the point of it. Whether an icon still reads once +// one pixel stands for four design units is the question, and the answer has to +// be looked at at the size it will really be. Seeing all 31 at once is also the +// only thing that catches the new asset failing to register: a missing image +// renders as nothing rather than throwing, so that failure is a page of labels +// with blank space above them. setLogMode(2); setScriptExecEcho(false); trace(false); @@ -26,7 +35,7 @@ schedule(2500, 0, "controlIconsShot"); $controlIcons::page = 0; -$controlIcons::pages = 2; +$controlIcons::pages = 3; function controlIconsShot() { @@ -45,8 +54,9 @@ function controlIconsShot() %big = ($controlIcons::page == 0); %tile = %big ? 96 : 48; - %sheet = %icons.sheetFor(%tile); - %cell = %big ? 128 : 64; + // Page 2 asks sheetFor for the 16 sheet and then draws it four times up. + %sheet = %icons.sheetFor(($controlIcons::page == 2) ? 16 : %tile); + %cell = %big ? 128 : (($controlIcons::page == 2) ? 16 : 64); %page = new GuiControl() { Position = "0 0"; Extent = "1024 768"; }; ThemeManager.setProfile(%page, "panelProfile"); diff --git a/tests/smoke/palette.cs b/tests/smoke/palette.cs index c4d44a85f..2459a5781 100644 --- a/tests/smoke/palette.cs +++ b/tests/smoke/palette.cs @@ -143,11 +143,31 @@ function palTableChecks() palCheck("an ordinary class asks for no category", %icons.categoryFor("GuiButtonCtrl") $= ""); palCheck("a class key builds its own class", %icons.classFor("GuiSliderCtrl") $= "GuiSliderCtrl"); - // The sheet threshold is the small sheet's own resolution: above 64 the big - // sheet is shrunk rather than the small one enlarged. + // Each threshold is that sheet's own resolution: past it the next sheet up is + // shrunk rather than this one enlarged. Both boundaries are checked from + // either side, because an off-by-one here reads as "the icons went soft" + // rather than as a failure. palCheck("64px art takes the 64 sheet", %icons.sheetFor(64) $= "GuiEditor:controlIcons64"); palCheck("80px art takes the 128 sheet", %icons.sheetFor(80) $= "GuiEditor:controlIcons128"); palCheck("32px art takes the 64 sheet", %icons.sheetFor(32) $= "GuiEditor:controlIcons64"); + palCheck("17px art takes the 64 sheet", %icons.sheetFor(17) $= "GuiEditor:controlIcons64"); + palCheck("16px art takes the 16 sheet", %icons.sheetFor(16) $= "GuiEditor:controlIcons16"); + + // And every sheet it can name has to exist. A missing image renders as + // nothing rather than throwing, so an unregistered asset would show up as + // blank space in the tree and nowhere else at all. + for(%i = 0; %i < 3; %i++) + { + %size = getWord("16 64 128", %i); + %sheet = %icons.sheetFor(%size); + %asset = AssetDatabase.acquireAsset(%sheet); + palCheck(%sheet @ " is a declared asset", isObject(%asset)); + if(isObject(%asset)) + { + palCheck(%sheet @ " carries all 32 cells", %asset.getFrameCount() == 32); + AssetDatabase.releaseAsset(%sheet); + } + } palPaletteChecks(); } From c7eb636ad814887dbd1a4e4e76b85339cb9cc07a Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Sun, 2 Aug 2026 21:33:49 -0400 Subject: [PATCH 29/55] Gui Editor: hidden and locked leave the properties pane for the Explorer They were the first two of six icon toggles in the pane's header, beside Visible, Active, Accepts Input and Accepts Children. Those four change the Gui a player will run. hidden and locked change your view of it while you work -- neither is ever written to a file -- and standing them next to the four that ARE the document read as a promise that they were the same kind of thing. A 36px gap was the only thing saying otherwise. They are now two columns down the left of the Explorer tree, in the manner of a layers panel: the eye is shown when a control is NOT hidden, the padlock when it IS locked, so a tidy Gui is a column of eyes and a column of nothing, and a whole branch reads at a glance. The columns do not indent with the tree -- they are a fixed rail -- and the boxes and dividers are drawn in the profile's hover fill, which is near enough the row fill to stay quiet. The cell under the pointer steps up to the selected fill, which is the only thing saying the rail can be clicked at all: on an unlocked, visible control both boxes are empty. The root row is the simulated canvas, stage furniture rather than a control in the document, so it draws no boxes. It still draws its dividers, or the rail would start one row down and read as a column that had failed to paint. Each row also gains a 16px picture of its own class, between the triangle and the text. Which one is onGetItemIcon's answer, and answering it needed something that did not exist: a way back from a live control to a palette entry. The two are not the same. 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. keyFor asks the pane's currentCategory, which is what keeps the Category dropdown and the row icon from disagreeing about the same control. onPostApply moves to refreshItem so a re-profiled control's picture follows it. The pane's toggle row keeps the four that remain and closes up to the front. The gap between them is gone with the boundary it marked. editorToggles() must keep naming both fields, and now says why. Removing the two buttons does not remove the two fields: buildOtherSection sweeps up every persist field no section claimed, so dropping them there would file them under "Other" as generic checkboxes -- same working state, now among the leftovers, which is worse than where they started. inspectorPane.cs asserts it directly rather than leaving it to be rediscovered. Its two lockedButton checks move to buttons that still exist: the box geometry to visibleButton, and the round-trip and disabled-toggle guard to activeButton, which has a true on/off icon pair and so can also prove the icon follows the value. explorerGutter.cs covers the rest. The central promise is a negative -- clicking a box toggles the flag and does NOT select the row -- and calling the toggle from script would skip the very code that could break it, so those three clicks are posted for real. The engine works out where each box landed and writes the point out; a hard-coded coordinate that drifted off the box would report a control that never toggled, which is exactly what a broken hit test reports, and the test would be lying either way. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qi2pZ9UzF5rsLCNVErpzwt --- .../GuiEditor/scripts/GuiEditorControlSpec.cs | 13 +- .../scripts/GuiEditorExplorerTree.cs | 44 +++- .../scripts/GuiEditorExplorerWindow.cs | 40 ++- .../GuiEditor/scripts/GuiEditorHeaderBlock.cs | 50 ++-- .../scripts/GuiEditorInspectorPane.cs | 5 +- tests/shots/explorerTree.cs | 187 +++++++++++++ tests/smoke/explorerGutter.cs | 248 ++++++++++++++++++ tests/smoke/explorerGutter.input.ps1 | 52 ++++ tests/smoke/inspectorPane.cs | 53 ++-- 9 files changed, 637 insertions(+), 55 deletions(-) create mode 100644 tests/shots/explorerTree.cs create mode 100644 tests/smoke/explorerGutter.cs create mode 100644 tests/smoke/explorerGutter.input.ps1 diff --git a/editor/GuiEditor/scripts/GuiEditorControlSpec.cs b/editor/GuiEditor/scripts/GuiEditorControlSpec.cs index 951966295..9c7e239cf 100644 --- a/editor/GuiEditor/scripts/GuiEditorControlSpec.cs +++ b/editor/GuiEditor/scripts/GuiEditorControlSpec.cs @@ -398,14 +398,21 @@ return "easeFillColorHL easeTimeFillColorHL easeFillColorSL easeTimeFillColorSL"; } -// The toggles the header draws as icons instead of rows. Visible, Active and -// useInput are what the control does when the game runs; hidden and locked are -// what the editor does with it. +// 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"; diff --git a/editor/GuiEditor/scripts/GuiEditorExplorerTree.cs b/editor/GuiEditor/scripts/GuiEditorExplorerTree.cs index d3db4fd96..5a3e17478 100644 --- a/editor/GuiEditor/scripts/GuiEditorExplorerTree.cs +++ b/editor/GuiEditor/scripts/GuiEditorExplorerTree.cs @@ -58,12 +58,16 @@ GuiEditor.undoRecorder.commitHierarchy("Reparent Control"); } +// 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); } } @@ -74,4 +78,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 6d0b9be95..88e0b553b 100644 --- a/editor/GuiEditor/scripts/GuiEditorExplorerWindow.cs +++ b/editor/GuiEditor/scripts/GuiEditorExplorerWindow.cs @@ -21,17 +21,53 @@ 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"; + + // 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); // 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 diff --git a/editor/GuiEditor/scripts/GuiEditorHeaderBlock.cs b/editor/GuiEditor/scripts/GuiEditorHeaderBlock.cs index ac4c34dca..61d43621e 100644 --- a/editor/GuiEditor/scripts/GuiEditorHeaderBlock.cs +++ b/editor/GuiEditor/scripts/GuiEditorHeaderBlock.cs @@ -22,12 +22,16 @@ // 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 split on purpose. hidden and locked are icon buttons, which -// is what they want to be and what the icon sheet has art for (an open and a -// closed padlock). Visible, Active and Accepts Input stay labelled checkboxes: -// the sheet has an eye, but nothing that reads as "active" or "accepts input" -// without a caption -- and a row of icons that each need a tooltip to be -// understood is worse than three short words. +// 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) @@ -94,44 +98,32 @@ %this.add(%row); %this.toggleRow = %row; - // Toggles rather than buttons: these hold a value, so they are checkboxes - // wearing an icon (GuiEditorToggleIcon). frameOn is the icon for the "on" - // reading -- a closed padlock against an open one, and an x in a circle - // against a tick. The x/tick pair is still a placeholder: an eye and a - // struck-through eye is what "hidden" actually wants, and the sheet now - // carries both ($EditorIcon::eye and ::invisible_light). Left alone here - // because hidden is moving out to its own Explorer tree column, and that is - // the change that should pick the icon. - %this.hiddenButton = %this.makeIconToggle(%row, 4, "hidden", "Hidden", - $EditorIcon::round_delete, $EditorIcon::round_checkmark, - "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.", - "Drawn on the canvas as it will be in the game."); - %this.lockedButton = %this.makeIconToggle(%row, 32, "locked", "Locked", - $EditorIcon::padlock_closed, $EditorIcon::padlock_open, - "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.", - "Can be picked and dragged on the canvas."); - // 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. Six icons fit where - // two captions did. + // 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. - %this.visibleButton = %this.makeIconToggle(%row, 68, "Visible", "Visible", + // + // 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, 96, "Active", "Active", + %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, 124, "useInput", "Accepts Input", + %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, 152, "isContainer", "Accepts Children", + %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."); diff --git a/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs b/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs index 05207b51d..8388bb7af 100644 --- a/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs +++ b/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs @@ -1167,9 +1167,8 @@ class = "GuiProfileEditorFieldRow"; { %header = %this.header; - %header.hiddenButton.setValue(%this.target.hidden); - %header.lockedButton.setValue(%this.target.locked); - + // 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); diff --git a/tests/shots/explorerTree.cs b/tests/shots/explorerTree.cs new file mode 100644 index 000000000..d0fee6c16 --- /dev/null +++ b/tests/shots/explorerTree.cs @@ -0,0 +1,187 @@ +// The Gui Editor's Explorer tree, with a Gui deep enough to show what the two +// gutter columns cost and what the rows look like once they carry a picture. +// +// This is the proof no assertion can give. The boxes are drawn in the profile's +// HOVER fill, which is deliberately almost the row fill -- the whole point is +// that the rail is quiet -- and "quiet but findable" is a judgement a person has +// to make by looking. Same for the 16px control icons at their real size on the +// real theme, and for how much of a 228px tree is left for text at depth. +// +// Two controls are locked and two are hidden, so both columns show a mix rather +// than a run of one thing, and one row is selected -- which is the case that has +// already been wrong once. +// +// The eye and the padlock are drawn on a box whose colour does NOT change with +// selection, so an icon that took the row's font colour like the text does put +// selected-text ink on an unselected background and all but disappeared. One +// shot per editor theme, because the amount by which that is wrong depends +// entirely on how far apart a theme's selected and normal colours are: it is +// nearly invisible under some and merely odd under others, and a single shot +// would not have shown it. +setLogMode(2); +setScriptExecEcho(false); +trace(false); +$Scripts::ignoreDSOs = true; +setCompanyAndProduct("Torque Game Engines", "Torque2D"); +ModuleDatabase.EchoInfo = false; +AssetDatabase.EchoInfo = false; + +testExec("editor/main.cs"); + +// screenShot does not create its folder, and fails by logging rather than +// throwing -- so a missing folder is a silent no-shot. +createPath(testRoot("shots/")); + +schedule(2500, 0, "expTreeOpenProject"); + +function expTreeOpenProject() +{ + ProjectManager.setProjectFolder("PlanetX"); + EditorCore.projectSelector.onProjectSelected(pathConcat(getMainDotCsDir(), "PlanetX")); + schedule(2500, 0, "expTreeOpenEditor"); +} + +// Pages register in load order: EditorConsole, ProjectManager, AssetAdmin, +// GuiEditor. +function expTreeOpenEditor() +{ + EditorCore.toggleEditor(); + EditorCore.tabBook.selectPage(3); + schedule(1500, 0, "expTreeClear"); +} + +// Start empty, and in a step of its own. PlanetX opens with a Gui already +// loaded and finishes displaying it after the editor page has come up, so +// clearing in the same frame as the build leaves that Gui's controls in the +// tree -- and the shot becomes a picture of PlanetX rather than of a shape +// chosen to show the columns. +function expTreeClear() +{ + GuiEditor.NewGui(); + schedule(500, 0, "expTreeSetup"); +} + +function expTreeSetup() +{ + %root = GuiEditor.rootGui; + + // A backdrop with a panel inside it, and rows inside that: three levels, so + // the indent budget is visible rather than theoretical. + // A spread of classes, so the row icons are not all the same picture. Names + // come from GuiEditorControlIcons.cs -- there is no GuiTextCtrl in this + // engine, and a class that does not exist fails silently: eval hands back 0, + // add() takes it, and the row simply never appears. + %backdrop = expAdd("GuiControl", %root, "8 8", "420 300", "backdrop"); + %panel = expAdd("GuiPanelCtrl", %backdrop, "12 12", "300 240", "panel"); + %title = expAdd("GuiControl", %panel, "8 8", "200 24", "title"); + %edit = expAdd("GuiTextEditCtrl", %panel, "8 40", "200 28", "nameBox"); + %list = expAdd("GuiListBoxCtrl", %panel, "8 76", "200 90", "choices"); + %check = expAdd("GuiCheckBoxCtrl", %panel, "8 176", "90 24", "agree"); + %ok = expAdd("GuiButtonCtrl", %panel, "8 206", "90 30", "okButton"); + %slider = expAdd("GuiSliderCtrl", %backdrop, "12 260", "300 24", "volume"); + + // A mix down both columns, so neither is a run of one thing. + %title.locked = true; + %list.locked = true; + %edit.hidden = true; + %ok.hidden = true; + + %tree = GuiEditor.explorerWindow.tree; + %tree.refresh(); + + // Open every branch, so the shot shows the whole shape rather than whatever + // happened to be expanded. + %count = %tree.getItemCount(); + for(%i = 0; %i < %count; %i++) + { + %tree.setItemOpen(%i, true); + } + %tree.refresh(); + + // Remembered rather than selected once: switching the editor theme drops the + // tree's selection, and a selected row is the whole point of these shots. + $expTree::selectId = %panel.getId(); + expTreeSelect(); + + echo("EXPTREE: " @ %tree.getItemCount() @ " rows, column width " @ + %tree.getGutterColumnWidth()); + + schedule(600, 0, "expTreeShot"); +} + +// Themed the way a dropped control would be, because the theme is what decides +// a bare GuiControl's category -- and its category is what decides which of the +// four GuiControl faces its row icon shows. +function expAdd(%class, %parent, %pos, %extent, %name) +{ + %ctrl = eval("return new " @ %class @ "();"); + if(!isObject(%ctrl)) + { + // Loud, because the failure is otherwise a row that quietly is not there + // and a shot that looks like a smaller Gui than the one asked for. + error("EXPTREE: could not make a " @ %class); + return 0; + } + %ctrl.Position = %pos; + %ctrl.Extent = %extent; + %parent.add(%ctrl); + %ctrl.setInternalName(%name); + + %theme = GuiEditor.themeByName(GuiEditor.themeName); + if(isObject(%theme)) + { + GuiEditor.themeApplier.applyToBranch(%ctrl, %theme, false); + } + return %ctrl; +} + +// One shot per registered editor theme. ThemeManager owns the editor's own +// chrome -- the tree wears its treeViewProfile -- which is a different thing +// from GuiEditor.setTheme, that being the theme of the Gui being edited. +$expTree::theme = 0; + +function expTreeShot() +{ + if($expTree::theme >= ThemeManager.themeList.getCount()) + { + echo("SHOTS DONE"); + quit(); + return; + } + + // The .class field, not getName or getClassName: the themes are registered + // as unnamed ScriptObjects, so the first is empty and the second says + // "ScriptObject" for all four. + ThemeManager.setTheme($expTree::theme); + echo("EXPTREE: theme " @ $expTree::theme @ " is " @ ThemeManager.activeTheme.class); + + // Selecting and grabbing in one step captures the frame BEFORE the + // selection: screenShot reads the framebuffer, which still holds the last + // frame drawn. The row has to be selected a frame ahead of the shot. + expTreeSelect(); + schedule(600, 0, "expTreeGrab"); +} + +// A selected row, every time. The rail's colours are deliberately independent of +// selection, so a shot without a selected row cannot show whether that is true. +function expTreeSelect() +{ + %tree = GuiEditor.explorerWindow.tree; + %index = %tree.findItemID($expTree::selectId); + if(%index >= 0) + { + %tree.clearSelection(); + %tree.setSelected(%index, true); + } + else + { + error("EXPTREE: lost the row that was meant to be selected"); + } +} + +function expTreeGrab() +{ + screenShot(testRoot("shots/explorerTree" @ $expTree::theme @ ".png"), "PNG"); + $expTree::theme++; + schedule(500, 0, "expTreeShot"); +} diff --git a/tests/smoke/explorerGutter.cs b/tests/smoke/explorerGutter.cs new file mode 100644 index 000000000..51c747488 --- /dev/null +++ b/tests/smoke/explorerGutter.cs @@ -0,0 +1,248 @@ +//----------------------------------------------------------------------------- +// The Explorer tree's two columns of editor state -- the eye and the padlock. +// +// The geometry is unit tested (guiTreeRowLayoutTests.cc): where the columns are, +// which one an x falls in, where the box sits in a cell. None of that needs a +// canvas and none of it is repeated here. +// +// What IS here is the part no unit test can reach. Adding a row to a tree loads +// a font, which registers a texture, which asserts with no GL context -- so +// anything involving real rows has to run in a real engine. And the central +// promise of the feature is a negative: clicking a box toggles the flag and does +// NOT change the selection. Calling the toggle directly would skip the very code +// that could break that, so the click itself is posted for real, by +// explorerGutter.input.ps1. +//----------------------------------------------------------------------------- + +setLogMode(1); +setScriptExecEcho(false); +trace(false); +$Scripts::ignoreDSOs = true; +setCompanyAndProduct("Torque Game Engines", "Torque2D"); +ModuleDatabase.EchoInfo = false; +AssetDatabase.EchoInfo = false; +AssetDatabase.IgnoreAutoUnload = true; + +testExec("editor/main.cs"); + +$Pass = 0; +$Fail = 0; + +function gutCheck(%label, %condition) +{ + if(%condition) + { + $Pass++; + echo("EGUT PASS: " @ %label); + } + else + { + $Fail++; + echo("EGUT FAIL: " @ %label); + } +} + +// The engine works out where a box actually landed and leaves the point for the +// PowerShell side to click. A hard-coded coordinate that drifted off the box +// would report a missing item, which is exactly what a broken hit test reports. +function gutPostTarget(%point) +{ + createPath(testRoot("shots/")); + %file = new FileObject(); + %file.openForWrite(testRoot("shots/explorerGutterTarget.txt")); + %file.writeLine(%point); + %file.close(); + %file.delete(); +} + +schedule(2500, 0, "gutOpenProject"); + +// The long way in, and it has to be. GuiEditor.open() is enough for a suite that +// only calls methods, but it does not put the editor on the canvas -- and a +// posted click goes to whatever is actually on screen. Clicks against an editor +// opened the short way land on the project selector and do nothing at all, which +// reads exactly like a broken hit test. +function gutOpenProject() +{ + ProjectManager.setProjectFolder("PlanetX"); + EditorCore.projectSelector.onProjectSelected(pathConcat(getMainDotCsDir(), "PlanetX")); + schedule(2500, 0, "gutOpenEditor"); +} + +// Pages register in load order: EditorConsole, ProjectManager, AssetAdmin, +// GuiEditor. +function gutOpenEditor() +{ + EditorCore.toggleEditor(); + EditorCore.tabBook.selectPage(3); + schedule(1500, 0, "gutClear"); +} + +// Start from an empty Gui, in a step of its own: PlanetX finishes displaying +// its own Gui after the editor page comes up, so clearing in the same frame as +// the build leaves its controls in the tree and moves every row. +function gutClear() +{ + GuiEditor.NewGui(); + schedule(500, 0, "gutRun"); +} + +function gutRun() +{ + %tree = GuiEditor.explorerWindow.tree; + + // --- The class swap itself. Both halves matter: GuiEditor.cs and the theme + // applier both branch on isMemberOfClass("GuiTreeViewCtrl"), so a tree that + // stopped being one would break things nowhere near here. + gutCheck("the Explorer wears the editor tree class", + %tree.getClassName() $= "GuiEditorExplorerTree"); + gutCheck("and is still a tree view", %tree.isMemberOfClass("GuiTreeViewCtrl")); + + // --- Editor-only: the palette must refuse it, and BOTH copies of that rule + // have to agree. They are separately maintained -- one generated, one typed + // -- and this is the assertion that catches them drifting apart. + gutCheck("the palette refuses the editor tree", + !GuiEditor.controlIcons.isPlaceableClass("GuiEditorExplorerTree")); + gutCheck("and the spec's copy of the rule agrees", + !GuiEditor.inspectorWindow.pane.spec.isPlaceableClass("GuiEditorExplorerTree")); + + // --- The columns answer where they say they do. gutterColumnAt is the same + // function the paint uses, so this is the contract between them. + %w = %tree.getGutterColumnWidth(); + gutCheck("a column is a sensible width", %w > 0 && %w < 64); + gutCheck("the first column is the eye", %tree.gutterColumnAt(0) $= "hidden"); + gutCheck("the second is the padlock", %tree.gutterColumnAt(%w) $= "locked"); + gutCheck("past both is the tree itself", %tree.gutterColumnAt(2 * %w) $= ""); + + // --- Now a real control to toggle. + %ctrl = new GuiButtonCtrl(); + GuiEditor.rootGui.add(%ctrl); + %ctrl.setInternalName("gutTarget"); + %theme = GuiEditor.themeByName(GuiEditor.themeName); + if(isObject(%theme)) + { + GuiEditor.themeApplier.applyToBranch(%ctrl, %theme, false); + } + %tree.refresh(); + + $gutCtrl = %ctrl; + $gutIndex = %tree.findItemID(%ctrl.getId()); + gutCheck("the control has a row", $gutIndex > 0); + + gutCheck("it starts shown", !%ctrl.hidden); + gutCheck("and unlocked", !%ctrl.locked); + + // --- Select something ELSE, so "the click did not change the selection" is + // a claim with something to lose. Selecting the target itself would pass + // whether the click preserved the selection or set it. + %tree.clearSelection(); + %tree.setSelected(0, true); + $gutSelected = %tree.getSelectedItem(); + gutCheck("something else is selected to start with", $gutSelected != $gutIndex); + + echo("EGUT: tree extent " @ %tree.getExtent() @ ", " @ %tree.getItemCount() @ " rows"); + + // --- The row icons. The frame is cached on the row when the tree builds, so + // what is checked is that the cache holds the right answer and that it is + // re-asked when the answer changes. + %icons = GuiEditor.controlIcons; + gutCheck("the tree draws from the 16px sheet", + %tree.IconImage $= "GuiEditor:controlIcons16"); + gutCheck("the root row wears no picture", %tree.getItemIcon(0) == -1); + gutCheck("a button row wears the button", + %tree.getItemIcon($gutIndex) == %icons.frameFor("GuiButtonCtrl")); + + // A bare GuiControl is four palette entries sharing one class, told apart by + // the category it wears -- so the class alone cannot pick its picture. This + // is the case keyFor exists for. + %bare = new GuiControl(); + GuiEditor.rootGui.add(%bare); + %bare.setInternalName("gutBare"); + if(isObject(%theme)) + { + GuiEditor.themeApplier.applyToBranch(%bare, %theme, false); + } + %tree.refresh(); + + %bareIndex = %tree.findItemID(%bare.getId()); + %pane = GuiEditor.inspectorWindow.pane; + %wasCategory = %pane.currentCategory(%bare); + gutCheck("a bare GuiControl wears the face for its category", + %tree.getItemIcon(%bareIndex) == + %icons.frameFor("GuiControl:" @ %wasCategory)); + + // Change the face, and the row must follow. This is the invalidation path: + // the Category picker rewrites the Profile, which commits, which posts + // PostApply, which is what re-asks this one row. + %newCategory = (%wasCategory $= "Label") ? "Panel" : "Label"; + %pane.bind(%bare); + %pane.setCategory(%newCategory); + gutCheck("changing the category changed the row's picture", + %tree.getItemIcon(%tree.findItemID(%bare.getId())) == + %icons.frameFor("GuiControl:" @ %newCategory)); + gutCheck("and it really is a different picture", + %icons.frameFor("GuiControl:" @ %newCategory) != + %icons.frameFor("GuiControl:" @ %wasCategory)); + + %bare.delete(); + %tree.refresh(); + $gutIndex = %tree.findItemID($gutCtrl.getId()); + %tree.clearSelection(); + %tree.setSelected(0, true); + $gutSelected = %tree.getSelectedItem(); + + // Hand the eye box's center to the PowerShell side. + gutPostTarget(%tree.getGutterPoint($gutIndex, "hidden")); + echo("EGUT: eye box of row " @ $gutIndex @ " at " @ + %tree.getGutterPoint($gutIndex, "hidden")); + + schedule(9000, 0, "gutAfterEyeClick"); +} + +function gutAfterEyeClick() +{ + %tree = GuiEditor.explorerWindow.tree; + + // --- The whole point of the feature. + gutCheck("clicking the eye hid the control", $gutCtrl.hidden); + gutCheck("and did not move the selection", %tree.getSelectedItem() == $gutSelected); + gutCheck("and did not select the row it was on", %tree.getSelCount() == 1); + + // Now the padlock on the same row, which also proves the two columns are + // told apart by a real click and not just by gutterColumnAt. + gutPostTarget(%tree.getGutterPoint($gutIndex, "locked")); + echo("EGUT: padlock box of row " @ $gutIndex @ " at " @ + %tree.getGutterPoint($gutIndex, "locked")); + + schedule(9000, 0, "gutAfterLockClick"); +} + +function gutAfterLockClick() +{ + %tree = GuiEditor.explorerWindow.tree; + + gutCheck("clicking the padlock locked the control", $gutCtrl.locked); + gutCheck("and left the eye alone", $gutCtrl.hidden); + gutCheck("and still did not move the selection", + %tree.getSelectedItem() == $gutSelected); + + // And a click on the row's TEXT must still select, or the columns have + // swallowed the tree. + %point = %tree.getGutterPoint($gutIndex, "hidden"); + %textPoint = (getWord(%point, 0) + (3 * %tree.getGutterColumnWidth())) SPC getWord(%point, 1); + gutPostTarget(%textPoint); + echo("EGUT: row text at " @ %textPoint); + + schedule(9000, 0, "gutAfterTextClick"); +} + +function gutAfterTextClick() +{ + %tree = GuiEditor.explorerWindow.tree; + + gutCheck("clicking the row still selects it", %tree.getSelectedItem() == $gutIndex); + gutCheck("and selecting did not toggle anything", $gutCtrl.hidden && $gutCtrl.locked); + + echo("EGUT RESULT: " @ $Pass @ " passed, " @ $Fail @ " failed"); + quit(); +} diff --git a/tests/smoke/explorerGutter.input.ps1 b/tests/smoke/explorerGutter.input.ps1 new file mode 100644 index 000000000..a4b5c5bf1 --- /dev/null +++ b/tests/smoke/explorerGutter.input.ps1 @@ -0,0 +1,52 @@ +# Input for explorerGutter.cs. Posts three real clicks: the eye box of a row, +# then the padlock box of the same row, then the row's text. +# +# None of the three points is written here. Where a box lands depends on the +# theme's borders and on how tall the rows turned out, so the engine works it out +# with getGutterPoint and leaves it in a file for this script to pick up. A +# hard-coded point that drifted off the box would report a control that never +# toggled - which is precisely what a broken hit test reports - and the test +# would be lying either way. +# +# The clicks have to be real. What is being checked is that a press in a column +# toggles a flag WITHOUT selecting the row, and everything that could break that +# hangs off GuiControl's touch path: first responder, handleItemClick, the click +# callbacks, the reorder drag. Calling the toggle from script would skip all of +# it and prove nothing. +param([IntPtr]$Hwnd) + +. "$PSScriptRoot\..\lib\input.ps1" + +$target = Join-Path $PSScriptRoot "..\..\shots\explorerGutterTarget.txt" + +# Anything left by an earlier run would be clicked before this run has even +# built its tree. +if (Test-Path $target) { Remove-Item $target -Force } + +function Wait-ForTarget { + param([string]$Path, [int]$Seconds = 20) + + $deadline = (Get-Date).AddSeconds($Seconds) + while ((Get-Date) -lt $deadline) { + if (Test-Path $Path) { + $line = (Get-Content $Path -TotalCount 1) + if ($line -and $line.Trim()) { return $line.Trim().Split(' ') } + } + Start-Sleep -Milliseconds 250 + } + return $null +} + +$labels = @('the eye box', 'the padlock box', 'the row text') +foreach ($label in $labels) { + $point = Wait-ForTarget -Path $target + if (-not $point) { + Write-Host " the engine never reported $label" + return + } + + Remove-Item $target -Force + + Send-EngineClick -Hwnd $Hwnd -X ([int]$point[0]) -Y ([int]$point[1]) + Write-Host " clicked $label at ($($point[0]),$($point[1]))" +} diff --git a/tests/smoke/inspectorPane.cs b/tests/smoke/inspectorPane.cs index 48880670b..fe09c99ba 100644 --- a/tests/smoke/inspectorPane.cs +++ b/tests/smoke/inspectorPane.cs @@ -442,10 +442,26 @@ function pStep6() // setBoxExtent document one argument and read two, so a single "0 0" left // the box at (0,32) -- drawn entirely below the control. pCheck("toggle box covers the whole control", - %header.lockedButton.getBoxOffset() $= "0 0" && - %header.lockedButton.getBoxExtent() $= %header.lockedButton.getExtent()); + %header.visibleButton.getBoxOffset() $= "0 0" && + %header.visibleButton.getBoxExtent() $= %header.visibleButton.getExtent()); + + // The four runtime state flags are icon toggles now that the sheet has art + // for them. hidden and locked are not among them any more -- they are editor + // working state and moved out to the Explorer tree's columns. + pCheck("the pane no longer offers hidden", !isObject(%header.hiddenButton)); + pCheck("the pane no longer offers locked", !isObject(%header.lockedButton)); + + // And they must not come back through the side door. Both are real persist + // fields on SimObject, and buildOtherSection sweeps up every field no section + // claimed -- so unless editorToggles() keeps naming them, removing the two + // buttons does not remove the two controls from the pane. It turns them into + // a pair of generic checkboxes in "Other", which is worse than where they + // started: same working state, now filed under the leftovers. + pCheck("hidden did not reappear as a generic row", !pRowBuilt(%pane, "hidden")); + pCheck("locked did not reappear as a generic row", !pRowBuilt(%pane, "locked")); + pCheck("the spec still claims both", + %pane.spec.editorToggles() $= "hidden locked"); - // All six state flags are icon toggles now that the sheet has art for them. pCheck("visible toggle reflects the control", %header.visibleButton.getValue() == $pButton.Visible); @@ -472,24 +488,27 @@ function pStep6() pCheck("accepts-children shown where it is live", %header.containerButton.isVisible()); - // hidden and locked are toggle icons -- checkboxes wearing an icon -- so - // they hold their own state. performClick drives the real path: the - // checkbox flips itself and its Command tells the pane what it became. - %header.lockedButton.performClick(); - pCheck("locked toggle reached the control", $pButton.locked); - pCheck("locked icon shows the on frame", - %header.lockedButton.icon.getImageFrame() == %header.lockedButton.frameOn); - %header.lockedButton.performClick(); - pCheck("locked toggle flips back", !$pButton.locked); + // A toggle icon is a checkbox wearing an icon, so it holds its own state. + // performClick drives the real path: the checkbox flips itself and its + // Command tells the pane what it became. activeButton has a true on/off pair, + // so it is the one that can prove the icon follows the value. + %header.activeButton.performClick(); + pCheck("active toggle reached the control", !$pButton.Active); + pCheck("active icon shows the off frame", + %header.activeButton.icon.getImageFrame() == %header.activeButton.frameOff); + %header.activeButton.performClick(); + pCheck("active toggle flips back", $pButton.Active); + pCheck("active icon shows the on frame", + %header.activeButton.icon.getImageFrame() == %header.activeButton.frameOn); // A disabled toggle must not act. The engine hands touch events to inactive // controls -- findHitControl checks mVisible and mUseInput, never mActive -- // so this is the checkbox's own guard doing the work. - %header.lockedButton.setActive(false); - %header.lockedButton.performClick(); - pCheck("a disabled toggle does not change the control", !$pButton.locked); - pCheck("a disabled toggle keeps its own state off", !%header.lockedButton.getValue()); - %header.lockedButton.setActive(true); + %header.activeButton.setActive(false); + %header.activeButton.performClick(); + pCheck("a disabled toggle does not change the control", $pButton.Active); + pCheck("a disabled toggle keeps its own state on", %header.activeButton.getValue()); + %header.activeButton.setActive(true); // The profile picker offers the theme's members for the control's category, // not every profile in the sim. From 713b22a06c51f9a916be27a3b9851dd82b9de205 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Sun, 2 Aug 2026 21:34:11 -0400 Subject: [PATCH 30/55] Gui Editor: the Explorer tree indents by twelve, not by a row Its own commit because it is the one change here that alters how the tree looks rather than what it holds. The default step is a row height, which is what every tree has always used and what IndentSize=0 still means. This tree can no longer afford it. Two columns, a triangle and a picture stand in front of the name, which is about eighty pixels of a 228px window before a single letter, and every level of depth was adding twenty more. At three deep there was almost nothing left for the text. Twelve still reads as a clear step and buys back ten pixels a level. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qi2pZ9UzF5rsLCNVErpzwt --- editor/GuiEditor/scripts/GuiEditorExplorerWindow.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/editor/GuiEditor/scripts/GuiEditorExplorerWindow.cs b/editor/GuiEditor/scripts/GuiEditorExplorerWindow.cs index 88e0b553b..51e824fdf 100644 --- a/editor/GuiEditor/scripts/GuiEditorExplorerWindow.cs +++ b/editor/GuiEditor/scripts/GuiEditorExplorerWindow.cs @@ -39,6 +39,13 @@ 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. From b783f0361733af8d5a6bf75a013c61e6c49e6926 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Sun, 2 Aug 2026 21:34:36 -0400 Subject: [PATCH 31/55] Tests: one command for the C++ unit suite, and correct what the docs claim tests\run.ps1 has always been the way to run the script suites; the GoogleTest side had nothing. You launched the exe with an alternate boot script by hand and read console.log yourself, and there is no CI job for it either -- PR-builds.yml configures and builds and stops there. tests\run-unit.ps1 tests\run-unit.ps1 GuiTreeRowLayoutTests.* Note it takes a FILTER rather than passing one on: runAllUnitTests is declared 1,1 and hands InitGoogleTest an empty argv, so there is no argument to forward. GoogleTest reads GTEST_FILTER from the environment instead, which is what the parameter sets. A filter matching nothing exits non-zero rather than reporting success, because GoogleTest is perfectly happy to run no tests at all. Two things CLAUDE.md said that were not true. There are no TEST() blocks "throughout the engine" -- all 150 are in engine/source/testing/tests/, each listed explicitly in EngineSources.cmake, so a new file needs a CMake edit and a re-configure to be compiled at all. And runAllUnitTests takes no filter argument, as above. Both files now also record what a unit test can actually reach, because the limit is sharper than "no canvas" and it decides where coverage goes. The engine boots far enough to give a test Con, Sim, the string table, the resource manager and GuiDefaultProfile, so it can new and registerObject a control, read and write fields, run script and round-trip TAML. What it cannot do is wake a control or measure text -- a font registers a texture and TextureManager::refresh asserts, which in a debug build is a modal box and so arrives as a hang rather than a failure. That rules out more than it looks. Adding a row to a list box or a tree calls updateSize, which asks the profile for a font, which LOADS one. So rows are script-suite territory and only the arithmetic can be unit tested, which is what the established move already assumes: pull it into a static that takes everything it uses and test the static. Worth saying plainly since the script suites are one process each with a 90 second timeout and the whole unit run takes seconds: prefer a unit test wherever the thing under test can be reached without a canvas. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qi2pZ9UzF5rsLCNVErpzwt --- CLAUDE.md | 15 ++++-- tests/README.md | 15 +++++- tests/run-unit.ps1 | 124 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+), 6 deletions(-) create mode 100644 tests/run-unit.ps1 diff --git a/CLAUDE.md b/CLAUDE.md index a2800fc47..5c15809fd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,15 +44,22 @@ There are two suites, and they test different things. ### C++ unit tests (GoogleTest) -Vendored at `engine/source/testing/googleTest`. Tests live in `engine/source/testing/tests/` (e.g. `platformFileIoTests.cc`, `platformStringTests.cc`) and as `TEST(...)` blocks throughout the engine. +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. -- Run **all** tests by launching the engine with the alternate boot script: `main.runAllUnitTests.cs`, which calls the `runAllUnitTests()` console function and quits. Point the executable at this script (or `exec` it) instead of the default `main.cs`. -- From the in-engine console you can invoke `runAllUnitTests()` directly, or run a subset via the test-name filter argument (forwarded to GoogleTest). +``` +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. +`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) diff --git a/tests/README.md b/tests/README.md index 795651fde..aaefdb951 100644 --- a/tests/README.md +++ b/tests/README.md @@ -2,19 +2,30 @@ These drive the **real engine** — a real canvas, the real editor, the real input path — and check that it behaves. They are the counterpart to the GoogleTest C++ -unit tests (`main.runAllUnitTests.cs`, see the repo README): those test functions, -these test the thing a person actually uses. +unit tests: those test functions, these test the thing a person actually uses. ``` tests\run.ps1 every pass/fail suite tests\run.ps1 colorPopup one of them (wildcards allowed) tests\run.ps1 -Shots the screenshot harnesses instead tests\run.ps1 -List what would run + +tests\run-unit.ps1 the C++ unit tests -- seconds, not minutes ``` The runner exits non-zero if anything came out other than expected. Build first: `cmake --build build --config Debug --target Torque2D`. +**Reach for `run-unit.ps1` first.** A suite here costs its own process and gets a +90 second timeout; the whole unit run takes seconds. What belongs here is what +genuinely needs a canvas: rendering, real input, first responder, tooltips, +anything that measures text, and anything involving the rows of a list box or a +tree — adding one calls `updateSize()`, which loads a font, which registers a +texture, which asserts with no GL context. Arithmetic does not belong here. The +established move is to pull it out into a `static` that takes everything it uses +and test that: `GuiScrollCtrl::subtractScrollBars`, `GuiTreeViewCtrl::resolveIndent`, +`GuiEditorExplorerTree::columnAt`. + ## Layout | | | diff --git a/tests/run-unit.ps1 b/tests/run-unit.ps1 new file mode 100644 index 000000000..2141df8db --- /dev/null +++ b/tests/run-unit.ps1 @@ -0,0 +1,124 @@ +<# +.SYNOPSIS + Runs the C++ GoogleTest unit tests. + +.DESCRIPTION + The counterpart to run.ps1. Those drive the real engine, one process per + suite, with a 90 second timeout each; these are one process for the lot and + take seconds, because they touch no canvas, no GL context and no font. + + That is also their limit. The engine boots far enough to have Con, Sim, the + string table, the resource manager and GuiDefaultProfile -- so a test can + new and registerObject a control, read and write its fields, run script + through Con::evaluate, and round-trip TAML. What it cannot do is wake a + control or measure text: a font registers a texture, and with no GL context + TextureManager asserts. In a debug build an assert is a modal box, so that + failure arrives as a hang rather than as a red line. + + There is no filter ARGUMENT -- runAllUnitTests takes none, and hands + InitGoogleTest an empty argv. GoogleTest reads GTEST_FILTER from the + environment instead, which is what -Filter sets here. + +.PARAMETER Filter + A GoogleTest filter, e.g. 'GuiTreeRowLayoutTests.*' or 'Gui*'. Omit to run + everything. + +.PARAMETER Release + Use Torque2D.exe rather than Torque2D_DEBUG.exe. + +.PARAMETER Timeout + Seconds to let the run take before killing it. A debug AssertFatal is a modal + message box, so a test that trips one hangs. + +.EXAMPLE + tests\run-unit.ps1 + tests\run-unit.ps1 GuiTreeRowLayoutTests.* + tests\run-unit.ps1 Gui* +#> +[CmdletBinding()] +param( + [Parameter(Position = 0)] + [string]$Filter = '', + [switch]$Release, + [int]$Timeout = 120 +) + +$ErrorActionPreference = 'Stop' + +$repo = Split-Path -Parent $PSScriptRoot +$exe = Join-Path $repo ($Release ? 'Torque2D.exe' : 'Torque2D_DEBUG.exe') +$boot = Join-Path $repo 'main.runAllUnitTests.cs' +$log = Join-Path $repo 'console.log' + +if (-not (Test-Path $exe)) { + Write-Host "No $([IO.Path]::GetFileName($exe)) at the repo root. Build first:" -ForegroundColor Red + Write-Host " cmake --build build --config $(if ($Release) { 'Release' } else { 'Debug' }) --target Torque2D" + exit 1 +} +if (-not (Test-Path $boot)) { + Write-Host "No main.runAllUnitTests.cs at the repo root." -ForegroundColor Red + exit 1 +} + +# The boot script sets logMode 2, which truncates console.log on open -- so a +# stale log cannot be mistaken for this run's. Removing it first also means a +# process that dies before opening the log leaves no results at all, rather than +# the previous run's. +if (Test-Path $log) { Remove-Item $log -Force } + +$previousFilter = $env:GTEST_FILTER +if ($Filter) { + $env:GTEST_FILTER = $Filter + Write-Host "Running unit tests matching '$Filter'" +} else { + Remove-Item Env:\GTEST_FILTER -ErrorAction SilentlyContinue + Write-Host 'Running all unit tests' +} + +$hung = $false +try { + $proc = Start-Process -FilePath $exe -ArgumentList $boot -WorkingDirectory $repo -PassThru -WindowStyle Minimized + if (-not $proc.WaitForExit($Timeout * 1000)) { + $hung = $true + Write-Host " timed out after $Timeout s -- almost always a modal AssertFatal" -ForegroundColor Red + try { $proc.Kill() } catch { } + $proc.WaitForExit(5000) | Out-Null + } +} +finally { + if ($null -ne $previousFilter) { $env:GTEST_FILTER = $previousFilter } + else { Remove-Item Env:\GTEST_FILTER -ErrorAction SilentlyContinue } +} + +if (-not (Test-Path $log)) { + Write-Host ' the run wrote no console.log' -ForegroundColor Red + exit 1 +} + +$lines = Get-Content $log +$ran = @($lines | Select-String -SimpleMatch '> Starting Test').Count +$failLines = @($lines | Select-String -SimpleMatch '>> Failed with') + +foreach ($line in $failLines) { + Write-Host " $($line.Line.Trim())" -ForegroundColor Red +} + +Write-Host '' +if ($hung) { + Write-Host "$ran ran, then it hung." -ForegroundColor Red + if ($lines.Count) { Write-Host " last line: $($lines[-1])" } + exit 1 +} +if ($failLines.Count -gt 0) { + Write-Host "$ran tests, $($failLines.Count) failed." -ForegroundColor Red + exit 1 +} +if ($ran -eq 0) { + # An over-narrow filter matches nothing and GoogleTest reports success, which + # would otherwise read as "everything passed". + Write-Host 'No tests ran. Check the filter.' -ForegroundColor Yellow + exit 1 +} + +Write-Host "All $ran passed." -ForegroundColor Green +exit 0 From 29ac8192984f1148d5e776aa43a7dc3ee517bfc7 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Sun, 2 Aug 2026 22:57:35 -0400 Subject: [PATCH 32/55] Gui Editor: four defects a read-through of the editor turned up Together in one commit because they are four small, unrelated repairs found in one pass rather than four pieces of work. Three had a test written that failed first. A drag that crosses a container boundary is now one undo step that puts the control back. This was the sharp one: Ctrl+Z after such a drag left the control in its new parent and wrote the old parent's coordinates into it, so it landed somewhere it had never been. A drag is not one event. guiEditCtrl.cc calls moveSelection once per mouse-move, and each of those brackets ITSELF with onPreSelectionNudged / onPostSelectionNudged, so the record was being built a frame at a time and coalesced back afterwards. Then somewhere in the middle of all that, onTouchDragged looks at what is under the cursor and calls moveSelectionToCtrl -- which reparents the selection and rewrites each control's position to keep it under the pointer, announcing neither. The frame-by-frame record has no way to see either one. So onPreEdit and onPostEdit now bracket the whole gesture rather than standing in for its first and last frame: beginGesture takes one snapshot of where every selected control stood, endGesture reads them again and writes one action, and the nudge callbacks are ignored while it runs. That is not only for the reparent -- it is also what a drag always meant, and it makes the hundred small moves and their coalescing unnecessary rather than merely harmless. A control that changed parent is recorded as a move op plus a layout fix. The fix rather than a Position field op, because it 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. One writer per control either way. Arrow-key nudges are untouched. They arrive with no onPreEdit, so they still take the snapshot + commitGeometry route and still coalesce. Snap to Grid no longer throws away the grid size. Set Grid Size offers 5 to 50; the Layout menu's toggle then called setSnapToGrid(10) and put it back to ten. The C++ was already built for the two to be separate -- passing 0 clears the flag and deliberately leaves mGridSnap alone, which is what lets getGridSize answer "the grid size even if the grid is off" -- and the script above it is now separate too. The Save Gui As button greys out again. Validate has been setting .active on %this.createButton, which is a name from the Asset Admin dialogs this form was copied from; this one's button is saveButton. So the write went nowhere and Save looked available whatever the form said. Harmless in effect, since onSave revalidates, but the form was telling the user the opposite of what it had just worked out. And GuiEditorColorWindow is gone. It was a scratch pad for looking at color picker modes side by side, most of its own body commented out, the window it builds commented out at the GuiEditor.cs end since before this branch -- and still exec'd on every editor start. Two new suites, both for behaviour that had none. gridSize drives the real Set Grid Size dialog and then proves snapping by what it does rather than by a getter, because there isn't one: hasSnapToGrid is C++-side only, so the tell is a one-pixel nudge either landing on the next grid line or moving one pixel. saveDialog checks the button against the form and deliberately never calls onSave -- the folder it points at is real PlanetX content, and writing a file into that to prove a button is grey would be a poor trade. undo.cs gains the canvas drag in two halves: one that stays inside its parent, which passed before this change and is here to hold the ordinary drag still while the recording underneath it was rewritten, and one that crosses. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qi2pZ9UzF5rsLCNVErpzwt --- editor/GuiEditor/GuiEditor.cs | 35 +--- editor/GuiEditor/scripts/GuiEditorBrain.cs | 13 +- .../GuiEditor/scripts/GuiEditorColorWindow.cs | 67 -------- .../scripts/GuiEditorSaveGuiDialog.cs | 8 +- .../scripts/GuiEditorUndoRecorder.cs | 155 +++++++++++++++++- tests/smoke/gridSize.cs | 126 ++++++++++++++ tests/smoke/saveDialog.cs | 93 +++++++++++ tests/smoke/undo.cs | 78 +++++++++ 8 files changed, 467 insertions(+), 108 deletions(-) delete mode 100644 editor/GuiEditor/scripts/GuiEditorColorWindow.cs create mode 100644 tests/smoke/gridSize.cs create mode 100644 tests/smoke/saveDialog.cs diff --git a/editor/GuiEditor/GuiEditor.cs b/editor/GuiEditor/GuiEditor.cs index 2e7658ea2..21227b992 100644 --- a/editor/GuiEditor/GuiEditor.cs +++ b/editor/GuiEditor/GuiEditor.cs @@ -32,7 +32,6 @@ exec("./scripts/GuiEditorExplorerTree.cs"); exec("./scripts/GuiEditorSaveGuiDialog.cs"); exec("./scripts/GuiEditorGridSizeDialog.cs"); - exec("./scripts/GuiEditorColorWindow.cs"); exec("./scripts/GuiEditorToolsWindow.cs"); exec("./scripts/GuiProfileEditorDialog.cs"); exec("./scripts/GuiProfileEditorColorPopup.cs"); @@ -270,29 +269,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); } @@ -1113,13 +1089,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/scripts/GuiEditorBrain.cs b/editor/GuiEditor/scripts/GuiEditorBrain.cs index ea1f846b1..ca75bf2ad 100644 --- a/editor/GuiEditor/scripts/GuiEditorBrain.cs +++ b/editor/GuiEditor/scripts/GuiEditorBrain.cs @@ -547,15 +547,22 @@ // what actually moved. A mouse-down that only selected records nothing. //----------------------------------------------------------------------------- -// Mouse-down on a selection, before a drag-move or a handle-resize. +// 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.snapshot(%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.commitGeometry("", ""); + GuiEditor.undoRecorder.endGesture(); } // Arrow-key and menu nudges, which the C++ gives a callback of their own 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/GuiEditorSaveGuiDialog.cs b/editor/GuiEditor/scripts/GuiEditorSaveGuiDialog.cs index 27cf5e9fe..ebb50f5ff 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,13 +158,13 @@ class = "EditorForm"; } if(isFile(%filePath)) { - %this.createButton.active = true; + %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.saveButton.setActive(true); %this.feedback.setText(%this.withFormatWarning("A new Gui file will be created!")); return true; } diff --git a/editor/GuiEditor/scripts/GuiEditorUndoRecorder.cs b/editor/GuiEditor/scripts/GuiEditorUndoRecorder.cs index 508576a35..67bde2e9f 100644 --- a/editor/GuiEditor/scripts/GuiEditorUndoRecorder.cs +++ b/editor/GuiEditor/scripts/GuiEditorUndoRecorder.cs @@ -5,17 +5,23 @@ // GuiEditorUndoAction. // // Everything that changes the Gui being authored comes through here, by one of -// two routes: +// 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. -// snapshot + commitGeometry a gesture whose result is only known -// when it ends - a drag, a run of nudges. -// The C++ edit control brackets both with -// callbacks (onPreEdit/onPostEdit, -// onPreSelectionNudged/...) which is what -// they were always for. +// 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 @@ -36,6 +42,8 @@ %this.lastAction = 0; %this.lastKind = ""; %this.snapCount = 0; + %this.gestCount = 0; + %this.inGesture = false; %this.hierCount = 0; %this.hierCtrlCount = 0; %this.watchCount = 0; @@ -536,7 +544,11 @@ class = "GuiEditorUndoAction"; function GuiEditorUndoRecorder::snapshot(%this, %selection) { %this.snapCount = 0; - if(%this.suspended || !isObject(%selection)) + + // 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; } @@ -556,7 +568,7 @@ class = "GuiEditorUndoAction"; // that selected without dragging, or a nudge into a wall, records nothing. function GuiEditorUndoRecorder::commitGeometry(%this, %name, %kind) { - if(%this.suspended || %this.snapCount <= 0) + if(%this.suspended || %this.inGesture || %this.snapCount <= 0) { return; } @@ -595,6 +607,124 @@ class = "GuiEditorUndoAction"; %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. // @@ -764,6 +894,13 @@ class = "GuiEditorUndoAction"; %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; + %this.replayTouched = ""; %this.refreshMenu(); } diff --git a/tests/smoke/gridSize.cs b/tests/smoke/gridSize.cs new file mode 100644 index 000000000..77c6bc972 --- /dev/null +++ b/tests/smoke/gridSize.cs @@ -0,0 +1,126 @@ +//----------------------------------------------------------------------------- +// The Layout menu's two grid commands, which are one setting between them. +// +// Set Grid Size names the spacing; Snap to Grid says whether to use it. The C++ +// keeps them apart on purpose -- setSnapToGrid(0) only clears the flag and leaves +// mGridSnap alone, so getGridSize still answers "the grid size even if the grid +// is off" -- and the point of this suite is that the script above it keeps them +// apart too. A toggle is not a place to decide how big the grid is. +// +// Snapping is checked by what it does rather than by a getter, because there +// isn't one: hasSnapToGrid is C++-side only. A nudge is the tell. The +// moveSelection binding asks hasSnapToGrid and picks moveAndSnapSelection or the +// plain move, so a one-pixel nudge either lands on the next grid line or moves +// the control by exactly one pixel. +//----------------------------------------------------------------------------- + +setLogMode(2); +setScriptExecEcho(false); +trace(false); +$Scripts::ignoreDSOs = true; +setCompanyAndProduct("Torque Game Engines", "Torque2D"); +ModuleDatabase.EchoInfo = false; +AssetDatabase.EchoInfo = false; +AssetDatabase.IgnoreAutoUnload = true; + +testExec("editor/main.cs"); + +function gsCheck(%label, %condition) +{ + echo(%condition ? ("GRID PASS: " @ %label) : ("GRID FAIL: " @ %label)); +} + +// A dialog is pushed onto the Canvas and nothing keeps a handle to it, so it is +// found the way it is displayed: as the Canvas's newest child. +function gsDialog(%class) +{ + for(%i = Canvas.getCount() - 1; %i >= 0; %i--) + { + %obj = Canvas.getObject(%i); + if(%obj.class $= %class) + { + return %obj; + } + } + + return 0; +} + +function gsNudgeBy(%ctrl) +{ + GuiEditor.brain.clearSelection(); + GuiEditor.brain.select(%ctrl); + + %before = getWord(%ctrl.getPosition(), 0); + GuiEditor.brain.moveSelection(1, 0); + + return getWord(%ctrl.getPosition(), 0) - %before; +} + +schedule(2000, 0, "gsSetup"); + +function gsSetup() +{ + ProjectManager.setProjectFolder("PlanetX"); + ModuleDatabase.ScanModules("PlanetX"); + ModuleDatabase.LoadExplicit("AppCore", 1); + + GuiEditor.open(); + + // Off a grid line to start with, so a snapped nudge and an unsnapped one + // cannot land on the same number by luck. + $gsCtrl = new GuiButtonCtrl() { Position = "3 3"; Extent = "80 30"; Text = "A"; }; + GuiEditor.rootGui.add($gsCtrl); + + schedule(300, 0, "gsStepDialog"); +} + +//----------------------------------------------------------------------------- +// Setting the size, through the dialog the menu opens. +//----------------------------------------------------------------------------- + +function gsStepDialog() +{ + GuiEditor.SetGridSize(); + + %dialog = gsDialog("GuiEditorGridSizeDialog"); + gsCheck("the Set Grid Size dialog opened", isObject(%dialog)); + gsCheck("and it starts on the current size", + %dialog.gridSizeBox.getText() == GuiEditor.brain.getGridSize()); + + %dialog.gridSizeBox.setText("25"); + %dialog.onDone(); + + gsCheck("the dialog set the grid size", GuiEditor.brain.getGridSize() == 25); + gsCheck("and a nudge snaps to it", gsNudgeBy($gsCtrl) == 22); + + schedule(300, 0, "gsStepToggle"); +} + +//----------------------------------------------------------------------------- +// Toggling it off and on, which is the Layout menu's Snap to Grid item. +//----------------------------------------------------------------------------- + +function gsStepToggle() +{ + $gsCtrl.setPosition(3, 3); + + GuiEditor.SnapToGrid(false); + gsCheck("snap off moves by the pixel", gsNudgeBy($gsCtrl) == 1); + gsCheck("and the size is remembered while it is off", + GuiEditor.brain.getGridSize() == 25); + + $gsCtrl.setPosition(3, 3); + + GuiEditor.SnapToGrid(true); + gsCheck("the size survived the round trip", GuiEditor.brain.getGridSize() == 25); + gsCheck("and snap is on again", gsNudgeBy($gsCtrl) == 22); + + schedule(300, 0, "gsDone"); +} + +function gsDone() +{ + echo("GRID DONE"); + quit(); +} diff --git a/tests/smoke/saveDialog.cs b/tests/smoke/saveDialog.cs new file mode 100644 index 000000000..4abb486d0 --- /dev/null +++ b/tests/smoke/saveDialog.cs @@ -0,0 +1,93 @@ +//----------------------------------------------------------------------------- +// Save Gui As, and the one thing its form owes the person filling it in: the +// Save button says whether what they have typed can be saved. +// +// Nothing here saves. Validate is the whole subject, and the folder it is +// pointed at is real content in the PlanetX project -- writing a file into that +// to prove a button is grey would be a poor trade. +//----------------------------------------------------------------------------- + +setLogMode(2); +setScriptExecEcho(false); +trace(false); +$Scripts::ignoreDSOs = true; +setCompanyAndProduct("Torque Game Engines", "Torque2D"); +ModuleDatabase.EchoInfo = false; +AssetDatabase.EchoInfo = false; +AssetDatabase.IgnoreAutoUnload = true; + +testExec("editor/main.cs"); + +function sdCheck(%label, %condition) +{ + echo(%condition ? ("SAVED PASS: " @ %label) : ("SAVED FAIL: " @ %label)); +} + +// A dialog is pushed onto the Canvas and nothing keeps a handle to it, so it is +// found the way it is displayed: as the Canvas's newest child. +function sdDialog(%class) +{ + for(%i = Canvas.getCount() - 1; %i >= 0; %i--) + { + %obj = Canvas.getObject(%i); + if(%obj.class $= %class) + { + return %obj; + } + } + + return 0; +} + +schedule(2000, 0, "sdSetup"); + +function sdSetup() +{ + ProjectManager.setProjectFolder("PlanetX"); + ModuleDatabase.ScanModules("PlanetX"); + ModuleDatabase.LoadExplicit("AppCore", 1); + + GuiEditor.open(); + + schedule(300, 0, "sdStepValidate"); +} + +function sdStepValidate() +{ + GuiEditor.SaveGuiAs(); + + $sdDialog = sdDialog("GuiEditorSaveGuiDialog"); + sdCheck("the Save Gui dialog opened", isObject($sdDialog)); + + // A Gui that has never been saved opens the form with no target folder, which + // is the first thing it asks for. + sdCheck("an unfilled form does not validate", !$sdDialog.validate()); + sdCheck("and Save is not offered", !$sdDialog.saveButton.isActive()); + + // A real module folder, which is what the form is holding out for: it refuses + // anything outside a module, and anything inside a library one. + $sdDialog.folderBox.setText("PlanetX/PlanetXGame/gui"); + $sdDialog.guiNameBox.setText("smokeSaveDialog.gui"); + + sdCheck("a filled form validates", $sdDialog.validate()); + sdCheck("and Save is offered", $sdDialog.saveButton.isActive()); + + // And back, because a form that only ever enables is a form that never told + // the truth in the first place. + $sdDialog.guiNameBox.setText(""); + + sdCheck("clearing the name invalidates it again", !$sdDialog.validate()); + sdCheck("and Save is withdrawn", !$sdDialog.saveButton.isActive()); + + schedule(300, 0, "sdDone"); +} + +function sdDone() +{ + // Closed rather than left standing, so the process exits through the same + // path a person's Cancel would take. + $sdDialog.onClose(); + + echo("SAVED DONE"); + quit(); +} diff --git a/tests/smoke/undo.cs b/tests/smoke/undo.cs index 679f1c40a..a2f957224 100644 --- a/tests/smoke/undo.cs +++ b/tests/smoke/undo.cs @@ -624,6 +624,84 @@ function uStepReparent() GuiEditor.Undo(); uCheck("undo restored the order", $uPanel.getObject(0) == %first); + schedule(300, 0, "uStepCanvasDrag"); +} + +//----------------------------------------------------------------------------- +// Dragging on the canvas, which the C++ brackets with onPreEdit and onPostEdit. +// +// The gesture is performed here the way guiEditCtrl.cc performs it, because two +// things happen inside it that no callback announces. onTouchDragged moves the +// selection once per mouse-move event - each of those brackets itself with the +// nudge pair - and then looks at what is under the cursor and reparents into it +// (moveSelectionToCtrl), rewriting the control's position so it stays under the +// pointer. So a drag that crosses a container boundary changes parent AND +// changes position a second time, silently, in the middle of the gesture. +//----------------------------------------------------------------------------- + +function uStepCanvasDrag() +{ + // A drag that stays where it is. The ordinary case, and the one the reparent + // record must not disturb. + GuiEditor.undoRecorder.clear(); + uSelect($uA); + + %wasPos = $uA.getPosition(); + + GuiEditor.brain.onPreEdit(GuiEditor.brain.getSelected()); + GuiEditor.brain.moveSelection(0, 20); + GuiEditor.brain.moveSelection(0, 20); + GuiEditor.brain.onPostEdit(GuiEditor.brain.getSelected()); + + %droppedPos = $uA.getPosition(); + uCheck("a canvas drag is one step", uUndoCount() == 1); + uCheck("and the control moved", %droppedPos !$= %wasPos); + + GuiEditor.Undo(); + uCheck("undo takes the whole drag back", $uA.getPosition() $= %wasPos); + GuiEditor.Redo(); + uCheck("redo replays the whole drag", $uA.getPosition() $= %droppedPos); + GuiEditor.Undo(); + + schedule(300, 0, "uStepCanvasDragReparent"); +} + +function uStepCanvasDragReparent() +{ + GuiEditor.undoRecorder.clear(); + uSelect($uA); + + %wasParent = $uA.getParent(); + %wasIndex = uIndexOf($uPanel, $uA); + %wasPos = $uA.getPosition(); + + GuiEditor.brain.onPreEdit(GuiEditor.brain.getSelected()); + GuiEditor.brain.moveSelection(0, 20); + GuiEditor.brain.moveSelection(0, 20); + GuiEditor.brain.moveSelectionToCtrl($uOther); + GuiEditor.brain.onPostEdit(GuiEditor.brain.getSelected()); + + %droppedPos = $uA.getPosition(); + uCheck("a drag into another container is one step", uUndoCount() == 1); + uCheck("and the control changed parent", $uA.getParent() == $uOther); + + GuiEditor.Undo(); + uCheck("undo returned it to its old parent", $uA.getParent() == %wasParent); + uCheck("at its old index", uIndexOf($uPanel, $uA) == %wasIndex); + + // The position matters as much as the parent: the reparent rewrote it to a + // number that means something only inside the container it moved to, so a + // replay that puts one back without the other lands the control somewhere it + // has never been. + uCheck("and its old position", $uA.getPosition() $= %wasPos); + + GuiEditor.Redo(); + uCheck("redo dragged it across again", $uA.getParent() == $uOther); + uCheck("to where the drag left it", $uA.getPosition() $= %droppedPos); + + GuiEditor.Undo(); + uCheck("and it came home", $uA.getParent() == %wasParent); + schedule(300, 0, "uStepMenu"); } From e6e59b305d98c0ac6442571309fc275649f16cb6 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Mon, 3 Aug 2026 11:42:14 -0400 Subject: [PATCH 33/55] Gui Editor: ask before throwing away a Gui that has not been saved New Gui cleared the canvas, Open Gui deleted what was on it, Close Project restarted the instance and Exit quit, and not one of them said a word. The file name was not on screen either, so there was nothing to say what was being edited or that it had changed. The Profile Editor has asked about unsaved themes since it was written; the document the editor exists to author had never asked about anything. All four ask now. The Gui Tools window's title carries the document - "titleScreen.gui", or "untitled.gui *" before its first save with changes in it - and File gains a Revert. Not the window's own close button, which cannot be done from here. 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 and nothing to veto with; onPreExit runs inside shutdown, long past the point where a question could be asked. Guarding it means a hook in each of six platform back-ends. The flag is not a count. UndoManager offers getUndoCount, getRedoCount and two name lookups and no way to ask what is on top, and a depth is not an identity anyway: save at five, undo once, make a DIFFERENT edit, and it is five again with a different document underneath. A flag built that way reads clean while dirty, which is the one direction it must never be wrong in. So every action carries the serial it leaves the document at and the serial it was applied to, and the recorder tracks where the document currently stands. Undo of A moves to A.priorSerial, redo to A.serial; markClean remembers the value, isModified compares against it. Undo back to the state that was saved and the marker goes away; branch off differently and it cannot, because that action's serial has never existed. Nothing mirrors the C++ stack, so nothing can drift out of step with it - an action trimmed off the bottom never replays and never restores its serial, which is the safe direction. clear() takes a fresh serial for the same reason: detachTheme 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. The awkward part is that Save, from the prompt, may not finish. On a Gui with no file it opens Save As, which has its own Cancel. So the interrupted command is released in exactly two places - the end of SaveCore, and Discard - and dropped when either dialog closes. There is no path where the file was not written and the thing the user was leaving for happens anyway. Close Project and Exit go through EditorCore.guardedCommand, which hands off to the Gui Editor when there is one. EditorCore naming GuiEditor is the coupling its File, Edit, Layout and Select menus already have. Revert re-reads through loadGuiFile, which is Open's own second half pulled out so the two cannot drift; it is guarded like the rest, because putting the file back discards everything since. Its greying is deliberately NOT on refreshDocumentTitle, which runs on every edit: setMenuActive walks the whole menu tree and re-applies every item's profile, and whether the document has a file changes only on a save, a new one and an open. tests/smoke/unsaved.cs covers all of it, including the branch case above and the cancelled Save As. undo.cs answers the new prompt where it uses New Gui to empty the stacks, and the prompt-answering helper is in the prelude because two other harnesses reach for New Gui the same way - one of them a shot harness, which would otherwise have photographed the dialog. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qi2pZ9UzF5rsLCNVErpzwt --- .gitignore | 1 + editor/EditorCore/EditorCore.cs | 40 +- editor/GuiEditor/GuiEditor.cs | 244 ++++++++-- .../scripts/GuiEditorConfirmSaveDialog.cs | 110 +++++ .../scripts/GuiEditorSaveGuiDialog.cs | 17 + .../GuiEditor/scripts/GuiEditorToolsWindow.cs | 11 + .../GuiEditor/scripts/GuiEditorUndoAction.cs | 4 +- .../scripts/GuiEditorUndoRecorder.cs | 106 ++++- tests/lib/prelude.cs | 29 ++ tests/shots/explorerTree.cs | 5 + tests/shots/unsavedPrompt.cs | 90 ++++ tests/smoke/explorerGutter.cs | 7 + tests/smoke/undo.cs | 5 + tests/smoke/unsaved.cs | 417 ++++++++++++++++++ 14 files changed, 1049 insertions(+), 37 deletions(-) create mode 100644 editor/GuiEditor/scripts/GuiEditorConfirmSaveDialog.cs create mode 100644 tests/shots/unsavedPrompt.cs create mode 100644 tests/smoke/unsaved.cs diff --git a/.gitignore b/.gitignore index cfe836a88..3c91b43a5 100755 --- a/.gitignore +++ b/.gitignore @@ -113,6 +113,7 @@ engine/compilers/android-studio/app/.cxx/ /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 diff --git a/editor/EditorCore/EditorCore.cs b/editor/EditorCore/EditorCore.cs index 1ef4e03cc..21ec11b9b 100644 --- a/editor/EditorCore/EditorCore.cs +++ b/editor/EditorCore/EditorCore.cs @@ -87,14 +87,22 @@ Command = "EditorCore.close();"; }; + // Both go through the Gui Editor's guard, because both throw away a + // Gui that is being authored and neither is undoable. GuiEditor is + // named directly, as it is by every item in the File, Edit, Layout + // and Select menus below; the isObject test is what keeps quitting + // working if the Gui Editor module ever fails to load. + // + // 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();"; + Command = "EditorCore.guardedCommand(\"quit();\");"; }; }; new GuiMenuItemCtrl() { @@ -122,6 +130,16 @@ Command = "GuiEditor.SaveGuiAs();"; Accelerator = "Ctrl-Shift S"; }; + new GuiMenuItemCtrl() { Text = "-"; }; + // Offered only once the Gui has a file to go back to; GuiEditor + // keeps that up to date in refreshFileMenu. No accelerator - it + // throws away everything since the last save, and that is not a + // thing to have a shortcut for. + new GuiMenuItemCtrl() { + Text = "Revert"; + Command = "GuiEditor.Revert();"; + Active = "0"; + }; }; new GuiMenuItemCtrl() { Text = "Edit"; @@ -418,6 +436,24 @@ } } +// Run %command, unless there is a Gui being authored with changes in it - in +// which case the Gui Editor asks about those first and runs it afterwards, or +// not at all. Used by Close Project and Exit, which are the two commands in this +// menu that discard a document without being able to give it back. +// +// The Gui Editor is the only editor with a document to lose. If another ever +// grows one, this is where it joins in. +function EditorCore::guardedCommand(%this, %command) +{ + if(isObject(GuiEditor)) + { + GuiEditor.guardDocument(%command); + return; + } + + eval(%command); +} + function EditorCore::RegisterEditor(%this, %name, %editor) { %this.page[%name] = new GuiTabPageCtrl() diff --git a/editor/GuiEditor/GuiEditor.cs b/editor/GuiEditor/GuiEditor.cs index 21227b992..6a7004cb2 100644 --- a/editor/GuiEditor/GuiEditor.cs +++ b/editor/GuiEditor/GuiEditor.cs @@ -31,6 +31,7 @@ exec("./scripts/GuiEditorExplorerWindow.cs"); exec("./scripts/GuiEditorExplorerTree.cs"); exec("./scripts/GuiEditorSaveGuiDialog.cs"); + exec("./scripts/GuiEditorConfirmSaveDialog.cs"); exec("./scripts/GuiEditorGridSizeDialog.cs"); exec("./scripts/GuiEditorToolsWindow.cs"); exec("./scripts/GuiProfileEditorDialog.cs"); @@ -386,6 +387,12 @@ class = "SimulatedCanvas"; %this.undoRecorder.forceRefreshMenu(); %this.clipboard.forceRefreshMenu(); %this.brain.toggleMenuItems(); + %this.refreshFileMenu(); + + // 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); } @@ -400,7 +407,18 @@ class = "SimulatedCanvas"; } //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 = ""; @@ -418,9 +436,20 @@ class = "SimulatedCanvas"; // 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() @@ -436,41 +465,68 @@ 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(); @@ -506,6 +562,12 @@ class = "SimulatedCanvas"; } %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) @@ -568,6 +630,117 @@ class = "GuiProfileEditorDialog"; 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. +// +// Deliberately NOT called from refreshDocumentTitle, which runs on every edit: +// setMenuActive walks the whole menu tree by item text and re-applies every +// item's profile, which is the cost GuiEditorUndoRecorder::refreshMenu keeps a +// cache to avoid paying per keystroke. Whether the document has a file changes +// far more rarely than the document does - on a save, a new one and an open - +// so this is called from those three and from open(), where the menu is +// rebuilt-looking. +function GuiEditor::refreshFileMenu(%this) +{ + EditorCore.menuBar.setMenuActive("Revert", %this.filePath !$= ""); +} + +//----------------------------------------------------------------------------- +// 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 @@ -886,6 +1059,17 @@ class = "GuiEditorThemeDialog"; %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------------------------------------------------------------------------- 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/GuiEditorSaveGuiDialog.cs b/editor/GuiEditor/scripts/GuiEditorSaveGuiDialog.cs index ebb50f5ff..bc19ba38a 100644 --- a/editor/GuiEditor/scripts/GuiEditorSaveGuiDialog.cs +++ b/editor/GuiEditor/scripts/GuiEditorSaveGuiDialog.cs @@ -200,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/GuiEditorToolsWindow.cs b/editor/GuiEditor/scripts/GuiEditorToolsWindow.cs index cd1cc27ab..4f77fade0 100644 --- a/editor/GuiEditor/scripts/GuiEditorToolsWindow.cs +++ b/editor/GuiEditor/scripts/GuiEditorToolsWindow.cs @@ -17,6 +17,17 @@ %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)) diff --git a/editor/GuiEditor/scripts/GuiEditorUndoAction.cs b/editor/GuiEditor/scripts/GuiEditorUndoAction.cs index 298b9c9a5..dd0ccb8fe 100644 --- a/editor/GuiEditor/scripts/GuiEditorUndoAction.cs +++ b/editor/GuiEditor/scripts/GuiEditorUndoAction.cs @@ -284,9 +284,11 @@ // 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); + %this.recorder.noteReplay(%this, %forward); } if(%forward) diff --git a/editor/GuiEditor/scripts/GuiEditorUndoRecorder.cs b/editor/GuiEditor/scripts/GuiEditorUndoRecorder.cs index 67bde2e9f..8241c8734 100644 --- a/editor/GuiEditor/scripts/GuiEditorUndoRecorder.cs +++ b/editor/GuiEditor/scripts/GuiEditorUndoRecorder.cs @@ -44,6 +44,13 @@ %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; @@ -140,6 +147,12 @@ class = "GuiEditorUndoAction"; { %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; } @@ -147,9 +160,15 @@ class = "GuiEditorUndoAction"; // 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(); } @@ -172,6 +191,75 @@ class = "GuiEditorUndoAction"; 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. //----------------------------------------------------------------------------- @@ -866,14 +954,17 @@ class = "GuiEditorUndoAction"; %this.suspended = false; } -// Called by the action as it replays, both ways. Two jobs: hand GuiEditor the -// controls to re-select afterwards, and drop the coalescing state - the action -// on top of the undo stack is no longer the one a merge would be aiming at. -function GuiEditorUndoRecorder::noteReplay(%this, %action) +// 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) @@ -901,6 +992,13 @@ class = "GuiEditorUndoAction"; %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(); } diff --git a/tests/lib/prelude.cs b/tests/lib/prelude.cs index 1c5e6e02c..9a098b5fc 100644 --- a/tests/lib/prelude.cs +++ b/tests/lib/prelude.cs @@ -24,3 +24,32 @@ function testExec(%relativePath) { exec(testRoot(%relativePath)); } + +//----------------------------------------------------------------------------- +// Answer the Gui Editor's unsaved-changes prompt with Discard, if it is up. +// +// New Gui, Open Gui and Revert ask before throwing a modified document away, so +// a test that uses one of them to get to a clean canvas has to answer. Returns +// whether there was anything to answer -- a document with no edits in it is +// taken away without a word, and callers that arrive clean are not doing +// anything wrong. +// +// Here rather than copied into each suite because three of them need it, and +// because a harness that does NOT answer does not fail: it screenshots the +// dialog, or carries on against the document it thought it had cleared. +//----------------------------------------------------------------------------- + +function discardUnsavedPrompt() +{ + for(%i = Canvas.getCount() - 1; %i >= 0; %i--) + { + %dialog = Canvas.getObject(%i); + if(%dialog.class $= "GuiEditorConfirmSaveDialog") + { + %dialog.onDiscard(); + return true; + } + } + + return false; +} diff --git a/tests/shots/explorerTree.cs b/tests/shots/explorerTree.cs index d0fee6c16..2ffec8685 100644 --- a/tests/shots/explorerTree.cs +++ b/tests/shots/explorerTree.cs @@ -58,6 +58,11 @@ function expTreeOpenEditor() function expTreeClear() { GuiEditor.NewGui(); + + // As in tests/smoke/explorerGutter.cs: usually nothing to answer, but a + // prompt left standing would be what the shot was of. + discardUnsavedPrompt(); + schedule(500, 0, "expTreeSetup"); } diff --git a/tests/shots/unsavedPrompt.cs b/tests/shots/unsavedPrompt.cs new file mode 100644 index 000000000..1fb7c5570 --- /dev/null +++ b/tests/shots/unsavedPrompt.cs @@ -0,0 +1,90 @@ +//----------------------------------------------------------------------------- +// The unsaved-changes prompt, for looking at. Three buttons and a message whose +// length depends on the document's name, in a box of a fixed size -- which is +// the shape of the bug that made the Profile Editor's confirm dialog grow to +// fit its message. +// +// Two shots: a Gui that has never been saved (the longest name it can have is +// the default one) and a saved one. +//----------------------------------------------------------------------------- + +setLogMode(2); +setScriptExecEcho(false); +trace(false); +$Scripts::ignoreDSOs = true; +setCompanyAndProduct("Torque Game Engines", "Torque2D"); +ModuleDatabase.EchoInfo = false; +AssetDatabase.EchoInfo = false; + +testExec("editor/main.cs"); + +createPath(testRoot("shots/")); +schedule(2500, 0, "upOpenProject"); + +// The long way round rather than GuiEditor.open(), because the point of a shot +// is the picture: opening the editor the way a person does is what puts the +// editor's own chrome behind the dialog instead of a black canvas. +function upOpenProject() +{ + ProjectManager.setProjectFolder("PlanetX"); + EditorCore.projectSelector.onProjectSelected(pathConcat(getMainDotCsDir(), "PlanetX")); + schedule(2500, 0, "upOpenEditor"); +} + +// Pages register in load order: EditorConsole, ProjectManager, AssetAdmin, +// GuiEditor. +function upOpenEditor() +{ + EditorCore.toggleEditor(); + EditorCore.tabBook.selectPage(3); + schedule(1500, 0, "upStep1"); +} + +function upStep1() +{ + %ctrl = new GuiButtonCtrl() { Position = "10 10"; Extent = "80 30"; Text = "A"; }; + GuiEditor.rootGui.add(%ctrl); + + GuiEditor.inspectorWindow.pane.bind(%ctrl); + GuiEditor.inspectorWindow.pane.writeField("Text", "unsaved work"); + + GuiEditor.NewGui(); + schedule(600, 0, "upStep2"); +} + +function upStep2() +{ + screenShot(testRoot("shots/unsavedPromptUntitled.png"), "PNG"); + schedule(400, 0, "upStep3"); +} + +function upStep3() +{ + discardUnsavedPrompt(); + + // And again with a name that came from a file, which is the longer message. + %ctrl = new GuiButtonCtrl() { Position = "10 10"; Extent = "80 30"; Text = "A"; }; + GuiEditor.rootGui.add(%ctrl); + + GuiEditor.fileName = "titleScreen.gui"; + GuiEditor.refreshDocumentTitle(); + + GuiEditor.inspectorWindow.pane.bind(%ctrl); + GuiEditor.inspectorWindow.pane.writeField("Text", "unsaved work"); + + GuiEditor.NewGui(); + schedule(600, 0, "upStep4"); +} + +function upStep4() +{ + screenShot(testRoot("shots/unsavedPromptNamed.png"), "PNG"); + schedule(400, 0, "upDone"); +} + +function upDone() +{ + discardUnsavedPrompt(); + echo("PROMPT SHOTS DONE"); + quit(); +} diff --git a/tests/smoke/explorerGutter.cs b/tests/smoke/explorerGutter.cs index 51c747488..4029af721 100644 --- a/tests/smoke/explorerGutter.cs +++ b/tests/smoke/explorerGutter.cs @@ -84,6 +84,13 @@ function gutOpenEditor() function gutClear() { GuiEditor.NewGui(); + + // In case PlanetX's Gui arrived by a route that counts as an edit. It does + // not today, and a clean document is taken away without asking, so this is + // usually a no-op -- but an unanswered prompt would leave the tree showing + // the Gui this step exists to clear. + discardUnsavedPrompt(); + schedule(500, 0, "gutRun"); } diff --git a/tests/smoke/undo.cs b/tests/smoke/undo.cs index a2f957224..9934e46f0 100644 --- a/tests/smoke/undo.cs +++ b/tests/smoke/undo.cs @@ -793,7 +793,12 @@ function uStepStale() uUndoCount() == 0 && uRedoCount() == 1); // A new document. Every id on the stack has just been freed. + // + // Through the prompt, because a document with edits in it is no longer thrown + // away without a word - and this one is nothing but edits. What the prompt + // itself does is tests/smoke/unsaved.cs; here it is only the way to the New. GuiEditor.NewGui(); + uCheck("New Gui on a modified document asks first", discardUnsavedPrompt()); uCheck("New Gui empties both stacks", uUndoCount() == 0 && uRedoCount() == 0); schedule(300, 0, "uStepChain"); diff --git a/tests/smoke/unsaved.cs b/tests/smoke/unsaved.cs new file mode 100644 index 000000000..0777014cf --- /dev/null +++ b/tests/smoke/unsaved.cs @@ -0,0 +1,417 @@ +//----------------------------------------------------------------------------- +// Unsaved-changes protection: the modified flag, the name on screen, the prompt +// that stands between a modified Gui and the four commands that would discard +// it, and Revert. +// +// The flag is derived from the undo recorder, which is already the one funnel +// every change goes through. What makes that worth testing rather than assuming +// is that depth is not identity: save, undo, then edit differently and the stack +// is exactly as deep as it was with a different document underneath it. A flag +// built on getUndoCount reads clean there, which is the one direction that must +// never happen, so that case has a check of its own below. +// +// Run: tests/run.ps1 unsaved ; grep SAVE in console.log. +//----------------------------------------------------------------------------- + +setLogMode(2); +setScriptExecEcho(false); +trace(false); +$Scripts::ignoreDSOs = true; +setCompanyAndProduct("Torque Game Engines", "Torque2D"); +ModuleDatabase.EchoInfo = false; +AssetDatabase.EchoInfo = false; +AssetDatabase.IgnoreAutoUnload = true; + +testExec("editor/main.cs"); + +function usCheck(%label, %condition) +{ + echo(%condition ? ("SAVE PASS: " @ %label) : ("SAVE FAIL: " @ %label)); +} + +function usModified() +{ + return GuiEditor.undoRecorder.isModified(); +} + +// A dialog is pushed onto the Canvas and nothing keeps a handle to it, so it is +// found the way it is displayed: as the Canvas's newest child. +function usDialog(%class) +{ + for(%i = Canvas.getCount() - 1; %i >= 0; %i--) + { + %obj = Canvas.getObject(%i); + if(%obj.class $= %class) + { + return %obj; + } + } + + return 0; +} + +// One edit, through the properties pane, which is the route every field change +// the user makes takes. +function usEdit(%ctrl, %field, %value) +{ + GuiEditor.brain.clearSelection(); + GuiEditor.brain.select(%ctrl); + + %pane = GuiEditor.inspectorWindow.pane; + %pane.bind(%ctrl); + %pane.writeField(%field, %value); +} + +schedule(2000, 0, "usSetup"); + +// A throwaway project rather than PlanetX, because this suite writes a Gui file +// and PlanetX is real content. run.ps1 clears any folder a test names ending in +// SmokeProject before each run, so the save below has somewhere of its own to +// land. Nothing here needs a theme. +function usSetup() +{ + ProjectManager.setProjectFolder("unsavedSmokeProject"); + createPath(testRoot("unsavedSmokeProject/")); + + GuiEditor.open(); + + $usCtrl = new GuiButtonCtrl() { Position = "10 10"; Extent = "80 30"; Text = "A"; }; + GuiEditor.rootGui.add($usCtrl); + + // The add itself is an edit, so start the measurements from a clean slate. + GuiEditor.undoRecorder.markClean(); + + schedule(300, 0, "usStepFlag"); +} + +//----------------------------------------------------------------------------- +// The flag. +//----------------------------------------------------------------------------- + +function usStepFlag() +{ + usCheck("a document nobody has touched is not modified", !usModified()); + + usEdit($usCtrl, "Text", "B"); + usCheck("an edit marks it modified", usModified()); + + GuiEditor.undoRecorder.markClean(); + usCheck("saving clears it", !usModified()); + + // Back to where the save was taken. The document is byte for byte what was + // written, so it is not modified, and a flag that only ever latches would say + // otherwise. + usEdit($usCtrl, "Text", "C"); + GuiEditor.Undo(); + usCheck("undoing back to the save point clears it", !usModified()); + + GuiEditor.Redo(); + usCheck("and redoing away from it marks it again", usModified()); + + schedule(300, 0, "usStepBranch"); +} + +// The case a depth counter gets wrong: undo past the save point, then make a +// different edit. The stack is the same height it was at the save, and the +// document is not the document that was saved. +function usStepBranch() +{ + GuiEditor.undoRecorder.clear(); + GuiEditor.undoRecorder.markClean(); + + usEdit($usCtrl, "Text", "one"); + %depth = GuiEditor.undoRecorder.undoCount(); + GuiEditor.undoRecorder.markClean(); + + GuiEditor.Undo(); + usCheck("undo past the save point marks it", usModified()); + + usEdit($usCtrl, "Text", "two"); + usCheck("the stack is back to the depth it was saved at", + GuiEditor.undoRecorder.undoCount() == %depth); + usCheck("but a different edit is still modified", usModified()); + + schedule(300, 0, "usStepWipe"); +} + +// Clearing the stack does not clean the document. detachTheme does exactly this +// -- a profile the document is wearing is about to be freed, so every record +// naming it has to go -- and the controls are just as edited afterwards. +function usStepWipe() +{ + GuiEditor.undoRecorder.clear(); + GuiEditor.undoRecorder.markClean(); + + usEdit($usCtrl, "Text", "edited"); + usCheck("modified before the wipe", usModified()); + + GuiEditor.undoRecorder.clear(); + usCheck("and still modified after it", usModified()); + + schedule(300, 0, "usStepTitle"); +} + +//----------------------------------------------------------------------------- +// The name on screen. The Gui Tools window's own title carries it: the window is +// the top-left panel and already holds the theme buttons, so it becomes the +// strip that says what is being worked on. +//----------------------------------------------------------------------------- + +function usTitle() +{ + return GuiEditor.guiToolsWindow.getText(); +} + +function usStepTitle() +{ + GuiEditor.undoRecorder.clear(); + GuiEditor.undoRecorder.markClean(); + + usCheck("a Gui with no file of its own is untitled", usTitle() $= "untitled.gui"); + + usEdit($usCtrl, "Text", "titled"); + usCheck("and wears a marker once edited", usTitle() $= "untitled.gui *"); + + // A real save rather than writing the field, because the point is that + // SaveCore both clears the flag and puts the new name up. + $usFile = testRoot("unsavedSmokeProject/unsavedTitle.gui"); + GuiEditor.SaveCore($usFile, 0, "unsavedSmokeProject", ""); + + usCheck("saving puts the file name up", usTitle() $= "unsavedTitle.gui"); + usCheck("and takes the marker off", !usModified()); + + usEdit($usCtrl, "Text", "changed since"); + usCheck("editing a saved Gui marks it again", usTitle() $= "unsavedTitle.gui *"); + + usCheck("and the file really was written", isFile($usFile)); + + schedule(300, 0, "usStepNoAsk"); +} + +//----------------------------------------------------------------------------- +// The guard. Four commands throw the document away; each one asks first, and +// only when there is something to lose. +//----------------------------------------------------------------------------- + +function usGuardDialog() +{ + return usDialog("GuiEditorConfirmSaveDialog"); +} + +// A control on the canvas with an edit against it, so there is work to lose. +function usDirtyDocument() +{ + $usCtrl = new GuiButtonCtrl() { Position = "10 10"; Extent = "80 30"; Text = "A"; }; + GuiEditor.rootGui.add($usCtrl); + usEdit($usCtrl, "Text", "worth keeping"); +} + +function usStepNoAsk() +{ + GuiEditor.undoRecorder.markClean(); + GuiEditor.NewGui(); + + usCheck("New on a clean document does not ask", !isObject(usGuardDialog())); + usCheck("it empties the document", GuiEditor.rootGui.getCount() == 0); + usCheck("the new document is clean", !usModified()); + usCheck("and untitled again", usTitle() $= "untitled.gui"); + + schedule(300, 0, "usStepCancel"); +} + +function usStepCancel() +{ + usDirtyDocument(); + %count = GuiEditor.rootGui.getCount(); + + GuiEditor.NewGui(); + %dialog = usGuardDialog(); + usCheck("New on a modified document asks", isObject(%dialog)); + + %dialog.onCancel(); + usCheck("Cancel leaves the document alone", GuiEditor.rootGui.getCount() == %count); + usCheck("and leaves it modified", usModified()); + + schedule(300, 0, "usStepDiscard"); +} + +function usStepDiscard() +{ + GuiEditor.NewGui(); + %dialog = usGuardDialog(); + usCheck("still asking", isObject(%dialog)); + + %dialog.onDiscard(); + usCheck("Discard goes through with the New", GuiEditor.rootGui.getCount() == 0); + usCheck("and what it left behind is clean", !usModified()); + + schedule(300, 0, "usStepSave"); +} + +// Save from the prompt, on a Gui that already has a file: it writes and carries +// on without another question. +function usStepSave() +{ + usDirtyDocument(); + GuiEditor.SaveCore($usFile, 0, "unsavedSmokeProject", ""); + usEdit($usCtrl, "Text", "changed after the save"); + + GuiEditor.NewGui(); + %dialog = usGuardDialog(); + usCheck("a saved-but-changed Gui asks too", isObject(%dialog)); + + %dialog.onSave(); + usCheck("Save did not need the Save As dialog", + !isObject(usDialog("GuiEditorSaveGuiDialog"))); + usCheck("and then went through with the New", GuiEditor.rootGui.getCount() == 0); + usCheck("leaving a clean document", !usModified()); + + schedule(300, 0, "usStepSaveAsCancelled"); +} + +// The trap. On a Gui with no file, Save opens Save As -- which has its own +// Cancel, and taking it must abandon the New rather than quietly going ahead +// with it having saved nothing. +function usStepSaveAsCancelled() +{ + usDirtyDocument(); + %count = GuiEditor.rootGui.getCount(); + + GuiEditor.NewGui(); + usGuardDialog().onSave(); + + %saveDialog = usDialog("GuiEditorSaveGuiDialog"); + usCheck("Save on a never-saved Gui opens Save As", isObject(%saveDialog)); + + %saveDialog.onClose(); + usCheck("cancelling Save As leaves the document in place", + GuiEditor.rootGui.getCount() == %count); + usCheck("and leaves it modified", usModified()); + + schedule(300, 0, "usStepMenuCommands"); +} + +// The two exits that cannot be run from a suite that has to survive to report. +// Menu items are nested controls, so this walks rather than indexes. +function usMenuItem(%parent, %text) +{ + for(%i = 0; %i < %parent.getCount(); %i++) + { + %item = %parent.getObject(%i); + if(%item.Text $= %text) + { + return %item; + } + + %found = usMenuItem(%item, %text); + if(isObject(%found)) + { + return %found; + } + } + + return 0; +} + +function usStepMenuCommands() +{ + %exit = usMenuItem(EditorCore.menuBar, "Exit"); + %closeProject = usMenuItem(EditorCore.menuBar, "Close Project"); + + usCheck("the Exit item exists", isObject(%exit)); + usCheck("the Close Project item exists", isObject(%closeProject)); + + // Both are EditorCore.guardedCommand with the real command inside, and + // running either one for real would take the process with it. So the route + // itself is exercised with a harmless command instead, and the two items are + // checked for naming that route. + usCheck("Exit goes through the guard", + strstr(%exit.Command, "guardedCommand") >= 0); + usCheck("Close Project goes through the guard", + strstr(%closeProject.Command, "guardedCommand") >= 0); + + schedule(300, 0, "usStepGuardedCommand"); +} + +// The route those two items take, with something safe at the end of it. +function usStepGuardedCommand() +{ + $usRan = false; + + // Nothing to lose: straight through. + GuiEditor.undoRecorder.markClean(); + EditorCore.guardedCommand("$usRan = true;"); + usCheck("a guarded command on a clean document just runs", $usRan); + + // Something to lose: held until answered for. + $usRan = false; + usDirtyDocument(); + EditorCore.guardedCommand("$usRan = true;"); + + usCheck("on a modified one it asks first", isObject(usGuardDialog())); + usCheck("and holds the command back", !$usRan); + + usGuardDialog().onCancel(); + usCheck("Cancel drops it for good", !$usRan); + + EditorCore.guardedCommand("$usRan = true;"); + usGuardDialog().onDiscard(); + usCheck("Discard lets it through", $usRan); + + schedule(300, 0, "usStepRevertGreyed"); +} + +//----------------------------------------------------------------------------- +// Revert, which is the only command here that puts something back rather than +// taking it away - and which still asks first, because putting the file back is +// discarding everything done since. +//----------------------------------------------------------------------------- + +function usStepRevertGreyed() +{ + // A document with no file of its own. There is nothing to revert TO. + GuiEditor.NewGui(); + usGuardDialog().onDiscard(); + + $usRevert = usMenuItem(EditorCore.menuBar, "Revert"); + usCheck("the Revert item exists", isObject($usRevert)); + usCheck("and is not offered before the first save", !$usRevert.Active); + + schedule(300, 0, "usStepRevert"); +} + +function usStepRevert() +{ + $usCtrl = new GuiButtonCtrl() { Position = "10 10"; Extent = "80 30"; Text = "A"; }; + GuiEditor.rootGui.add($usCtrl); + usEdit($usCtrl, "Text", "on disk"); + + GuiEditor.SaveCore($usFile, 0, "unsavedSmokeProject", ""); + usCheck("Revert is offered once the Gui has a file", $usRevert.Active); + + usEdit($usCtrl, "Text", "not on disk"); + usCheck("and the change is showing", $usCtrl.getText() $= "not on disk"); + + GuiEditor.Revert(); + usCheck("Revert asks before discarding", isObject(usGuardDialog())); + usGuardDialog().onDiscard(); + + schedule(300, 0, "usStepRevertCheck"); +} + +function usStepRevertCheck() +{ + usCheck("the document was re-read", GuiEditor.rootGui.getCount() == 1); + usCheck("with what was on disk in it", + GuiEditor.rootGui.getObject(0).getText() $= "on disk"); + usCheck("and it is clean again", !usModified()); + usCheck("still under its own name", usTitle() $= "unsavedTitle.gui"); + + schedule(300, 0, "usDone"); +} + +function usDone() +{ + echo("SAVE DONE"); + quit(); +} From 5e06aa022f1bac6150d20b3ab4cad2443b4958f8 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Mon, 3 Aug 2026 12:33:29 -0400 Subject: [PATCH 34/55] GuiEditCtrl: remove saveSelection and loadSelection A prefab mechanism from the original editor that nothing has ever called. No script in this repository names either one, and the pair was already orphaned when the 4.0 editor was written around this control. It is not worth reviving. saveSelection writes through SimSet::write, the old console-object format, into a file nothing else in the editor reads, and loadSelection execs that file back and hunts for a set called guiClipboard by name. The editor grew its own answer to this on the way past: the clipboard holds deep clones, uniquifies the names they arrive with, and places them through the same acceptControl every dropped control uses. If prefabs are wanted later, that is the foundation, not this. Neither was dangerous - I checked, because at a glance saveSelection looks like it reparents the selection into a set and then deletes it. SimSet does not own its members (SimSet::onRemove only clears notifications, where SimGroup::onRemove unregisters), so the controls would have survived. Dead rather than sharp. GuiEditorBrain::onAddNewCtrlSet goes with them. loadSelection was the only caller of that callback and the brain was its only implementation, so it has been recording undo for an event that could not be raised. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qi2pZ9UzF5rsLCNVErpzwt --- editor/GuiEditor/scripts/GuiEditorBrain.cs | 11 ---- engine/source/gui/editor/guiEditCtrl.cc | 62 ---------------------- engine/source/gui/editor/guiEditCtrl.h | 2 - 3 files changed, 75 deletions(-) diff --git a/editor/GuiEditor/scripts/GuiEditorBrain.cs b/editor/GuiEditor/scripts/GuiEditorBrain.cs index ca75bf2ad..0c79df459 100644 --- a/editor/GuiEditor/scripts/GuiEditorBrain.cs +++ b/editor/GuiEditor/scripts/GuiEditorBrain.cs @@ -590,17 +590,6 @@ GuiEditor.undoRecorder.recordAdd(%ctrl, ""); } -// The same for a whole set of them, which is how a loaded selection arrives. -function GuiEditorBrain::onAddNewCtrlSet(%this, %selection) -{ - GuiEditor.undoRecorder.begin("Add Controls", ""); - for(%i = 0; %i < %selection.getCount(); %i++) - { - GuiEditor.undoRecorder.recordAdd(%selection.getObject(%i), ""); - } - GuiEditor.undoRecorder.end(); -} - // Put the selection on the controls a replay changed, and tell everyone. function GuiEditorBrain::restoreSelection(%this, %list) { diff --git a/engine/source/gui/editor/guiEditCtrl.cc b/engine/source/gui/editor/guiEditCtrl.cc index 328464c22..dfd3437d0 100755 --- a/engine/source/gui/editor/guiEditCtrl.cc +++ b/engine/source/gui/editor/guiEditCtrl.cc @@ -209,18 +209,6 @@ ConsoleMethod(GuiEditCtrl, moveSelection, void, 4, 4, "(int deltax, int deltay) } } -ConsoleMethod(GuiEditCtrl, saveSelection, void, 3, 3, "(string fileName) Saves the current selection to given filename\n" - "@return No return value.") -{ - object->saveSelection(argv[2]); -} - -ConsoleMethod(GuiEditCtrl, loadSelection, void, 3, 3, "(string fileName) Loads from given filename\n" - "@return No return value.") -{ - object->loadSelection(argv[2]); -} - ConsoleMethod(GuiEditCtrl, selectAll, void, 2, 2, "() Selects all controls\n" "@return No return value.") { @@ -1424,56 +1412,6 @@ void GuiEditCtrl::deleteSelection(void) mSelectedControls.clear(); } -void GuiEditCtrl::loadSelection(const char* filename) -{ - if (!mCurrentAddSet) - mCurrentAddSet = mEditorRoot; - - Con::executef(2, "exec", filename); - SimSet* set; - if (!Sim::findObject("guiClipboard", set)) - return; - - if (set->size()) - { - Con::executef(this, 1, "onClearSelected"); - mSelectedControls.clear(); - for (U32 i = 0; i < (U32)set->size(); i++) - { - GuiControl* ctrl = dynamic_cast((*set)[i]); - if (ctrl) - { - mCurrentAddSet->addObject(ctrl); - mSelectedControls.push_back(ctrl); - Con::executef(this, 2, "onAddSelected", Con::getIntArg(ctrl->getId())); - } - } - Con::executef(this, 2, "onAddNewCtrlSet", Con::getIntArg(getSelectedSet().getId())); - } - set->deleteObject(); -} - -void GuiEditCtrl::saveSelection(const char* filename) -{ - // if there are no selected objects, then don't save - if (mSelectedControls.size() == 0) - return; - - FileStream stream; - if (!ResourceManager->openFileForWrite(stream, filename)) - return; - SimSet* clipboardSet = new SimSet; - clipboardSet->registerObject(); - Sim::getRootGroup()->addObject(clipboardSet, "guiClipboard"); - - Vector::iterator i; - for (i = mSelectedControls.begin(); i != mSelectedControls.end(); i++) - clipboardSet->addObject(*i); - - clipboardSet->write(stream, 0); - clipboardSet->deleteObject(); -} - void GuiEditCtrl::selectAll() { if (!mCurrentAddSet) diff --git a/engine/source/gui/editor/guiEditCtrl.h b/engine/source/gui/editor/guiEditCtrl.h index 1e9173d45..1b91a9c4f 100755 --- a/engine/source/gui/editor/guiEditCtrl.h +++ b/engine/source/gui/editor/guiEditCtrl.h @@ -130,8 +130,6 @@ class GuiEditCtrl : public GuiControl void justifySelection( Justification j); void moveSelection(const Point2I &delta); void moveAndSnapSelection(const Point2I &delta); - void saveSelection(const char *filename); - void loadSelection(const char *filename); void addSelection(S32 id); void removeSelection(S32 id); void deleteSelection(); From 3aff6ba24012cf9e46d13c5de77c1a173f638685 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Mon, 3 Aug 2026 12:33:43 -0400 Subject: [PATCH 35/55] README: describe the Gui Editor that is actually there It still sold the editor as "an inspector, tree view, menus, save/load dialogs, frame set layouts, color picker, and control reordering" - written before most of the work and describing an inspector that has since been replaced. Nothing about the control palette, the theme system, the Profile Editor, undo, or the clipboard, all of which are the reasons to use it. This is the first thing anyone evaluating 4.0 reads, so it now says what you would notice in the first ten minutes: how a screen gets built, what the two side panels are for, that the properties pane offers only the fields the selected class actually reads, that a Gui you have changed is not thrown away without being asked, and that appearance comes from a theme rather than from profiles wired up by hand. Two claims deliberately not made. "Every edit is undoable" is not quite true - a frame set inside the Gui being authored can handle its own divider drags through onMouseDownEditor, and nothing records those - so it says there is undo and redo and leaves it there. And the Explorer is not opposite the properties pane; it shares the right-hand column with the palette. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qi2pZ9UzF5rsLCNVErpzwt --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0b2c066af..a6193e146 100644 --- a/README.md +++ b/README.md @@ -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 3 introduces a **full GUI Editor**, built from the ground up to replace the previous Gui Editor Toy. 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). From ca3635ee12788f3f922be0273bad14c303b3e7d5 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Mon, 3 Aug 2026 15:07:04 -0400 Subject: [PATCH 36/55] Gui Editor: Duplicate, and a Delete you can find The Edit menu offered Undo, Redo, Cut, Copy and Paste and stopped there. Delete existed but was invisible. The key works on the canvas and in the Explorer tree, and both routes have been recorded for undo all along, but nothing in a menu said so and there was no other way to reach it. It is now the last item in Edit, with the key beside it. Duplicate did not exist. It was Ctrl+C, Ctrl+V, then drag the copy back to roughly where you wanted it -- and the Ctrl+C took whatever was on the clipboard with it. Now it is one command that puts the copy in the parent the original is in, one grid step off, and leaves the clipboard alone. That last part is the whole reason it is a command rather than a habit, and the suite checks it directly: copy one control, duplicate another, paste, and what arrives is what was copied. It lives on GuiEditorClipboard because four of the five things it needs are already there and are exactly right -- the reduction that stops a control being copied twice when it and its parent are both selected, the name carrying that deepClone deliberately does not do, the uniquifying, and the counting-on from a trailing number so a copy of okButton2 is okButton3. Two things a paste has to do it does not: no canBeChildOf test, because the original is already a legal child of that parent, and no per-container step count, because a duplicate is always one step from its own original. Cut is now copy-then-DeleteSelection rather than carrying its own copy of what deleting means. DeleteSelection, and not Delete, for a reason worth writing down: delete is a console method on every SimObject, so GuiEditor.Delete() destroys the editor. Quietly, too -- the object goes and the next line to touch GuiEditor is the one that reports an error, which in a test reads as four unrelated failures several steps later. The smoke suite found it on the first run. Ctrl+D goes to Duplicate, so Deselect moves to Ctrl-Shift A, which is what it is bound to nearly everywhere else and pairs with Ctrl+A above it. The Delete accelerator cannot double-fire with the key the canvas and tree handle themselves: the canvas consults accelerators only once the first responder has passed on the key, the same mechanism 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. Both items follow the selection in toggleMenuItems, beside Cut and Copy. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qi2pZ9UzF5rsLCNVErpzwt --- editor/EditorCore/EditorCore.cs | 26 +++- editor/GuiEditor/GuiEditor.cs | 30 ++++- editor/GuiEditor/scripts/GuiEditorBrain.cs | 2 + .../GuiEditor/scripts/GuiEditorClipboard.cs | 98 +++++++++++++++ tests/smoke/clipboard.cs | 113 ++++++++++++++++++ tests/smoke/explorerDelete.cs | 26 ++++ 6 files changed, 288 insertions(+), 7 deletions(-) diff --git a/editor/EditorCore/EditorCore.cs b/editor/EditorCore/EditorCore.cs index 21ec11b9b..e5ca147fa 100644 --- a/editor/EditorCore/EditorCore.cs +++ b/editor/EditorCore/EditorCore.cs @@ -171,6 +171,27 @@ Command = "GuiEditor.Paste();"; Accelerator = "Ctrl V"; }; + new GuiMenuItemCtrl() { + Text = "Duplicate"; + Command = "GuiEditor.Duplicate();"; + Accelerator = "Ctrl D"; + }; + new GuiMenuItemCtrl() { Text = "-"; }; + // 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. + new GuiMenuItemCtrl() { + Text = "Delete"; + Command = "GuiEditor.DeleteSelection();"; + Accelerator = "Delete"; + }; }; new GuiMenuItemCtrl() { Text = "Layout"; @@ -286,10 +307,13 @@ Command = "GuiEditor.brain.SelectAll();"; Accelerator = "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. new GuiMenuItemCtrl() { Text = "Deselect"; Command = "GuiEditor.brain.clearSelection();"; - Accelerator = "Ctrl D"; + Accelerator = "Ctrl-Shift A"; }; }; new GuiMenuItemCtrl() { diff --git a/editor/GuiEditor/GuiEditor.cs b/editor/GuiEditor/GuiEditor.cs index 6a7004cb2..c5923cd97 100644 --- a/editor/GuiEditor/GuiEditor.cs +++ b/editor/GuiEditor/GuiEditor.cs @@ -1170,12 +1170,8 @@ class = "GuiEditorThemeDialog"; %this.clipboard.copy(%this.brain.getSelected()); } -// Copy, then the delete the Delete key already does: the C++ moves the selection -// into the trash and announces it, which the recorder turns into one undo step -// (GuiEditorBrain::onTrashSelection). Nothing is deleted for real, so a cut is -// undoable and the controls it took 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. +// 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())) @@ -1183,6 +1179,21 @@ class = "GuiEditorThemeDialog"; 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(); } @@ -1192,6 +1203,13 @@ class = "GuiEditorThemeDialog"; %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, diff --git a/editor/GuiEditor/scripts/GuiEditorBrain.cs b/editor/GuiEditor/scripts/GuiEditorBrain.cs index 0c79df459..8ad70b519 100644 --- a/editor/GuiEditor/scripts/GuiEditorBrain.cs +++ b/editor/GuiEditor/scripts/GuiEditorBrain.cs @@ -649,6 +649,8 @@ EditorCore.menuBar.setMenuActive("Deselect", %count != 0); EditorCore.menuBar.setMenuActive("Cut", %count != 0); EditorCore.menuBar.setMenuActive("Copy", %count != 0); + EditorCore.menuBar.setMenuActive("Duplicate", %count != 0); + EditorCore.menuBar.setMenuActive("Delete", %count != 0); EditorCore.menuBar.setMenuActive("Nudge Up", %count != 0); EditorCore.menuBar.setMenuActive("Nudge Down", %count != 0); EditorCore.menuBar.setMenuActive("Nudge Left", %count != 0); diff --git a/editor/GuiEditor/scripts/GuiEditorClipboard.cs b/editor/GuiEditor/scripts/GuiEditorClipboard.cs index 4c2325ed3..6aebf3362 100644 --- a/editor/GuiEditor/scripts/GuiEditorClipboard.cs +++ b/editor/GuiEditor/scripts/GuiEditorClipboard.cs @@ -3,6 +3,10 @@ // 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 @@ -405,6 +409,100 @@ 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. //----------------------------------------------------------------------------- diff --git a/tests/smoke/clipboard.cs b/tests/smoke/clipboard.cs index baae37c8c..2333bd78f 100644 --- a/tests/smoke/clipboard.cs +++ b/tests/smoke/clipboard.cs @@ -713,6 +713,119 @@ function cStepChainRun() GuiEditor.Undo(); cCheck("undo took it back out", $cChain.getCount() == 3); + schedule(300, 0, "cStepDuplicate"); +} + +//----------------------------------------------------------------------------- +// Duplicate, which is a copy that never touches the clipboard: it lands in the +// control's OWN parent, one grid step off, whatever container is currently being +// worked in and whatever is on the clipboard at the time. +//----------------------------------------------------------------------------- + +function cStepDuplicate() +{ + GuiEditor.undoRecorder.clear(); + %grid = GuiEditor.brain.getGridSize(); + + %before = $cPanel.getCount(); + %at = $cA.getPosition(); + + cSelect($cA); + GuiEditor.Duplicate(); + + %copy = cPasted(0); + cCheck("duplicate made one control", cPastedCount() == 1); + cCheck("in the same parent as the original", %copy.getParent() == $cPanel); + cCheck("which now holds one more", $cPanel.getCount() == %before + 1); + cCheck("the original is still there", $cA.getParent() == $cPanel); + + cCheck("the copy is one grid step across", + getWord(%copy.getPosition(), 0) == getWord(%at, 0) + %grid); + cCheck("and one down", + getWord(%copy.getPosition(), 1) == getWord(%at, 1) + %grid); + + cCheck("the copy counted on from the original's name", + %copy.getName() $= "clipA2"); + cCheck("and kept its caption", %copy.Text $= $cA.Text); + + cCheck("duplicate is one step", cUndoCount() == 1); + GuiEditor.Undo(); + cCheck("undo took the copy back out", $cPanel.getCount() == %before); + + schedule(300, 0, "cStepDuplicateClipboard"); +} + +// The whole reason it is not Ctrl+C, Ctrl+V: what is on the clipboard is still +// on the clipboard afterwards. +function cStepDuplicateClipboard() +{ + GuiEditor.undoRecorder.clear(); + + cSelect($cB); + GuiEditor.Copy(); + + cSelect($cA); + GuiEditor.Duplicate(); + + cCheck("the clipboard still holds something", !GuiEditor.clipboard.isEmpty()); + + GuiEditor.brain.setCurrentAddSet($cOther); + GuiEditor.Paste(); + + %pasted = cPasted(0); + cCheck("and what it holds is what was copied, not what was duplicated", + %pasted.Text $= $cB.Text); + + schedule(300, 0, "cStepDuplicateNested"); +} + +// A panel and a button inside it: the reduction that stops the button being +// duplicated twice is the same one copy uses. +function cStepDuplicateNested() +{ + GuiEditor.undoRecorder.clear(); + + %before = GuiEditor.rootGui.getCount(); + + GuiEditor.brain.clearSelection(); + GuiEditor.brain.addSelection($cPanel); + GuiEditor.brain.addSelection($cA); + + GuiEditor.Duplicate(); + + cCheck("the panel was duplicated once", cPastedCount() == 1); + cCheck("into the root", GuiEditor.rootGui.getCount() == %before + 1); + + %copy = cPasted(0); + cCheck("and the button came with it rather than separately", + %copy.getCount() == $cPanel.getCount()); + + cCheck("still one step", cUndoCount() == 1); + GuiEditor.Undo(); + cCheck("undo removed the whole thing", GuiEditor.rootGui.getCount() == %before); + + schedule(300, 0, "cStepMenuGreying"); +} + +// Both new items follow the selection, the way Cut and Copy beside them do. +// There is nothing to duplicate or delete when nothing is selected, and an item +// that stays lit is an item that lies about it. +function cStepMenuGreying() +{ + %duplicate = cMenuItem(EditorCore.menuBar, "Duplicate"); + %delete = cMenuItem(EditorCore.menuBar, "Delete"); + + cCheck("the Duplicate item exists", isObject(%duplicate)); + cCheck("the Delete item exists", isObject(%delete)); + + GuiEditor.brain.clearSelection(); + cCheck("Duplicate is greyed with nothing selected", !%duplicate.Active); + cCheck("Delete is greyed with nothing selected", !%delete.Active); + + cSelect($cA); + cCheck("Duplicate is offered once something is", %duplicate.Active); + cCheck("Delete is offered once something is", %delete.Active); + schedule(300, 0, "cDone"); } diff --git a/tests/smoke/explorerDelete.cs b/tests/smoke/explorerDelete.cs index cb943feb2..00d00c452 100644 --- a/tests/smoke/explorerDelete.cs +++ b/tests/smoke/explorerDelete.cs @@ -140,6 +140,32 @@ function edTreeKey() edCheck("undo brought the control back", %ctrl.getGroup() == GuiEditor.rootGui); edCheck("and the tree lists it again", %tree.findItemID(%ctrl) != -1); + schedule(100, 0, "edMenu"); +} + +//----------------------------------------------------------------------------- +// The Edit menu's Delete, which is the route that does not need the right thing +// to hold first responder first -- and the only one that says out loud that the +// command exists. +// +// DeleteSelection, NOT Delete: delete is a console method on every SimObject, so +// GuiEditor.Delete() destroys the editor. It does it quietly, too -- the object +// goes, and the next line to touch GuiEditor is the one that reports an error. +//----------------------------------------------------------------------------- + +function edMenu() +{ + %tree = GuiEditor.explorerWindow.tree; + %ctrl = edPlace(); + %items = %tree.getItemCount(); + + GuiEditor.DeleteSelection(); + edGone("menu Delete", %ctrl, %items); + + // Cut is this with a copy in front of it, and says so by calling it. + GuiEditor.Undo(); + edCheck("undo brought it back once more", %ctrl.getGroup() == GuiEditor.rootGui); + echo("EDEL DONE " @ $Pass @ " passed, " @ $Fail @ " failed"); quit(); } From 615c4f3a51b4975c3aab8c14ebe1261fb6fed94b Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Tue, 4 Aug 2026 20:53:21 -0400 Subject: [PATCH 37/55] GuiProfileTheme: cursors, and an editor for aiming them Cursors were the last part of the GUI a theme did not own. AppCore built seven of them out of literals, and the file said so itself: "Cursors have not moved into the theme yet, so they stay here." So a themed Gui got themed everything except its pointer. They are now the third member family, and they follow the profile shape rather than the border one: seven categories, each guaranteed a default member, each able to hold extras. A category with two cursors in it is the thing that makes the Gui Editor offer a choice on a control's cursor slot. Art is per theme, in /themes/cursors/. Two themes in one game may want cursors that look nothing alike -- a menu pointer and a combat reticle -- and a shared folder would mean one overwriting the other's files. Which forces the rule the rest of this hangs on: bitmapName, hotSpot and renderOffset are set once when the member is created and are never stamped. Art cannot be derived from a palette, so a restamp must not fight what the user chose. They are also exempt from override tracking, because there is no theme value behind them to override or to reset to -- and they persist regardless, since nothing could rebuild them on load. Only the tint is stamped, from colorForeground: a pointer is the mouse's equivalent of text and wants the same contrast against the background. The tint is worth having because the stock art is grayscale -- black outline, white body -- so multiplying it colors the body and leaves the outline. A new theme's cursors match its palette with nobody drawing anything. dglClearBitmapModulation is literally white, so a colour field defaulting to white is a byte-for-byte no-op for every cursor that existed before this. hotSpot and renderOffset both stay, and the split is not redundant. renderOffset is a FRACTION of the art's own size, so one value anchors art of any size; that is what stops a 13x17 pointer and a 32x32 sizer from appearing to leap when one replaces the other. hotSpot is the pixel nudge on top of it. The pointer ends up at hotSpot + trunc(extent * renderOffset), and the editor drags only the second. Two defects in GuiCursor had to go first. It cached its TextureHandle and never dropped it, so pointing a live cursor at new art went on drawing the old bitmap forever -- fatal for an editor. And bitmapName is TypeFilename, which expands to an absolute path the moment it is set, so a saved theme would have named a folder on one machine; it now has the same relative read-back GuiControlProfile's bitmap already had. GuiEditorCursorCtrl is the hot-spot editor. A hot spot 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. It draws the art a pixel at a time rather than as a stretched bitmap, because magnifying through the texture filter would blur exactly the boundaries the user is aiming at, and it owns its own decoded copy of the art: a BitmapTexture handle's CPU-side bitmap is freed once the texture manager has uploaded it, so asking the cursor for its pixels gets NULL. The zoom clamps to what fits and reports what it clamped to, because a readout naming a magnification nothing is drawn at is worse than no readout. The name matters: an editor-only Gui control must begin "GuiEdit" or the control palette treats it as placeable. Two rules enforce that by prefix, and the one in GuiEditorControlIcons.cs cannot be hand-edited because that file is generated. The placement values in the table are not the ones this started with. Five of the seven moved once they were aimed in the editor rather than guessed: the resize cursors want their crosshair centred on the pointer rather than a pixel down and right of it, and the two bars want a pixel of lift so the gap between their arrowheads straddles the edge being dragged. That is the editor paying for itself on its first outing. The arithmetic both the dot and the hit test depend on lives in statics that take everything they use, so it can be checked without a canvas -- rendering a cursor needs a GL context and a texture, and a unit test has neither. 14 new tests: the category table, member naming, extras, rename, the tint override surviving a restamp, art NOT surviving one, a Taml round trip, and the placement arithmetic including the truncation the engine does and the editor has to reproduce. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qi2pZ9UzF5rsLCNVErpzwt --- cmake/EngineSources.cmake | 2 + .../source/gui/editor/guiEditorCursorCtrl.cc | 431 ++++++++++++++++++ .../source/gui/editor/guiEditorCursorCtrl.h | 152 ++++++ .../guiEditorCursorCtrl_ScriptBinding.h | 87 ++++ engine/source/gui/guiProfileTheme.cc | 318 ++++++++++++- engine/source/gui/guiProfileTheme.h | 53 +++ .../gui/guiProfileTheme_ScriptBinding.h | 130 +++++- engine/source/gui/guiTypes.cc | 177 ++++++- engine/source/gui/guiTypes.h | 57 +++ .../testing/tests/guiCursorHotSpotTests.cc | 163 +++++++ .../testing/tests/guiProfileThemeTests.cc | 245 ++++++++++ 11 files changed, 1791 insertions(+), 24 deletions(-) create mode 100644 engine/source/gui/editor/guiEditorCursorCtrl.cc create mode 100644 engine/source/gui/editor/guiEditorCursorCtrl.h create mode 100644 engine/source/gui/editor/guiEditorCursorCtrl_ScriptBinding.h create mode 100644 engine/source/testing/tests/guiCursorHotSpotTests.cc diff --git a/cmake/EngineSources.cmake b/cmake/EngineSources.cmake index 40ab45e53..8122d199f 100644 --- a/cmake/EngineSources.cmake +++ b/cmake/EngineSources.cmake @@ -200,6 +200,7 @@ set(TORQUE_ENGINE_SOURCES ${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/guiEditorExplorerTree.cc @@ -339,6 +340,7 @@ set(TORQUE_ENGINE_SOURCES # ---- testing ---- ${TORQUE_SRC}/testing/unitTesting.cc # ---- testing/tests ---- + ${TORQUE_SRC}/testing/tests/guiCursorHotSpotTests.cc ${TORQUE_SRC}/testing/tests/guiProfileThemeTests.cc ${TORQUE_SRC}/testing/tests/guiScrollLayoutTests.cc ${TORQUE_SRC}/testing/tests/guiTextEditTests.cc diff --git a/engine/source/gui/editor/guiEditorCursorCtrl.cc b/engine/source/gui/editor/guiEditorCursorCtrl.cc new file mode 100644 index 000000000..07b66590a --- /dev/null +++ b/engine/source/gui/editor/guiEditorCursorCtrl.cc @@ -0,0 +1,431 @@ +//----------------------------------------------------------------------------- +// 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. +//----------------------------------------------------------------------------- + +#include "gui/editor/guiEditorCursorCtrl.h" +#include "gui/guiTypes.h" +#include "gui/guiDefaultControlRender.h" +#include "graphics/dgl.h" +#include "graphics/gBitmap.h" +#include "console/consoleTypes.h" +#include "io/resource/resourceManager.h" + +#include "guiEditorCursorCtrl_ScriptBinding.h" + +IMPLEMENT_CONOBJECT(GuiEditorCursorCtrl); + +GuiEditorCursorCtrl::GuiEditorCursorCtrl() +{ + mCursor = NULL; + mBitmap = NULL; + mBitmapKey = StringTable->EmptyString; + mZoom = 8; + mDragging = false; + + // Defaults chosen to read against the stock art, which is a white body with + // a black outline: a saturated red dot is visible on both, and the checker + // greys are far enough from either to never be mistaken for the art. + mDotColor.set(220, 40, 40, 255); + mAnchorColor.set(90, 170, 255, 160); + mCheckerLight.set(110, 110, 110, 255); + mCheckerDark.set(80, 80, 80, 255); + mGridColor.set(0, 0, 0, 60); +} + +GuiEditorCursorCtrl::~GuiEditorCursorCtrl() +{ + releaseBitmap(); +} + +void GuiEditorCursorCtrl::initPersistFields() +{ + Parent::initPersistFields(); + + addField("cursor", TypeGuiCursor, Offset(mCursor, GuiEditorCursorCtrl)); + addField("zoom", TypeS32, Offset(mZoom, GuiEditorCursorCtrl)); + addField("dotColor", TypeColorI, Offset(mDotColor, GuiEditorCursorCtrl)); + addField("anchorColor", TypeColorI, Offset(mAnchorColor, GuiEditorCursorCtrl)); + addField("checkerLight", TypeColorI, Offset(mCheckerLight, GuiEditorCursorCtrl)); + addField("checkerDark", TypeColorI, Offset(mCheckerDark, GuiEditorCursorCtrl)); + addField("gridColor", TypeColorI, Offset(mGridColor, GuiEditorCursorCtrl)); +} + +//----------------------------------------------------------------------------- +// The hot-spot arithmetic. Both directions are static and take everything they +// use: guiCanvas.cc positions a cursor at cursorPos - hotSpot and then +// GuiCursor::render subtracts (S32)(extent * renderOffset), so the pointer ends +// up at hotSpot + trunc(extent * renderOffset) within the image. The truncation +// is the engine's, reproduced rather than rounded, so the dot marks the pixel +// that will really be under the mouse. +//----------------------------------------------------------------------------- + +Point2I GuiEditorCursorCtrl::getEffectiveHotSpot(const Point2I& hotSpot, const Point2F& renderOffset, const Point2I& imageExtent) +{ + return Point2I(hotSpot.x + (S32)(imageExtent.x * renderOffset.x), + hotSpot.y + (S32)(imageExtent.y * renderOffset.y)); +} + +Point2I GuiEditorCursorCtrl::getHotSpotForPixel(const Point2I& pixel, const Point2F& renderOffset, const Point2I& imageExtent) +{ + return Point2I(pixel.x - (S32)(imageExtent.x * renderOffset.x), + pixel.y - (S32)(imageExtent.y * renderOffset.y)); +} + +//----------------------------------------------------------------------------- + +void GuiEditorCursorCtrl::setCursorObject(GuiCursor* cursor) +{ + mCursor = cursor; + mDragging = false; +} + +void GuiEditorCursorCtrl::releaseBitmap() +{ + if (mBitmap != NULL) + { + delete mBitmap; + mBitmap = NULL; + } + mBitmapKey = StringTable->EmptyString; +} + +const GBitmap* GuiEditorCursorCtrl::resolveBitmap() +{ + if (mCursor == NULL) + { + releaseBitmap(); + return NULL; + } + + StringTableEntry key = mCursor->getBitmapName(); + if (key == NULL || *key == '\0') + { + releaseBitmap(); + return NULL; + } + + // Reload when the cursor is pointed at different art - which is exactly what + // happens when the user picks a file in the pane beside this. + if (key != mBitmapKey) + { + releaseBitmap(); + mBitmapKey = key; + + // Straight from the resource manager rather than through a + // TextureHandle: a cursor's handle is a BitmapTexture, whose CPU-side + // bitmap the texture manager frees once it has uploaded it. The same + // extension probe TextureManager::loadBitmap does, so a bitmapName + // written without one (as AppCore's hand-made cursors always were) still + // finds its file. + static const char* const extensions[] = { "", ".png", ".jpg" }; + char pathBuffer[1024]; + for (S32 i = 0; i < 3 && mBitmap == NULL; ++i) + { + dSprintf(pathBuffer, sizeof(pathBuffer), "%s%s", key, extensions[i]); + mBitmap = (GBitmap*)ResourceManager->loadInstance(pathBuffer); + } + + if (mBitmap == NULL) + Con::warnf("GuiEditorCursorCtrl - could not read the cursor art '%s'.", key); + } + + return mBitmap; +} + +void GuiEditorCursorCtrl::setZoom(S32 zoom) +{ + mZoom = mClamp(zoom, 1, getMaxZoom()); +} + +// The largest magnification whose art still fits the pane. Asking the control +// rather than assuming smMaxZoom is what keeps the zoom readout honest: a 32x32 +// cursor in a 370x200 pane tops out at 6x, and a control that reported 16x while +// drawing 6x would be lying about the one number the user is steering by. +S32 GuiEditorCursorCtrl::getMaxZoom() +{ + const Point2I imageExtent = getImageExtent(); + if (imageExtent.x <= 0 || imageExtent.y <= 0) + return smMaxZoom; + + Point2I globalOffset = localToGlobalCoord(Point2I(0, 0)); + Point2I extent = mBounds.extent; + const RectI content = getInnerRect(globalOffset, extent, NormalState, mProfile); + + S32 zoom = smMaxZoom; + while (zoom > 1 && (imageExtent.x * zoom > content.extent.x || imageExtent.y * zoom > content.extent.y)) + --zoom; + + return zoom; +} + +Point2I GuiEditorCursorCtrl::getImageExtent() +{ + const GBitmap* bitmap = resolveBitmap(); + if (bitmap == NULL) + return Point2I(0, 0); + + return Point2I((S32)bitmap->getWidth(), (S32)bitmap->getHeight()); +} + +bool GuiEditorCursorCtrl::getArtRect(RectI& artRect, Point2I& imageExtent) +{ + imageExtent = getImageExtent(); + if (imageExtent.x <= 0 || imageExtent.y <= 0) + return false; + + // Global, because the hit test asks this question with a global mouse point + // and the renderer draws in global coordinates too. + Point2I globalOffset = localToGlobalCoord(Point2I(0, 0)); + Point2I extent = mBounds.extent; + const RectI content = getInnerRect(globalOffset, extent, NormalState, mProfile); + + // Never magnify past what fits: a 32x32 cursor at 16x wants 512 pixels, and + // a pane that narrow would otherwise clip the art the user is aiming at. + // Written back rather than used locally, so what mZoom says and what is drawn + // can never differ -- a readout showing a zoom nothing is drawn at is worse + // than no readout. A pane that grows lets the user zoom in again. + S32 zoom = mClamp(mZoom, 1, smMaxZoom); + while (zoom > 1 && (imageExtent.x * zoom > content.extent.x || imageExtent.y * zoom > content.extent.y)) + --zoom; + mZoom = zoom; + + const Point2I size(imageExtent.x * zoom, imageExtent.y * zoom); + artRect.point.set(content.point.x + ((content.extent.x - size.x) / 2), + content.point.y + ((content.extent.y - size.y) / 2)); + artRect.extent = size; + + return true; +} + +Point2I GuiEditorCursorCtrl::pixelAt(const Point2I& globalPoint, const RectI& artRect, const Point2I& imageExtent) +{ + const S32 zoom = artRect.extent.x / imageExtent.x; + if (zoom <= 0) + return Point2I(0, 0); + + return Point2I(mClamp((globalPoint.x - artRect.point.x) / zoom, 0, imageExtent.x - 1), + mClamp((globalPoint.y - artRect.point.y) / zoom, 0, imageExtent.y - 1)); +} + +void GuiEditorCursorCtrl::setHotSpotPixel(const Point2I& pixel) +{ + if (mCursor == NULL) + return; + + const Point2I imageExtent = getImageExtent(); + if (imageExtent.x <= 0 || imageExtent.y <= 0) + return; + + const Point2I hotSpot = getHotSpotForPixel(pixel, mCursor->getRenderOffset(), imageExtent); + if (hotSpot == mCursor->getHotSpot()) + return; + + mCursor->setHotSpot(hotSpot); + + // The pane writes the field and marks the theme dirty; this only reports + // that the value moved, so the control never has to know about themes. + Con::executef(this, 3, "onHotSpotChanged", Con::getIntArg(hotSpot.x), Con::getIntArg(hotSpot.y)); +} + +//----------------------------------------------------------------------------- +// Rendering. +//----------------------------------------------------------------------------- + +void GuiEditorCursorCtrl::onRender(Point2I offset, const RectI& updateRect) +{ + RectI ctrlRect = applyMargins(offset, mBounds.extent, NormalState, mProfile); + renderUniversalRect(ctrlRect, mProfile, NormalState); + + RectI artRect; + Point2I imageExtent; + if (getArtRect(artRect, imageExtent)) + { + renderChecker(artRect); + renderArt(artRect, imageExtent); + renderGrid(artRect, imageExtent); + renderMarks(artRect, imageExtent); + } + + Point2I contentOffset = offset; + Point2I contentExtent = mBounds.extent; + renderChildControls(offset, getInnerRect(contentOffset, contentExtent, NormalState, mProfile), updateRect); +} + +void GuiEditorCursorCtrl::renderChecker(const RectI& artRect) +{ + // Squares in screen space rather than image space, so the backdrop stays the + // same size as the zoom changes and never reads as part of the art. + const S32 square = 8; + + dglDrawRectFill(artRect, mCheckerDark); + + for (S32 y = 0; y < artRect.extent.y; y += square) + { + for (S32 x = ((y / square) % 2) * square; x < artRect.extent.x; x += square * 2) + { + RectI cell(artRect.point.x + x, artRect.point.y + y, + getMin(square, artRect.extent.x - x), getMin(square, artRect.extent.y - y)); + dglDrawRectFill(cell, mCheckerLight); + } + } +} + +void GuiEditorCursorCtrl::renderArt(const RectI& artRect, const Point2I& imageExtent) +{ + const GBitmap* bitmap = resolveBitmap(); + if (bitmap == NULL) + return; + + const S32 zoom = artRect.extent.x / imageExtent.x; + const ColorI& tint = mCursor->mColor; + + for (S32 y = 0; y < imageExtent.y; ++y) + { + for (S32 x = 0; x < imageExtent.x; ++x) + { + ColorI pixel; + if (!bitmap->getColor((U32)x, (U32)y, pixel) || pixel.alpha == 0) + continue; + + // The same multiply the canvas applies through bitmap modulation, so + // what is magnified here is what will be drawn on screen. + pixel.red = (U8)((pixel.red * tint.red) / 255); + pixel.green = (U8)((pixel.green * tint.green) / 255); + pixel.blue = (U8)((pixel.blue * tint.blue) / 255); + pixel.alpha = (U8)((pixel.alpha * tint.alpha) / 255); + + dglDrawRectFill(RectI(artRect.point.x + (x * zoom), artRect.point.y + (y * zoom), zoom, zoom), pixel); + } + } +} + +void GuiEditorCursorCtrl::renderGrid(const RectI& artRect, const Point2I& imageExtent) +{ + const S32 zoom = artRect.extent.x / imageExtent.x; + + // Below this the lines cost more than the pixels they separate. + if (zoom < 4) + return; + + for (S32 x = 1; x < imageExtent.x; ++x) + { + const S32 lineX = artRect.point.x + (x * zoom); + dglDrawLine(lineX, artRect.point.y, lineX, artRect.point.y + artRect.extent.y, mGridColor); + } + + for (S32 y = 1; y < imageExtent.y; ++y) + { + const S32 lineY = artRect.point.y + (y * zoom); + dglDrawLine(artRect.point.x, lineY, artRect.point.x + artRect.extent.x, lineY, mGridColor); + } + + dglDrawRect(artRect, mGridColor); +} + +void GuiEditorCursorCtrl::renderMarks(const RectI& artRect, const Point2I& imageExtent) +{ + const S32 zoom = artRect.extent.x / imageExtent.x; + const Point2F renderOffset = mCursor->getRenderOffset(); + + // The anchor: where renderOffset alone puts the pointer. Drawn first and + // faintly, as crosshairs across the whole art, so the distance between it + // and the dot IS the hotSpot nudge -- which is the one thing a reader needs + // to understand why two fields exist. + const Point2I anchor((S32)(imageExtent.x * renderOffset.x), (S32)(imageExtent.y * renderOffset.y)); + const S32 anchorX = artRect.point.x + (anchor.x * zoom); + const S32 anchorY = artRect.point.y + (anchor.y * zoom); + dglDrawLine(anchorX, artRect.point.y, anchorX, artRect.point.y + artRect.extent.y, mAnchorColor); + dglDrawLine(artRect.point.x, anchorY, artRect.point.x + artRect.extent.x, anchorY, mAnchorColor); + + // The hot spot itself: the pixel that will sit under the mouse, boxed so the + // pixel is identifiable, with a dot at its center for low zooms where the + // box is only a few pixels across. + const Point2I hot = getEffectiveHotSpot(mCursor->getHotSpot(), renderOffset, imageExtent); + const RectI hotPixel(artRect.point.x + (hot.x * zoom), artRect.point.y + (hot.y * zoom), zoom, zoom); + + dglDrawRect(hotPixel, mDotColor); + if (zoom >= 3) + { + const RectI inner(hotPixel.point.x + 1, hotPixel.point.y + 1, hotPixel.extent.x - 2, hotPixel.extent.y - 2); + dglDrawRectFill(inner, mDotColor); + } + else + { + dglDrawRectFill(hotPixel, mDotColor); + } +} + +//----------------------------------------------------------------------------- +// Input. +//----------------------------------------------------------------------------- + +void GuiEditorCursorCtrl::onTouchDown(const GuiEvent& event) +{ + RectI artRect; + Point2I imageExtent; + if (!getArtRect(artRect, imageExtent)) + return; + + // A press outside the art is not a hot spot: it would clamp to an edge pixel + // nobody aimed at. + if (!artRect.pointInRect(event.mousePoint)) + return; + + mDragging = true; + mouseLock(); + setHotSpotPixel(pixelAt(event.mousePoint, artRect, imageExtent)); +} + +void GuiEditorCursorCtrl::onTouchDragged(const GuiEvent& event) +{ + if (!mDragging) + return; + + RectI artRect; + Point2I imageExtent; + if (!getArtRect(artRect, imageExtent)) + return; + + // Clamped rather than ignored: a drag that wanders off the art should pin the + // hot spot to the edge it left by, not stop responding. + setHotSpotPixel(pixelAt(event.mousePoint, artRect, imageExtent)); +} + +void GuiEditorCursorCtrl::onTouchUp(const GuiEvent& event) +{ + if (!mDragging) + return; + + mDragging = false; + mouseUnlock(); +} + +void GuiEditorCursorCtrl::onMouseWheelUp(const GuiEvent& event) +{ + setZoom(mZoom + 1); + Con::executef(this, 2, "onZoomChanged", Con::getIntArg(mZoom)); +} + +void GuiEditorCursorCtrl::onMouseWheelDown(const GuiEvent& event) +{ + setZoom(mZoom - 1); + Con::executef(this, 2, "onZoomChanged", Con::getIntArg(mZoom)); +} diff --git a/engine/source/gui/editor/guiEditorCursorCtrl.h b/engine/source/gui/editor/guiEditorCursorCtrl.h new file mode 100644 index 000000000..239809555 --- /dev/null +++ b/engine/source/gui/editor/guiEditorCursorCtrl.h @@ -0,0 +1,152 @@ +//----------------------------------------------------------------------------- +// 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. +//----------------------------------------------------------------------------- + +#ifndef _GUI_EDITOR_CURSOR_CTRL_H_ +#define _GUI_EDITOR_CURSOR_CTRL_H_ + +#ifndef _GUICONTROL_H_ +#include "gui/guiControl.h" +#endif + +//----------------------------------------------------------------------------- +// The Gui Profile Editor's hot-spot editor: one cursor's art, magnified, with +// its hot spot marked and draggable. +// +// This exists because a hot spot cannot be set by typing numbers. It is a +// single pixel in a 13x17 image, and whether it is the right pixel is a +// question about what the art looks like -- so the only way to answer it is to +// see the two together at a size where individual pixels are visible. +// +// Two fields decide where the pointer lands, and the display keeps them +// distinct because they do different jobs: +// +// renderOffset a FRACTION of the bitmap's own size, so "0.5 0.5" means +// "centered" whatever the art measures. This is what stops a +// 13x17 arrow and a 32x32 sizer from appearing to leap when one +// replaces the other, and it is drawn as the faint anchor mark. +// hotSpot a pixel nudge applied on top of it, drawn as the bright dot. +// +// The pointer ends up at hotSpot + trunc(extent * renderOffset), which is the +// dot; dragging moves the dot and writes hotSpot, leaving the anchor alone. +// +// Art is drawn a pixel at a time rather than as a stretched bitmap: magnifying +// through the texture filter would blur exactly the pixel boundaries the user +// is trying to aim at. Transparent pixels are skipped, which is most of a +// cursor. +// +// Editor-only, like guiEditCtrl and the explorer tree, and never offered in the +// palette (every class whose name begins "GuiEdit" is refused there -- this one +// is excluded by having no entry in the placeable list at all). +//----------------------------------------------------------------------------- + +class GuiCursor; +class GBitmap; + +class GuiEditorCursorCtrl : public GuiControl +{ +private: + typedef GuiControl Parent; + +protected: + GuiCursor* mCursor; ///< The cursor being edited. Nothing is drawn without one. + + // Its own decoded copy of the art, because the pixels are the point. A + // cursor's TextureHandle is a BitmapTexture, and TextureManager deletes the + // CPU-side bitmap once it has uploaded one of those -- so asking the cursor + // for its pixels gets NULL. Reloaded whenever the cursor, or the file it + // names, changes. + GBitmap* mBitmap; + StringTableEntry mBitmapKey; + S32 mZoom; ///< Magnification, 1 to smMaxZoom. + ColorI mDotColor; ///< The hot-spot mark. User-settable because a dark dot vanishes on dark art. + ColorI mAnchorColor; ///< The renderOffset anchor mark. + ColorI mCheckerLight; ///< The two squares of the transparency backdrop. + ColorI mCheckerDark; + ColorI mGridColor; ///< Pixel grid, drawn only when zoomed enough to be useful. + bool mDragging; + + /// The decoded art, loading or reloading it if the cursor now names a + /// different file. NULL when there is no cursor or the file is missing. + const GBitmap* resolveBitmap(); + void releaseBitmap(); + + /// The rect the magnified art occupies, in global coordinates, and the + /// image size it was computed from. Returns false when there is nothing to + /// draw (no cursor, or art that failed to load). + bool getArtRect(RectI& artRect, Point2I& imageExtent); + + /// Which image pixel a global point falls on, clamped into the image. + Point2I pixelAt(const Point2I& globalPoint, const RectI& artRect, const Point2I& imageExtent); + + /// Move the hot spot to an image pixel and tell script. hotSpot absorbs the + /// change; renderOffset is deliberately untouched. + void setHotSpotPixel(const Point2I& pixel); + + void renderChecker(const RectI& artRect); + void renderArt(const RectI& artRect, const Point2I& imageExtent); + void renderGrid(const RectI& artRect, const Point2I& imageExtent); + void renderMarks(const RectI& artRect, const Point2I& imageExtent); + +public: + static const S32 smMaxZoom = 16; + + /// Where the pointer sits within the image: the anchor, floored to a pixel, + /// plus the nudge. Static and taking everything it uses, so the arithmetic + /// that the renderer, the hit test and the script readout all depend on can + /// be tested without a canvas -- rendering a cursor needs a GL context, and + /// a unit test has none. + static Point2I getEffectiveHotSpot(const Point2I& hotSpot, const Point2F& renderOffset, const Point2I& imageExtent); + + /// The inverse: the hotSpot that puts the pointer on a given pixel. + static Point2I getHotSpotForPixel(const Point2I& pixel, const Point2F& renderOffset, const Point2I& imageExtent); + + GuiEditorCursorCtrl(); + ~GuiEditorCursorCtrl(); + static void initPersistFields(); + + void onRender(Point2I offset, const RectI& updateRect); + + void onTouchDown(const GuiEvent& event); + void onTouchDragged(const GuiEvent& event); + void onTouchUp(const GuiEvent& event); + + /// Zoom in and out over the art, which is what a magnifier should do. + void onMouseWheelUp(const GuiEvent& event); + void onMouseWheelDown(const GuiEvent& event); + + inline GuiCursor* getCursor() const { return mCursor; } + void setCursorObject(GuiCursor* cursor); + void setZoom(S32 zoom); + inline S32 getZoom() const { return mZoom; } + + /// The largest magnification the pane can actually show this art at. The + /// zoom is clamped to it, so getZoom() never reports one that is not being + /// drawn -- and the pane's "+" can go grey when there is no more to give. + S32 getMaxZoom(); + + /// The image's real size, or (0,0) when there is no art. + Point2I getImageExtent(); + + DECLARE_CONOBJECT(GuiEditorCursorCtrl); +}; + +#endif //_GUI_EDITOR_CURSOR_CTRL_H_ diff --git a/engine/source/gui/editor/guiEditorCursorCtrl_ScriptBinding.h b/engine/source/gui/editor/guiEditorCursorCtrl_ScriptBinding.h new file mode 100644 index 000000000..6bc20c643 --- /dev/null +++ b/engine/source/gui/editor/guiEditorCursorCtrl_ScriptBinding.h @@ -0,0 +1,87 @@ +//----------------------------------------------------------------------------- +// 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. +//----------------------------------------------------------------------------- + +ConsoleMethodGroupBeginWithDocs(GuiEditorCursorCtrl, GuiControl) + +/*! Gets the size of the cursor art, or "0 0" when there is none to show. + @return The image size as "width height". +*/ +ConsoleMethodWithDocs(GuiEditorCursorCtrl, getImageExtent, ConsoleString, 2, 2, ()) +{ + char* buffer = Con::getReturnBuffer(32); + const Point2I extent = object->getImageExtent(); + dSprintf(buffer, 32, "%d %d", extent.x, extent.y); + return buffer; +} + +/*! Gets the pixel of the art the pointer actually lands on: the renderOffset + anchor floored to a pixel, plus the hotSpot nudge. This is the number a + readout should show, because neither field alone answers "where does it + point". + @return The pixel as "x y", or "0 0" when there is no art. +*/ +ConsoleMethodWithDocs(GuiEditorCursorCtrl, getEffectiveHotSpot, ConsoleString, 2, 2, ()) +{ + char* buffer = Con::getReturnBuffer(32); + + GuiCursor* cursor = object->getCursor(); + const Point2I extent = object->getImageExtent(); + if (cursor == NULL || extent.x <= 0 || extent.y <= 0) + { + dStrcpy(buffer, "0 0"); + return buffer; + } + + const Point2I hotSpot = GuiEditorCursorCtrl::getEffectiveHotSpot(cursor->getHotSpot(), cursor->getRenderOffset(), extent); + dSprintf(buffer, 32, "%d %d", hotSpot.x, hotSpot.y); + return buffer; +} + +/*! Sets the magnification, clamped to 1..16. + @param zoom The magnification factor. + @return No return value. +*/ +ConsoleMethodWithDocs(GuiEditorCursorCtrl, setZoom, ConsoleVoid, 3, 3, (zoom)) +{ + object->setZoom(dAtoi(argv[2])); +} + +/*! Gets the current magnification, which is always the one being drawn: a zoom + too big for the pane is clamped rather than pretended at. + @return The magnification factor. +*/ +ConsoleMethodWithDocs(GuiEditorCursorCtrl, getZoom, ConsoleInt, 2, 2, ()) +{ + return object->getZoom(); +} + +/*! Gets the largest magnification this art fits the pane at. Zooming in stops + here, so a pane can grey out its "+" instead of offering a zoom that would + do nothing. + @return The maximum magnification factor. +*/ +ConsoleMethodWithDocs(GuiEditorCursorCtrl, getMaxZoom, ConsoleInt, 2, 2, ()) +{ + return object->getMaxZoom(); +} + +ConsoleMethodGroupEndWithDocs(GuiEditorCursorCtrl) diff --git a/engine/source/gui/guiProfileTheme.cc b/engine/source/gui/guiProfileTheme.cc index 3de53b52c..6570887f6 100644 --- a/engine/source/gui/guiProfileTheme.cc +++ b/engine/source/gui/guiProfileTheme.cc @@ -932,6 +932,22 @@ static void stampDragAndDropProfile(GuiProfileTheme* theme, GuiControlProfile* p STAMP_FIELD(profile, "fontColor", mFontColor, theme->getColorHighlight()); } +//----------------------------------------------------------------------------- +// Cursor recipe. There is only the one: a pointer is the mouse's equivalent of +// text and wants the same contrast against the background, so every cursor in a +// theme takes the foreground color and the set reads as a set. Anything else is +// a per-cursor override, which costs one click in the editor. +// +// This tints art that is deliberately grayscale - black outline, white body - +// so the body takes the color and the outline stays put. A theme that brings +// its own colored art sets the tint to white and this stops mattering. +//----------------------------------------------------------------------------- + +static void stampCursor(GuiProfileTheme* theme, GuiCursor* cursor) +{ + STAMP_FIELD(cursor, "color", mColor, theme->getColorForeground()); +} + //----------------------------------------------------------------------------- // The engine-defined category tables: the canonical set of profiles a // complete theme provides, one entry per profile slot the stock GuiControls @@ -1003,6 +1019,29 @@ const GuiProfileTheme::BorderCategory GuiProfileTheme::smBorderCategories[] = }; const S32 GuiProfileTheme::smBorderCategoryCount = sizeof(smBorderCategories) / sizeof(smBorderCategories[0]); +// The seven cursors the engine can ask for by name. +// +// The placement values started as the ones AppCore's hand-written cursors had +// always used and are now what came back from actually aiming them in the +// hot-spot editor -- which is the point of having built it. Five of the seven +// moved: the resize cursors want their crosshair centred on the pointer rather +// than a pixel down and right of it, and the two bars want one pixel of lift so +// the gap between their arrowheads straddles the edge being dragged. +// +// A "0" hot spot with a centred anchor is not a missing value: the anchor does +// the placing, and the nudge is only what the anchor cannot express. +const GuiProfileTheme::CursorCategory GuiProfileTheme::smCursorCategories[] = +{ + { "Default", "DefaultCursor", "defaultCursor.png", 1, 1, 0.0f, 0.0f, stampCursor }, + { "Edit", "EditCursor", "ibeam.png", 0, 0, 0.5f, 0.5f, stampCursor }, + { "Move", "MoveCursor", "move.png", 0, 0, 0.5f, 0.5f, stampCursor }, + { "LeftRight", "LeftRightCursor", "leftRight.png", 0, -1, 0.5f, 0.5f, stampCursor }, + { "UpDown", "UpDownCursor", "upDown.png", 0, -1, 0.5f, 0.5f, stampCursor }, + { "NWSE", "NWSECursor", "NWSE.png", 0, 0, 0.5f, 0.5f, stampCursor }, + { "NESW", "NESWCursor", "NESW.png", 0, 0, 0.5f, 0.5f, stampCursor }, +}; +const S32 GuiProfileTheme::smCursorCategoryCount = sizeof(smCursorCategories) / sizeof(smCursorCategories[0]); + //----------------------------------------------------------------------------- GuiProfileTheme::GuiProfileTheme() @@ -1013,6 +1052,7 @@ GuiProfileTheme::GuiProfileTheme() mFontTitle = StringTable->insert("Arial"); mFontCode = StringTable->insert("Courier New"); mFontDirectory = StringTable->EmptyString; + mCursorDirectory = StringTable->EmptyString; mFontSize = 12; // Semantic dark palette. Every profile fill/border is derived from these six @@ -1035,6 +1075,10 @@ GuiProfileTheme::GuiProfileTheme() mDefaultBorders.setSize(smBorderCategoryCount); for (S32 i = 0; i < smBorderCategoryCount; ++i) mDefaultBorders[i] = NULL; + + mDefaultCursors.setSize(smCursorCategoryCount); + for (S32 i = 0; i < smCursorCategoryCount; ++i) + mDefaultCursors[i] = NULL; } void GuiProfileTheme::initPersistFields() @@ -1059,6 +1103,13 @@ void GuiProfileTheme::initPersistFields() endGroup("Colors"); addField("borderSize", TypeS32, Offset(mBorderSize, GuiProfileTheme)); + + // Where this theme's own cursor art lives, relative to the game root. Each + // theme gets its own folder so two themes can carry dramatically different + // cursors without one overwriting the other's files. Filled by whoever + // seeds the art (the Profile Editor, or AppCore for the stock theme); a + // theme that names none simply has cursors with no bitmap yet. + addField("cursorDirectory", TypeString, Offset(mCursorDirectory, GuiProfileTheme)); } bool GuiProfileTheme::onAdd() @@ -1083,6 +1134,19 @@ void GuiProfileTheme::onRemove() while (mExtraBorders.size() > 0) mExtraBorders.last()->deleteObject(); + while (mExtraCursors.size() > 0) + mExtraCursors.last()->deleteObject(); + + for (S32 i = 0; i < smCursorCategoryCount; ++i) + { + if (mDefaultCursors[i] != NULL) + { + GuiCursor* cursor = mDefaultCursors[i]; + mDefaultCursors[i] = NULL; + cursor->deleteObject(); + } + } + for (S32 i = 0; i < smProfileCategoryCount; ++i) { if (mDefaultProfiles[i] != NULL) @@ -1149,6 +1213,21 @@ void GuiProfileTheme::onDeleteNotify(SimObject* object) mDefaultBorders[i] = NULL; } + for (S32 i = 0; i < mExtraCursors.size(); ++i) + { + if (mExtraCursors[i] == object) + { + mExtraCursors.erase(i); + break; + } + } + + for (S32 i = 0; i < smCursorCategoryCount; ++i) + { + if (mDefaultCursors[i] == object) + mDefaultCursors[i] = NULL; + } + Parent::onDeleteNotify(object); } @@ -1192,6 +1271,40 @@ S32 GuiProfileTheme::findBorderCategoryIndex(StringTableEntry categoryName) return -1; } +StringTableEntry GuiProfileTheme::getCursorCategoryName(S32 index) +{ + if (index < 0 || index >= smCursorCategoryCount) + return NULL; + + return StringTable->insert(smCursorCategories[index].name); +} + +S32 GuiProfileTheme::findCursorCategoryIndex(StringTableEntry categoryName) +{ + for (S32 i = 0; i < smCursorCategoryCount; ++i) + { + if (StringTable->insert(smCursorCategories[i].name) == categoryName) + return i; + } + return -1; +} + +const char* GuiProfileTheme::getCursorStockFile(S32 index) +{ + if (index < 0 || index >= smCursorCategoryCount) + return ""; + + return smCursorCategories[index].stockFile; +} + +const char* GuiProfileTheme::getCursorCanonicalName(S32 index) +{ + if (index < 0 || index >= smCursorCategoryCount) + return ""; + + return smCursorCategories[index].suffix; +} + //----------------------------------------------------------------------------- // Members. //----------------------------------------------------------------------------- @@ -1219,6 +1332,12 @@ GuiBorderProfile* GuiProfileTheme::getBorder(StringTableEntry categoryName) cons return (index >= 0) ? mDefaultBorders[index] : NULL; } +GuiCursor* GuiProfileTheme::getCursor(StringTableEntry categoryName) const +{ + const S32 index = findCursorCategoryIndex(categoryName); + return (index >= 0) ? mDefaultCursors[index] : NULL; +} + GuiControlProfile* GuiProfileTheme::createMemberProfile(S32 categoryIndex, const char* objectName) { const ProfileCategory& category = smProfileCategories[categoryIndex]; @@ -1273,6 +1392,63 @@ GuiBorderProfile* GuiProfileTheme::createMemberBorder(S32 categoryIndex) return border; } +GuiCursor* GuiProfileTheme::createMemberCursor(S32 categoryIndex, const char* objectName) +{ + const CursorCategory& category = smCursorCategories[categoryIndex]; + + GuiCursor* cursor = new GuiCursor(); + + char nameBuffer[256]; + if (objectName == NULL && getName() != NULL && *getName() != '\0') + { + dSprintf(nameBuffer, sizeof(nameBuffer), "%s%s", getName(), category.suffix); + objectName = nameBuffer; + } + if (objectName != NULL && *objectName != '\0') + cursor->assignName(objectName); + + if (!cursor->registerObject()) + { + delete cursor; + return NULL; + } + + cursor->mCategory = StringTable->insert(category.name); + + // Placement comes from the table and belongs to the art, so it is set here + // rather than stamped -- a restamp must never move a hot spot the user + // tuned. setTheme comes after, so these writes are not seen as overrides. + cursor->setHotSpot(Point2I(category.hotSpotX, category.hotSpotY)); + cursor->setRenderOffset(Point2F(category.renderOffsetX, category.renderOffsetY)); + + cursor->setTheme(this); + fillCursorArt(cursor, categoryIndex); + deleteNotify(cursor); + + return cursor; +} + +void GuiProfileTheme::fillCursorArt(GuiCursor* cursor, S32 categoryIndex) +{ + if (cursor == NULL || mCursorDirectory == NULL || *mCursorDirectory == '\0') + return; + + // Only ever fills a blank. A cursor pointed at the user's own art keeps it + // through every restamp, which is the whole difference between art and the + // derived fields around it. + const StringTableEntry current = cursor->getBitmapName(); + if (current != NULL && *current != '\0') + return; + + char pathBuffer[1024]; + dSprintf(pathBuffer, sizeof(pathBuffer), "%s/%s", mCursorDirectory, smCursorCategories[categoryIndex].stockFile); + + // Through setDataField so TypeFilename expands it: the directory is stored + // relative to the game root, and the texture manager wants a real path. + // GuiCursor treats bitmapName as art, so this does not mark an override. + cursor->setDataField(StringTable->insert("bitmapName"), NULL, pathBuffer); +} + GuiControlProfile* GuiProfileTheme::createProfile(const char* categoryName, const char* objectName) { const S32 categoryIndex = findCategoryIndex(StringTable->insert(categoryName)); @@ -1360,6 +1536,58 @@ bool GuiProfileTheme::removeBorder(GuiBorderProfile* border) return false; } +// An extra cursor belongs to a category, exactly as an extra profile does: a +// theme with two "Default" cursors is offering a choice between two pointers, +// which is the case the Gui Editor shows a cursor slot for. It starts on the +// category's stock art; the editor gives it a copy of its own to edit. +GuiCursor* GuiProfileTheme::createCursor(const char* categoryName, const char* objectName) +{ + const S32 categoryIndex = findCursorCategoryIndex(StringTable->insert(categoryName)); + if (categoryIndex < 0) + { + Con::warnf("GuiProfileTheme::createCursor() - unknown category '%s'.", categoryName); + return NULL; + } + + // Generate when no name is given. + char nameBuffer[256]; + if ((objectName == NULL || *objectName == '\0') && getName() != NULL && *getName() != '\0') + { + for (S32 n = 2; n < 1000000; ++n) + { + dSprintf(nameBuffer, sizeof(nameBuffer), "%s%s%d", getName(), smCursorCategories[categoryIndex].suffix, n); + if (Sim::findObject(nameBuffer) == NULL) + break; + } + objectName = nameBuffer; + } + + GuiCursor* cursor = createMemberCursor(categoryIndex, objectName); + if (cursor == NULL) + return NULL; + + mExtraCursors.push_back(cursor); + smCursorCategories[categoryIndex].stamp(this, cursor); + + return cursor; +} + +bool GuiProfileTheme::removeCursor(GuiCursor* cursor) +{ + for (S32 i = 0; i < mExtraCursors.size(); ++i) + { + if (mExtraCursors[i] == cursor) + { + // Deletion notifies us back and erases the list entry. + cursor->deleteObject(); + return true; + } + } + + // Default members are never removed: a theme is always complete. + return false; +} + bool GuiProfileTheme::renameTheme(const char* newName) { if (!isProperlyAdded()) @@ -1412,6 +1640,15 @@ bool GuiProfileTheme::renameTheme(const char* newName) renames.push_back(rename); } + for (S32 i = 0; i < smCursorCategoryCount; ++i) + { + if (mDefaultCursors[i] == NULL) + continue; + dSprintf(nameBuffer, sizeof(nameBuffer), "%s%s", newName, smCursorCategories[i].suffix); + PendingRename rename = { mDefaultCursors[i], StringTable->insert(nameBuffer) }; + renames.push_back(rename); + } + // Extras rename only when they follow the ... pattern. for (S32 i = 0; i < mExtraProfiles.size(); ++i) { @@ -1423,6 +1660,16 @@ bool GuiProfileTheme::renameTheme(const char* newName) renames.push_back(rename); } + for (S32 i = 0; i < mExtraCursors.size(); ++i) + { + const char* extraName = mExtraCursors[i]->getName(); + if (!hasOldName || extraName == NULL || dStrncmp(extraName, oldName, oldNameLength) != 0) + continue; + dSprintf(nameBuffer, sizeof(nameBuffer), "%s%s", newName, extraName + oldNameLength); + PendingRename rename = { mExtraCursors[i], StringTable->insert(nameBuffer) }; + renames.push_back(rename); + } + // Collision pre-check: every target must be free or already belong to the // object being renamed to it. for (S32 i = 0; i < renames.size(); ++i) @@ -1524,6 +1771,31 @@ void GuiProfileTheme::restamp() if (categoryIndex >= 0) smProfileCategories[categoryIndex].stamp(this, mExtraProfiles[i]); } + + // Cursors. fillCursorArt runs on every pass rather than only at creation: + // a theme usually learns where its cursor folder is after its members + // already exist (the editor names the folder once the theme has a name), + // and it is also what re-points a member whose art went missing. + for (S32 i = 0; i < smCursorCategoryCount; ++i) + { + if (mDefaultCursors[i] == NULL) + mDefaultCursors[i] = createMemberCursor(i, NULL); + if (mDefaultCursors[i] != NULL) + { + fillCursorArt(mDefaultCursors[i], i); + smCursorCategories[i].stamp(this, mDefaultCursors[i]); + } + } + + for (S32 i = 0; i < mExtraCursors.size(); ++i) + { + const S32 categoryIndex = findCursorCategoryIndex(mExtraCursors[i]->mCategory); + if (categoryIndex >= 0) + { + fillCursorArt(mExtraCursors[i], categoryIndex); + smCursorCategories[categoryIndex].stamp(this, mExtraCursors[i]); + } + } } //----------------------------------------------------------------------------- @@ -1536,7 +1808,7 @@ void GuiProfileTheme::restamp() U32 GuiProfileTheme::getTamlChildCount(void) const { - U32 count = (U32)mExtraProfiles.size() + (U32)mExtraBorders.size(); + U32 count = (U32)mExtraProfiles.size() + (U32)mExtraBorders.size() + (U32)mExtraCursors.size(); for (S32 i = 0; i < smBorderCategoryCount; ++i) { @@ -1544,6 +1816,12 @@ U32 GuiProfileTheme::getTamlChildCount(void) const ++count; } + for (S32 i = 0; i < smCursorCategoryCount; ++i) + { + if (mDefaultCursors[i] != NULL) + ++count; + } + for (S32 i = 0; i < smProfileCategoryCount; ++i) { if (mDefaultProfiles[i] != NULL) @@ -1572,6 +1850,22 @@ SimObject* GuiProfileTheme::getTamlChild(const U32 childIndex) const return mExtraBorders[index]; index -= (U32)mExtraBorders.size(); + // Cursors reference nothing and are referenced by nothing inside the file, + // so their position is free; they sit between the borders and the profiles + // to keep the written order stable and readable. + for (S32 i = 0; i < smCursorCategoryCount; ++i) + { + if (mDefaultCursors[i] == NULL) + continue; + if (index == 0) + return mDefaultCursors[i]; + --index; + } + + if (index < (U32)mExtraCursors.size()) + return mExtraCursors[index]; + index -= (U32)mExtraCursors.size(); + for (S32 i = 0; i < smProfileCategoryCount; ++i) { if (mDefaultProfiles[i] == NULL) @@ -1614,6 +1908,28 @@ void GuiProfileTheme::addTamlChild(SimObject* pSimObject) return; } + GuiCursor* cursor = dynamic_cast(pSimObject); + if (cursor != NULL) + { + const S32 categoryIndex = findCursorCategoryIndex(cursor->mCategory); + if (categoryIndex < 0) + { + Con::warnf("GuiProfileTheme::addTamlChild() - cursor child with unknown category '%s' left unattached.", cursor->mCategory); + return; + } + + // First one in a category is that category's default; the rest are the + // extras the user added. Same rule as profiles. + if (mDefaultCursors[categoryIndex] == NULL) + mDefaultCursors[categoryIndex] = cursor; + else + mExtraCursors.push_back(cursor); + + cursor->setTheme(this, true); + deleteNotify(cursor); + return; + } + GuiControlProfile* profile = dynamic_cast(pSimObject); if (profile != NULL) { diff --git a/engine/source/gui/guiProfileTheme.h b/engine/source/gui/guiProfileTheme.h index ba0171a94..4d90b943e 100644 --- a/engine/source/gui/guiProfileTheme.h +++ b/engine/source/gui/guiProfileTheme.h @@ -94,6 +94,7 @@ struct GuiThemeMembership class GuiControlProfile; class GuiBorderProfile; +class GuiCursor; //----------------------------------------------------------------------------- /// A set of theme-wide values (fonts, palette colors, border size) from which @@ -117,6 +118,7 @@ class GuiProfileTheme : public SimObject, public TamlChildren public: typedef void (*StampProfileFn)(GuiProfileTheme* theme, GuiControlProfile* profile); typedef void (*StampBorderFn)(GuiProfileTheme* theme, GuiBorderProfile* border); + typedef void (*StampCursorFn)(GuiProfileTheme* theme, GuiCursor* cursor); /// One entry of the engine-defined category table: the mCategory value, /// the member-name suffix, and the recipe deriving the member's fields @@ -135,12 +137,35 @@ class GuiProfileTheme : public SimObject, public TamlChildren StampBorderFn stamp; }; + /// One entry of the cursor table. Unlike a profile or a border, a cursor is + /// a bitmap, which no recipe can derive from a palette - so the table also + /// carries the stock art a new member starts from and the placement values + /// that art wants. Those three are written once when the member is created + /// and never stamped again; the recipe derives only the tint. + /// + /// The suffixes deliberately match the canonical cursor names the engine + /// looks up when a control names none ("EditCursor", "LeftRightCursor" and + /// the rest), so installing a theme's cursors under those names is a matter + /// of dropping the theme's own name from the front. + struct CursorCategory + { + const char* name; + const char* suffix; + const char* stockFile; ///< Art seeded into the theme's cursor folder. + S32 hotSpotX; + S32 hotSpotY; + F32 renderOffsetX; ///< A fraction of the bitmap's own size, so it + F32 renderOffsetY; ///< anchors the same way whatever the art measures. + StampCursorFn stamp; + }; + private: // Theme-wide values, the inputs to every category recipe. StringTableEntry mFontBody; ///< Font for body text; the most common font. StringTableEntry mFontTitle; ///< Font for titles and headers. StringTableEntry mFontCode; ///< Monospace font for code and console text. StringTableEntry mFontDirectory; ///< Directory searched for the fonts. + StringTableEntry mCursorDirectory; ///< Directory holding this theme's own cursor art. S32 mFontSize; ///< Base font size; recipes may offset it. ColorI mColorBackground; ///< Deepest background color. ColorI mColorSurface; ///< Raised surface/control background color. @@ -156,16 +181,27 @@ class GuiProfileTheme : public SimObject, public TamlChildren Vector mExtraProfiles; Vector mDefaultBorders; Vector mExtraBorders; ///< User-authored single-use "custom" borders owned by the theme (not category members). + Vector mDefaultCursors; + Vector mExtraCursors; ///< Extra cursors, each one within a category (the profile pattern, not the border one). static const ProfileCategory smProfileCategories[]; static const BorderCategory smBorderCategories[]; + static const CursorCategory smCursorCategories[]; static const S32 smProfileCategoryCount; static const S32 smBorderCategoryCount; + static const S32 smCursorCategoryCount; bool mTamlReading; ///< Suppresses auto-creation/stamping while Taml populates the theme. GuiControlProfile* createMemberProfile(S32 categoryIndex, const char* objectName); GuiBorderProfile* createMemberBorder(S32 categoryIndex); + GuiCursor* createMemberCursor(S32 categoryIndex, const char* objectName); + + /// Point a cursor at this theme's copy of its category's stock art. Only + /// ever fills an EMPTY bitmapName: art is the user's, and a restamp must + /// never overwrite what they chose. Does nothing until the theme knows a + /// cursor directory, which is why it is retried on every restamp. + void fillCursorArt(GuiCursor* cursor, S32 categoryIndex); public: DECLARE_CONOBJECT(GuiProfileTheme); @@ -192,17 +228,33 @@ class GuiProfileTheme : public SimObject, public TamlChildren static S32 getBorderCategoryCount() { return smBorderCategoryCount; } static StringTableEntry getBorderCategoryName(S32 index); static S32 findBorderCategoryIndex(StringTableEntry categoryName); + static S32 getCursorCategoryCount() { return smCursorCategoryCount; } + static StringTableEntry getCursorCategoryName(S32 index); + static S32 findCursorCategoryIndex(StringTableEntry categoryName); + + /// The stock art file name for a cursor category ("defaultCursor.png"). The + /// editor asks so it can seed a theme's cursor folder; the theme itself + /// never copies files. + static const char* getCursorStockFile(S32 index); + + /// The name the engine falls back to for a cursor category ("EditCursor"). + /// Also the member-name suffix, which is why a member is findable both ways. + static const char* getCursorCanonicalName(S32 index); // Members. S32 getProfileCount() const; inline const Vector& getExtraProfiles() const { return mExtraProfiles; } inline const Vector& getExtraBorders() const { return mExtraBorders; } + inline const Vector& getExtraCursors() const { return mExtraCursors; } GuiControlProfile* getProfile(StringTableEntry categoryName) const; ///< The category's default member. GuiBorderProfile* getBorder(StringTableEntry categoryName) const; ///< The border category's default member. + GuiCursor* getCursor(StringTableEntry categoryName) const; ///< The cursor category's default member. GuiControlProfile* createProfile(const char* categoryName, const char* objectName); ///< Create an extra profile in a category. bool removeProfile(GuiControlProfile* profile); ///< Delete an extra; defaults are refused. GuiBorderProfile* createBorder(const char* objectName); ///< Create a single-use custom border owned by the theme. bool removeBorder(GuiBorderProfile* border); ///< Delete a custom border. + GuiCursor* createCursor(const char* categoryName, const char* objectName); ///< Create an extra cursor in a category. + bool removeCursor(GuiCursor* cursor); ///< Delete an extra; defaults are refused. /// Create any missing default members and re-derive every non-overridden /// field of every member from the current theme values. @@ -220,6 +272,7 @@ class GuiProfileTheme : public SimObject, public TamlChildren inline StringTableEntry getFontTitle() const { return mFontTitle; } inline StringTableEntry getFontCode() const { return mFontCode; } inline StringTableEntry getFontDirectory() const { return mFontDirectory; } + inline StringTableEntry getCursorDirectory() const { return mCursorDirectory; } inline S32 getFontSize() const { return mFontSize; } inline const ColorI& getColorBackground() const { return mColorBackground; } inline const ColorI& getColorSurface() const { return mColorSurface; } diff --git a/engine/source/gui/guiProfileTheme_ScriptBinding.h b/engine/source/gui/guiProfileTheme_ScriptBinding.h index 784cc2144..107abb872 100644 --- a/engine/source/gui/guiProfileTheme_ScriptBinding.h +++ b/engine/source/gui/guiProfileTheme_ScriptBinding.h @@ -164,8 +164,116 @@ ConsoleMethodWithDocs(GuiProfileTheme, removeBorder, ConsoleBool, 3, 3, (border) return object->removeBorder(border); } +/*! Gets the default member cursor for a cursor category. + @param category The cursor category name (see getCursorCategoryNames). + @return The GuiCursor id, or 0 if the category is unknown. +*/ +ConsoleMethodWithDocs(GuiProfileTheme, getCursor, ConsoleInt, 3, 3, (category)) +{ + GuiCursor* cursor = object->getCursor(StringTable->insert(argv[2])); + return cursor != NULL ? cursor->getId() : 0; +} + +/*! Gets all member cursors for a category: the default first, then extras. + This is how a game picks between several cursors a theme offers for the same + job - Canvas.setCursor(getWord(%theme.getCursors("Default"), 1)). + @param category The cursor category name (see getCursorCategoryNames). + @return A space-separated list of GuiCursor ids. +*/ +ConsoleMethodWithDocs(GuiProfileTheme, getCursors, ConsoleString, 3, 3, (category)) +{ + StringTableEntry category = StringTable->insert(argv[2]); + + char* buffer = Con::getReturnBuffer(1024); + S32 offset = 0; + buffer[0] = '\0'; + + GuiCursor* cursor = object->getCursor(category); + if (cursor != NULL) + offset += dSprintf(buffer + offset, 1024 - offset, "%d", cursor->getId()); + + const Vector& extras = object->getExtraCursors(); + for (S32 i = 0; i < extras.size(); ++i) + { + if (extras[i]->mCategory != category) + continue; + offset += dSprintf(buffer + offset, 1024 - offset, "%s%d", offset > 0 ? " " : "", extras[i]->getId()); + } + + return buffer; +} + +/*! Gets the engine-defined cursor category names. The suffixes these produce + are also the canonical cursor names the engine falls back to when a control + names no cursor of its own. + @return A space-separated list of cursor category names. +*/ +ConsoleMethodWithDocs(GuiProfileTheme, getCursorCategoryNames, ConsoleString, 2, 2, ()) +{ + char* buffer = Con::getReturnBuffer(1024); + S32 offset = 0; + buffer[0] = '\0'; + + for (S32 i = 0; i < GuiProfileTheme::getCursorCategoryCount(); ++i) + offset += dSprintf(buffer + offset, 1024 - offset, "%s%s", i > 0 ? " " : "", GuiProfileTheme::getCursorCategoryName(i)); + + return buffer; +} + +/*! Gets the stock art file name a cursor category starts from + ("defaultCursor.png"). The editor asks so it can seed a theme's own cursor + folder; a theme never copies files itself. + @param category The cursor category name (see getCursorCategoryNames). + @return The stock file name, or "" if the category is unknown. +*/ +ConsoleMethodWithDocs(GuiProfileTheme, getCursorStockFile, ConsoleString, 3, 3, (category)) +{ + return GuiProfileTheme::getCursorStockFile(GuiProfileTheme::findCursorCategoryIndex(StringTable->insert(argv[2]))); +} + +/*! Gets the name the engine falls back to for a cursor category + ("EditCursor"). Installing a theme's cursors means registering copies of + them under these names; see AppCore::installThemeCursors. + @param category The cursor category name (see getCursorCategoryNames). + @return The canonical cursor name, or "" if the category is unknown. +*/ +ConsoleMethodWithDocs(GuiProfileTheme, getCursorCanonicalName, ConsoleString, 3, 3, (category)) +{ + return GuiProfileTheme::getCursorCanonicalName(GuiProfileTheme::findCursorCategoryIndex(StringTable->insert(argv[2]))); +} + +/*! Creates an extra member cursor in a category, alongside the default. A + category holding more than one cursor is what makes the Gui Editor offer a + choice on a control's cursor slot. + @param category The cursor category name (see getCursorCategoryNames). + @param name Optional object name; defaults to . + @return The new GuiCursor id, or 0 on failure. +*/ +ConsoleMethodWithDocs(GuiProfileTheme, createCursor, ConsoleInt, 3, 4, (category, [name])) +{ + GuiCursor* cursor = object->createCursor(argv[2], argc > 3 ? argv[3] : NULL); + return cursor != NULL ? cursor->getId() : 0; +} + +/*! Removes an extra member cursor. Default members are refused: a theme always + provides one cursor per category. + @param cursor The extra cursor to remove. + @return True if the cursor was an extra and was removed. +*/ +ConsoleMethodWithDocs(GuiProfileTheme, removeCursor, ConsoleBool, 3, 3, (cursor)) +{ + GuiCursor* cursor = dynamic_cast(Sim::findObject(argv[2])); + if (cursor == NULL) + { + Con::warnf("GuiProfileTheme::removeCursor() - could not find cursor '%s'.", argv[2]); + return false; + } + + return object->removeCursor(cursor); +} + /*! Clears a member's override on one field, so the field re-derives from the - theme. Accepts a member profile or border profile. + theme. Accepts a member profile, border profile or cursor. @param member The member profile or border profile. @param fieldName The field to reset. @return No return value. @@ -191,6 +299,14 @@ ConsoleMethodWithDocs(GuiProfileTheme, resetField, ConsoleVoid, 4, 4, (member, f return; } + GuiCursor* cursor = dynamic_cast(target); + if (cursor != NULL && cursor->getTheme() == object) + { + cursor->clearThemeFieldOverride(field); + object->restamp(); + return; + } + Con::warnf("GuiProfileTheme::resetField() - '%s' is not a member of this theme.", argv[2]); } @@ -219,6 +335,14 @@ ConsoleMethodWithDocs(GuiProfileTheme, resetProfile, ConsoleVoid, 3, 3, (member) return; } + GuiCursor* cursor = dynamic_cast(target); + if (cursor != NULL && cursor->getTheme() == object) + { + cursor->clearAllThemeOverrides(); + object->restamp(); + return; + } + Con::warnf("GuiProfileTheme::resetProfile() - '%s' is not a member of this theme.", argv[2]); } @@ -240,6 +364,10 @@ ConsoleMethodWithDocs(GuiProfileTheme, isFieldOverridden, ConsoleBool, 4, 4, (me if (border != NULL) return border->isThemeFieldOverridden(field); + GuiCursor* cursor = dynamic_cast(target); + if (cursor != NULL) + return cursor->isThemeFieldOverridden(field); + return false; } diff --git a/engine/source/gui/guiTypes.cc b/engine/source/gui/guiTypes.cc index 1e461e340..b315e7d62 100755 --- a/engine/source/gui/guiTypes.cc +++ b/engine/source/gui/guiTypes.cc @@ -33,6 +33,21 @@ #include "graphics/TextureManager.h" // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- // +//------------------------------------------------------------------------------ +// The theme-managed field names, shared by the membership glue on GuiCursor, +// GuiBorderProfile and GuiControlProfile. +static StringTableEntry themeCategoryField() +{ + static StringTableEntry categoryField = StringTable->insert("category"); + return categoryField; +} + +static StringTableEntry themeOverridesField() +{ + static StringTableEntry overridesField = StringTable->insert("themeOverrides"); + return overridesField; +} + IMPLEMENT_CONOBJECT(GuiCursor); GuiCursor::GuiCursor() @@ -40,6 +55,12 @@ GuiCursor::GuiCursor() mHotSpot.set(0,0); mRenderOffset.set(0.0f,0.0f); mExtent.set(1,1); + mBitmapName = StringTable->EmptyString; + + // White is the identity for bitmap modulation, so an untinted cursor draws + // exactly as it did before this field existed. + mColor.set(255, 255, 255, 255); + mCategory = StringTable->EmptyString; } GuiCursor::~GuiCursor() @@ -49,9 +70,22 @@ GuiCursor::~GuiCursor() void GuiCursor::initPersistFields() { Parent::initPersistFields(); + + // hotSpot and renderOffset both shift where the art lands, and they are not + // redundant. hotSpot is a pixel nudge; renderOffset is a fraction of the + // bitmap's own size, so "0.5 0.5" means "centered on the pointer" whatever + // the art measures. That is what stops a 13x17 arrow and a 32x32 sizer from + // appearing to leap when one replaces the other. The pointer ends up at + // hotSpot + (extent * renderOffset) within the image; see render(). addField("hotSpot", TypePoint2I, Offset(mHotSpot, GuiCursor)); addField("renderOffset",TypePoint2F, Offset(mRenderOffset, GuiCursor)); - addField("bitmapName", TypeFilename, Offset(mBitmapName, GuiCursor)); + // Written relative to the game root; see getRelativeBitmapName. + addProtectedField("bitmapName", TypeFilename, Offset(mBitmapName, GuiCursor), &defaultProtectedSetFn, &getBitmapName, "Bitmap drawn for this cursor"); + addField("color", TypeColorI, Offset(mColor, GuiCursor)); + + addField("category", TypeString, Offset(mCategory, GuiCursor)); + // The offset is unused: both accessors are supplied and the setter returns false. + addProtectedField("themeOverrides", TypeString, Offset(mCategory, GuiCursor), &setThemeOverrides, &getThemeOverrides, "Theme-overridden field names"); } bool GuiCursor::onAdd() @@ -69,16 +103,24 @@ void GuiCursor::onRemove() Parent::onRemove(); } -void GuiCursor::render(const Point2I &pos) +const Point2I& GuiCursor::resolve() { if (!mTextureHandle && mBitmapName && mBitmapName[0]) { mTextureHandle = TextureHandle(mBitmapName, TextureHandle::BitmapTexture); - if(!mTextureHandle) - return; - mExtent.set(mTextureHandle.getWidth(), mTextureHandle.getHeight()); + if (mTextureHandle) + mExtent.set(mTextureHandle.getWidth(), mTextureHandle.getHeight()); } + return mExtent; +} + +void GuiCursor::render(const Point2I &pos) +{ + resolve(); + if(!mTextureHandle) + return; + // Render the cursor centered according to dimensions of texture S32 texWidth = mTextureHandle.getWidth(); S32 texHeight = mTextureHandle.getHeight(); @@ -86,9 +128,115 @@ void GuiCursor::render(const Point2I &pos) Point2I renderPos = pos; renderPos.x -= (S32)( texWidth * mRenderOffset.x ); renderPos.y -= (S32)( texHeight * mRenderOffset.y ); - - dglClearBitmapModulation(); + + // The stock cursors are grayscale - black outline, white body - so this + // colors the body and leaves the outline. mColor starts white, which is what + // dglClearBitmapModulation sets, so an untinted cursor is unaffected. + dglSetBitmapModulation(mColor); dglDrawBitmap(mTextureHandle, renderPos); + dglClearBitmapModulation(); +} + +StringTableEntry GuiCursor::getRelativeBitmapName( void ) const +{ + if (mBitmapName == NULL || mBitmapName == StringTable->EmptyString) + return mBitmapName; + + // Only a path inside the game is made relative. Art somewhere else - another + // drive, a folder beside the repository - is left alone: a "../.." chain + // climbing out of the game folder is no more portable than the absolute path + // it came from. Same rule as GuiControlProfile's bitmap. + StringTableEntry gameRoot = Platform::getMainDotCsDir(); + if (!Con::isBasePath(mBitmapName, gameRoot)) + return mBitmapName; + + return Platform::makeRelativePathName(mBitmapName, gameRoot); +} + +void GuiCursor::setTheme(GuiProfileTheme* theme, bool preserveOverrides) +{ + if (mThemeMembership.mTheme == theme) + return; + + if (mThemeMembership.mTheme != NULL) + clearNotify(mThemeMembership.mTheme); + + mThemeMembership.mTheme = theme; + if (!preserveOverrides) + mThemeMembership.clearAll(); + + if (theme != NULL) + deleteNotify(theme); +} + +// A cursor's art - which bitmap, and where the pointer sits within it - is the +// one thing a theme cannot derive from a palette, so a theme sets these once +// when it creates the member and never stamps them again. That makes them +// unlike every other member field: there is no theme value behind them to +// override or to reset to, and they must persist whether or not anything marked +// them. Only "color" is stamped, and only it takes part in override tracking. +static bool isCursorArtField(StringTableEntry field) +{ + static StringTableEntry bitmapField = StringTable->insert("bitmapName"); + static StringTableEntry hotSpotField = StringTable->insert("hotSpot"); + static StringTableEntry renderOffsetField = StringTable->insert("renderOffset"); + + return field == bitmapField || field == hotSpotField || field == renderOffsetField; +} + +void GuiCursor::onStaticModified(const char* slotName, const char* newValue) +{ + Parent::onStaticModified(slotName, newValue); + + StringTableEntry slot = StringTable->insert(slotName); + + // The texture is loaded once and cached, so pointing a live cursor at new + // art has to drop it - otherwise the old bitmap draws forever and the + // extent, which the hot spot is measured against, stays wrong. + static StringTableEntry bitmapField = StringTable->insert("bitmapName"); + if (slot == bitmapField) + { + mTextureHandle = TextureHandle(); + mExtent.set(1, 1); + } + + // On a themed cursor, an external write to a stamped field becomes an + // override that stamping will preserve. Category, the override list and the + // art fields are not stamped, so they are not overridable. + if (mThemeMembership.mTheme != NULL) + { + if (slot != themeCategoryField() && slot != themeOverridesField() && !isCursorArtField(slot)) + mThemeMembership.markOverride(slot); + } +} + +bool GuiCursor::writeField(StringTableEntry fieldname, const char* value) +{ + if (!Parent::writeField(fieldname, value)) + return false; + + // Themed cursors persist their art, their category and the override list + // unconditionally; everything else is derived from the theme and persists + // only when explicitly overridden. + if (mThemeMembership.mTheme != NULL) + { + if (fieldname != themeCategoryField() && + fieldname != themeOverridesField() && + !isCursorArtField(fieldname) && + findField(fieldname) != NULL && + !mThemeMembership.isOverridden(fieldname)) + return false; + } + + return true; +} + +void GuiCursor::onDeleteNotify(SimObject* object) +{ + if (object == (SimObject*)mThemeMembership.mTheme) + mThemeMembership.mTheme = NULL; + + Parent::onDeleteNotify(object); } // Setup the type, this will keep Border profiles from being listed with normal profiles. @@ -131,21 +279,6 @@ ConsoleGetType(TypeGuiCursor) return returnBuffer; } -//------------------------------------------------------------------------------ -// The theme-managed field names, shared by the membership glue on -// GuiBorderProfile and GuiControlProfile. -static StringTableEntry themeCategoryField() -{ - static StringTableEntry categoryField = StringTable->insert("category"); - return categoryField; -} - -static StringTableEntry themeOverridesField() -{ - static StringTableEntry overridesField = StringTable->insert("themeOverrides"); - return overridesField; -} - IMPLEMENT_CONOBJECT(GuiBorderProfile); GuiBorderProfile::GuiBorderProfile() diff --git a/engine/source/gui/guiTypes.h b/engine/source/gui/guiTypes.h index bb804f0a7..2540f991b 100755 --- a/engine/source/gui/guiTypes.h +++ b/engine/source/gui/guiTypes.h @@ -129,6 +129,11 @@ enum VertAlignmentType DefaultVAlign }; +/// The pointer the canvas draws. A control names one through a TypeGuiCursor +/// field (a text edit's editCursor, a window's resize cursors); anything that +/// names none falls back to the canonical name for its kind - "EditCursor", +/// "LeftRightCursor" and the rest - which is what a GuiProfileTheme's cursor +/// members are installed under. See GuiProfileTheme::smCursorCategories. class GuiCursor : public SimObject { private: @@ -140,9 +145,28 @@ class GuiCursor : public SimObject Point2I mExtent; TextureHandle mTextureHandle; + GuiThemeMembership mThemeMembership; ///< Theme membership and per-field override tracking. + public: + /// Multiplied into the bitmap as it is drawn. The stock art is grayscale - + /// black outline, white body - so a tint colors the body and leaves the + /// outline alone, which is what lets a theme skin the stock cursors without + /// anyone drawing new ones. White is the identity (dglClearBitmapModulation + /// is exactly white), so a cursor that never sets this renders as it always did. + ColorI mColor; + + StringTableEntry mCategory; ///< The theme category this cursor belongs to. See GuiProfileTheme. + Point2I getHotSpot() { return mHotSpot; } Point2I getExtent() { return mExtent; } + Point2F getRenderOffset() { return mRenderOffset; } + + // Used by GuiProfileTheme when it creates a member from the cursor table, + // and by the hot-spot editor. Neither is derived from the theme's values, so + // neither goes through the stamping path. + void setHotSpot(const Point2I& hotSpot) { mHotSpot = hotSpot; } + void setRenderOffset(const Point2F& renderOffset) { mRenderOffset = renderOffset; } + inline StringTableEntry getBitmapName() const { return mBitmapName; } DECLARE_CONOBJECT(GuiCursor); GuiCursor(void); @@ -152,6 +176,39 @@ class GuiCursor : public SimObject bool onAdd(void); void onRemove(); void render(const Point2I &pos); + + /// Load the bitmap now and answer its real size. render() does this on its + /// first pass, so until a cursor has been drawn once its extent is (1,1) - + /// no use to an editor that has to lay out and measure before drawing. + const Point2I& resolve(); + + /// The bitmap path as it should be written down: relative to the game root + /// when it points inside the game, and unchanged when it does not. As with + /// GuiControlProfile's bitmap, mBitmapName itself is always absolute - + /// TypeFilename expands whatever it is given the moment it is set - and an + /// absolute path in a saved theme names a folder on one machine only. + StringTableEntry getRelativeBitmapName( void ) const; + + // Theme membership. A cursor stamped by a GuiProfileTheme tracks which + // fields were explicitly overridden; standalone cursors are unaffected. + // preserveOverrides keeps an override set loaded before attachment (Taml). + void setTheme(GuiProfileTheme* theme, bool preserveOverrides = false); + inline GuiProfileTheme* getTheme() const { return mThemeMembership.mTheme; } + bool isThemeFieldOverridden(StringTableEntry field) const { return mThemeMembership.isOverridden(field); } + void clearThemeFieldOverride(StringTableEntry field) { mThemeMembership.clearOverride(field); } + void clearAllThemeOverrides() { mThemeMembership.clearAll(); } + + virtual void onStaticModified(const char* slotName, const char* newValue = NULL); + virtual bool writeField(StringTableEntry fieldname, const char* value); + virtual void onDeleteNotify(SimObject* object); + +protected: + static bool setThemeOverrides(void* obj, const char* data) { static_cast(obj)->mThemeMembership.parseOverrideList(data); return false; } + static const char* getThemeOverrides(void* obj, const char* data) { return static_cast(obj)->mThemeMembership.formatOverrideList(); } + + // Set is left to TypeFilename, which expands the path; only the read-back is + // ours, so that what gets written stays portable. + static const char* getBitmapName(void* obj, const char* data) { return static_cast(obj)->getRelativeBitmapName(); } }; DefineConsoleType(TypeGuiCursor) diff --git a/engine/source/testing/tests/guiCursorHotSpotTests.cc b/engine/source/testing/tests/guiCursorHotSpotTests.cc new file mode 100644 index 000000000..959632eda --- /dev/null +++ b/engine/source/testing/tests/guiCursorHotSpotTests.cc @@ -0,0 +1,163 @@ +//----------------------------------------------------------------------------- +// 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. +//----------------------------------------------------------------------------- + +// We don't want tests in a shipping version. +#ifndef TORQUE_SHIPPING + +#ifndef _UNIT_TESTING_H_ +#include "testing/unitTesting.h" +#endif + +#ifndef _GUI_EDITOR_CURSOR_CTRL_H_ +#include "gui/editor/guiEditorCursorCtrl.h" +#endif + +//----------------------------------------------------------------------------- +// Where a cursor actually points. +// +// Two fields decide it, and the split is not redundant: guiCanvas.cc positions +// the art at cursorPos - hotSpot, then GuiCursor::render subtracts +// (S32)(extent * renderOffset). So the pixel under the mouse is +// hotSpot + trunc(extent * renderOffset). +// +// renderOffset is a FRACTION of the art's own size, which is what makes it +// size-independent: "0.5 0.5" is the middle of a 13x17 pointer and the middle +// of a 32x32 sizer alike, so swapping one for the other does not make the shape +// appear to jump. hotSpot is the pixel nudge on top of that. +// +// The hot-spot editor draws both and drags the second, so this arithmetic is +// what its dot, its hit test and its readout all agree on. It lives in statics +// precisely so it can be checked here: rendering a cursor needs a GL context +// and a texture, and a unit test has neither. +//----------------------------------------------------------------------------- + +TEST( GuiCursorHotSpotTests, HotSpotAloneIsTheAnswerWithNoRenderOffset ) +{ + // The stock Default cursor: 13x17 art, hot spot one pixel in, no offset. + const Point2I hot = GuiEditorCursorCtrl::getEffectiveHotSpot( + Point2I( 1, 1 ), Point2F( 0.0f, 0.0f ), Point2I( 13, 17 ) ); + + ASSERT_EQ( hot.x, 1 ); + ASSERT_EQ( hot.y, 1 ); + + SUCCEED(); +} + +TEST( GuiCursorHotSpotTests, RenderOffsetIsAFractionOfTheArtSize ) +{ + // The stock Move cursor: 32x32 art centred on the pointer by the anchor + // alone, so the pointer lands in the middle and the nudge is zero. + const Point2I move = GuiEditorCursorCtrl::getEffectiveHotSpot( + Point2I( 0, 0 ), Point2F( 0.5f, 0.5f ), Point2I( 32, 32 ) ); + ASSERT_EQ( move.x, 16 ); + ASSERT_EQ( move.y, 16 ); + + // The stock Edit cursor: 8x20 art, centered, no nudge. + const Point2I edit = GuiEditorCursorCtrl::getEffectiveHotSpot( + Point2I( 0, 0 ), Point2F( 0.5f, 0.5f ), Point2I( 8, 20 ) ); + ASSERT_EQ( edit.x, 4 ); + ASSERT_EQ( edit.y, 10 ); + + SUCCEED(); +} + +// The point of a fractional offset: the same value centers art of any size, so +// a pointer and a sizer of different dimensions still sit under the same spot. +TEST( GuiCursorHotSpotTests, TheSameRenderOffsetCentersArtOfAnySize ) +{ + const Point2F centered( 0.5f, 0.5f ); + + const Point2I small = GuiEditorCursorCtrl::getEffectiveHotSpot( Point2I( 0, 0 ), centered, Point2I( 16, 16 ) ); + const Point2I large = GuiEditorCursorCtrl::getEffectiveHotSpot( Point2I( 0, 0 ), centered, Point2I( 32, 32 ) ); + + ASSERT_EQ( small.x, 8 ); + ASSERT_EQ( large.x, 16 ); + ASSERT_EQ( small.x * 2, large.x ) << "A fraction must scale with the art, not sit at a fixed pixel."; + + SUCCEED(); +} + +// The engine truncates rather than rounds, and the editor must mark the pixel +// that will really be under the mouse - not the one that ought to be. +TEST( GuiCursorHotSpotTests, TheFractionTruncatesJustAsTheEngineDoes ) +{ + // 32x16 art at an anchor of 0.5, 0.4: 16 * 0.4 is 6.4, and (S32)6.4 is 6 -- + // guiTypes.cc casts, it does not round. Any anchor other than a half or a + // whole can land between pixels, so the editor has to reproduce the cast + // rather than round, or its dot would sit a pixel off what the canvas draws. + const Point2I hot = GuiEditorCursorCtrl::getEffectiveHotSpot( + Point2I( 0, 0 ), Point2F( 0.5f, 0.4f ), Point2I( 32, 16 ) ); + + ASSERT_EQ( hot.x, 16 ); + ASSERT_EQ( hot.y, 6 ) << "6.4 truncates to 6; rounding here would put the dot a pixel off."; + + // An odd extent halved does the same: 17 * 0.5 is 8.5 -> 8. + const Point2I odd = GuiEditorCursorCtrl::getEffectiveHotSpot( + Point2I( 0, 0 ), Point2F( 0.5f, 0.5f ), Point2I( 13, 17 ) ); + + ASSERT_EQ( odd.x, 6 ); + ASSERT_EQ( odd.y, 8 ); + + SUCCEED(); +} + +// Dragging the dot asks the inverse question: which hotSpot puts the pointer on +// the pixel I clicked? It has to invert exactly, or a drag would drift. +TEST( GuiCursorHotSpotTests, HotSpotForPixelInvertsEffectiveHotSpot ) +{ + const Point2F renderOffset( 0.5f, 0.4f ); + const Point2I imageExtent( 32, 16 ); + + for ( S32 x = 0; x < imageExtent.x; ++x ) + { + for ( S32 y = 0; y < imageExtent.y; ++y ) + { + const Point2I pixel( x, y ); + const Point2I hotSpot = GuiEditorCursorCtrl::getHotSpotForPixel( pixel, renderOffset, imageExtent ); + const Point2I roundTrip = GuiEditorCursorCtrl::getEffectiveHotSpot( hotSpot, renderOffset, imageExtent ); + + ASSERT_EQ( roundTrip.x, pixel.x ); + ASSERT_EQ( roundTrip.y, pixel.y ); + } + } + + SUCCEED(); +} + +// A drag writes hotSpot and leaves renderOffset alone, so aiming at the same +// pixel under a different anchor must produce a different nudge. This is what +// keeps the two fields meaningfully separate rather than one number in disguise. +TEST( GuiCursorHotSpotTests, TheNudgeAbsorbsTheAnchorChoice ) +{ + const Point2I imageExtent( 16, 16 ); + const Point2I target( 4, 4 ); + + const Point2I fromCorner = GuiEditorCursorCtrl::getHotSpotForPixel( target, Point2F( 0.0f, 0.0f ), imageExtent ); + const Point2I fromCenter = GuiEditorCursorCtrl::getHotSpotForPixel( target, Point2F( 0.5f, 0.5f ), imageExtent ); + + ASSERT_EQ( fromCorner.x, 4 ) << "With no anchor the nudge is the pixel itself."; + ASSERT_EQ( fromCenter.x, -4 ) << "Anchored at the middle, reaching pixel 4 means nudging back 4."; + + SUCCEED(); +} + +#endif // TORQUE_SHIPPING diff --git a/engine/source/testing/tests/guiProfileThemeTests.cc b/engine/source/testing/tests/guiProfileThemeTests.cc index 091a84ff5..92de42958 100644 --- a/engine/source/testing/tests/guiProfileThemeTests.cc +++ b/engine/source/testing/tests/guiProfileThemeTests.cc @@ -1237,4 +1237,249 @@ TEST( GuiProfileThemeTests, FreshlyStampedProfileResolvesFallbackSidesToDefaultB SUCCEED(); } +//----------------------------------------------------------------------------- +// Cursors: the third member family. They follow the profile pattern - a +// category table, one guaranteed default per category, extras within a +// category - with one difference that drives most of these tests: a cursor's +// art cannot be derived from a palette, so bitmapName, hotSpot and renderOffset +// are set once at creation and never stamped, while only the tint is. +// +// Nothing here may draw or resolve a cursor: a unit test has no GL context, and +// loading a texture trips a modal assert that arrives as a hang. +//----------------------------------------------------------------------------- + +TEST( GuiProfileThemeTests, ThemeProvidesOneCursorPerCategory ) +{ + GuiProfileTheme* theme = new GuiProfileTheme(); + theme->registerObject( "UnitTestTheme" ); + + ASSERT_EQ( GuiProfileTheme::getCursorCategoryCount(), 7 ); + + for ( S32 i = 0; i < GuiProfileTheme::getCursorCategoryCount(); ++i ) + { + StringTableEntry category = GuiProfileTheme::getCursorCategoryName( i ); + GuiCursor* cursor = theme->getCursor( category ); + + ASSERT_TRUE( cursor != NULL ) << "Every cursor category must have a default member."; + ASSERT_STREQ( cursor->mCategory, category ); + ASSERT_EQ( cursor->getTheme(), theme ); + } + + theme->deleteObject(); + + SUCCEED(); +} + +// The load-bearing invariant behind installing a theme's cursors: a member is +// named , and the suffix IS the canonical name the engine +// falls back to when a control names no cursor (guiTextEditCtrl.cc, +// guiWindowCtrl.cc, guiFrameSetCtrl.cc, guiEditCtrl.cc). Installing is then +// just dropping the theme's name from the front - no lookup table to drift. +TEST( GuiProfileThemeTests, CursorMemberNamesAreThemeNamePlusCanonicalName ) +{ + static const char* const canonicalNames[] = + { + "DefaultCursor", "EditCursor", "MoveCursor", + "LeftRightCursor", "UpDownCursor", "NWSECursor", "NESWCursor" + }; + + GuiProfileTheme* theme = new GuiProfileTheme(); + theme->registerObject( "UnitTestTheme" ); + + for ( S32 i = 0; i < GuiProfileTheme::getCursorCategoryCount(); ++i ) + { + char expected[256]; + dSprintf( expected, sizeof( expected ), "UnitTestTheme%s", canonicalNames[i] ); + + GuiCursor* cursor = theme->getCursor( GuiProfileTheme::getCursorCategoryName( i ) ); + ASSERT_EQ( (SimObject*)cursor, Sim::findObject( expected ) ) + << "Member name must be the theme name followed by the canonical cursor name."; + } + + theme->deleteObject(); + + SUCCEED(); +} + +TEST( GuiProfileThemeTests, CursorColorIsStampedFromForegroundAndOverridable ) +{ + GuiProfileTheme* theme = new GuiProfileTheme(); + theme->registerObject( "UnitTestTheme" ); + + GuiCursor* cursor = theme->getCursor( StringTable->insert( "Default" ) ); + ASSERT_TRUE( cursor != NULL ); + + theme->setDataField( StringTable->insert( "colorForeground" ), NULL, "11 22 33 255" ); + ASSERT_TRUE( cursor->mColor == ColorI( 11, 22, 33, 255 ) ); + + // An explicit tint survives later theme changes... + cursor->setDataField( StringTable->insert( "color" ), NULL, "9 8 7 6" ); + ASSERT_TRUE( cursor->isThemeFieldOverridden( StringTable->insert( "color" ) ) ); + theme->setDataField( StringTable->insert( "colorForeground" ), NULL, "44 55 66 255" ); + ASSERT_TRUE( cursor->mColor == ColorI( 9, 8, 7, 6 ) ); + + // ...and clearing it re-derives. + cursor->clearThemeFieldOverride( StringTable->insert( "color" ) ); + theme->restamp(); + ASSERT_TRUE( cursor->mColor == ColorI( 44, 55, 66, 255 ) ); + + theme->deleteObject(); + + SUCCEED(); +} + +TEST( GuiProfileThemeTests, CursorArtIsNeverStampedAndNeverOverrideTracked ) +{ + GuiProfileTheme* theme = new GuiProfileTheme(); + theme->registerObject( "UnitTestTheme" ); + + GuiCursor* cursor = theme->getCursor( StringTable->insert( "Move" ) ); + ASSERT_TRUE( cursor != NULL ); + + // The table's placement values for Move: centred on the pointer by the + // anchor alone, so the nudge has nothing left to say. + ASSERT_EQ( cursor->getHotSpot().x, 0 ); + ASSERT_EQ( cursor->getHotSpot().y, 0 ); + ASSERT_EQ( cursor->getRenderOffset().x, 0.5f ); + + cursor->setDataField( StringTable->insert( "hotSpot" ), NULL, "4 9" ); + cursor->setDataField( StringTable->insert( "bitmapName" ), NULL, "unitTestArt/pointer.png" ); + + // Art is the user's, not the theme's: it is not an "override" of anything, + // because no recipe would ever write it. + ASSERT_FALSE( cursor->isThemeFieldOverridden( StringTable->insert( "hotSpot" ) ) ); + ASSERT_FALSE( cursor->isThemeFieldOverridden( StringTable->insert( "bitmapName" ) ) ); + + // And a restamp leaves all of it alone. + theme->setDataField( StringTable->insert( "colorForeground" ), NULL, "1 2 3 255" ); + theme->restamp(); + ASSERT_EQ( cursor->getHotSpot().x, 4 ); + ASSERT_EQ( cursor->getHotSpot().y, 9 ); + ASSERT_TRUE( dStrstr( cursor->getBitmapName(), "pointer.png" ) != NULL ); + + theme->deleteObject(); + + SUCCEED(); +} + +// A theme usually learns where its cursor folder is after its members already +// exist, so filling the art is retried on every restamp - but only ever into a +// blank, so it can never overwrite art the user chose. +TEST( GuiProfileThemeTests, CursorArtFillsFromCursorDirectoryButOnlyWhenEmpty ) +{ + GuiProfileTheme* theme = new GuiProfileTheme(); + theme->registerObject( "UnitTestTheme" ); + + GuiCursor* cursor = theme->getCursor( StringTable->insert( "Default" ) ); + ASSERT_TRUE( cursor != NULL ); + ASSERT_STREQ( cursor->getBitmapName(), "" ) << "A theme with no cursor directory has no art to point at."; + + theme->setDataField( StringTable->insert( "cursorDirectory" ), NULL, "unitTestThemes/cursors/UnitTestTheme" ); + ASSERT_TRUE( dStrstr( cursor->getBitmapName(), "defaultCursor.png" ) != NULL ) + << "Naming the directory fills the category's stock art."; + + // Pointing the directory somewhere else does not move a cursor that already + // has art - including art that was filled from the old directory. + theme->setDataField( StringTable->insert( "cursorDirectory" ), NULL, "unitTestThemes/cursors/Other" ); + ASSERT_TRUE( dStrstr( cursor->getBitmapName(), "UnitTestTheme" ) != NULL ) + << "Filled art must survive a directory change; only a blank is filled."; + + theme->deleteObject(); + + SUCCEED(); +} + +TEST( GuiProfileThemeTests, ExtraCursorsShareCategoryAndOnlyExtrasAreRemovable ) +{ + GuiProfileTheme* theme = new GuiProfileTheme(); + theme->registerObject( "UnitTestTheme" ); + + GuiCursor* extra = theme->createCursor( "Default", NULL ); + ASSERT_TRUE( extra != NULL ); + ASSERT_STREQ( extra->getName(), "UnitTestThemeDefaultCursor2" ); + ASSERT_STREQ( extra->mCategory, "Default" ); + ASSERT_EQ( extra->getTheme(), theme ); + + // Both members of the category are offered; the default comes first. + ASSERT_EQ( theme->getExtraCursors().size(), 1 ); + ASSERT_EQ( theme->getExtraCursors()[0], extra ); + + GuiCursor* defaultCursor = theme->getCursor( StringTable->insert( "Default" ) ); + ASSERT_FALSE( theme->removeCursor( defaultCursor ) ) << "Default members must not be removable."; + ASSERT_TRUE( theme->removeCursor( extra ) ); + ASSERT_EQ( theme->getExtraCursors().size(), 0 ); + + theme->deleteObject(); + + SUCCEED(); +} + +TEST( GuiProfileThemeTests, RenameThemeRenamesCursorMembers ) +{ + GuiProfileTheme* theme = new GuiProfileTheme(); + theme->registerObject( "UnitTestThemeA" ); + + GuiCursor* extra = theme->createCursor( "Edit", NULL ); + ASSERT_STREQ( extra->getName(), "UnitTestThemeAEditCursor2" ); + + ASSERT_TRUE( theme->renameTheme( "UnitTestThemeB" ) ); + + ASSERT_EQ( (SimObject*)theme->getCursor( StringTable->insert( "Edit" ) ), + Sim::findObject( "UnitTestThemeBEditCursor" ) ); + ASSERT_STREQ( extra->getName(), "UnitTestThemeBEditCursor2" ); + ASSERT_TRUE( Sim::findObject( "UnitTestThemeAEditCursor" ) == NULL ); + + theme->deleteObject(); + + SUCCEED(); +} + +TEST( GuiProfileThemeTests, CursorTamlRoundTripPreservesArtAndOverrides ) +{ + const char* fileName = "unitTestGuiProfileThemeCursors.taml"; + + GuiProfileTheme* theme = new GuiProfileTheme(); + theme->registerObject( "UnitTestTheme" ); + theme->setDataField( StringTable->insert( "cursorDirectory" ), NULL, "unitTestThemes/cursors/UnitTestTheme" ); + + GuiCursor* edit = theme->getCursor( StringTable->insert( "Edit" ) ); + edit->setDataField( StringTable->insert( "hotSpot" ), NULL, "3 7" ); + edit->setDataField( StringTable->insert( "color" ), NULL, "9 8 7 6" ); + ASSERT_TRUE( theme->createCursor( "Default", NULL ) != NULL ); + + Taml taml; + ASSERT_TRUE( taml.write( theme, fileName ) ); + theme->deleteObject(); + ASSERT_TRUE( Sim::findObject( "UnitTestThemeEditCursor" ) == NULL ); + + GuiProfileTheme* loaded = taml.read( fileName ); + ASSERT_TRUE( loaded != NULL ); + + GuiCursor* loadedEdit = loaded->getCursor( StringTable->insert( "Edit" ) ); + ASSERT_TRUE( loadedEdit != NULL ); + ASSERT_EQ( (SimObject*)loadedEdit, Sim::findObject( "UnitTestThemeEditCursor" ) ); + ASSERT_EQ( loadedEdit->getTheme(), loaded ); + + // Art persists although nothing marked it overridden - that is the whole + // point of exempting it, since no recipe could rebuild it on load. + ASSERT_EQ( loadedEdit->getHotSpot().x, 3 ); + ASSERT_EQ( loadedEdit->getHotSpot().y, 7 ); + ASSERT_TRUE( dStrstr( loadedEdit->getBitmapName(), "ibeam.png" ) != NULL ); + + // The tint round-trips as an override. + ASSERT_TRUE( loadedEdit->isThemeFieldOverridden( StringTable->insert( "color" ) ) ); + ASSERT_TRUE( loadedEdit->mColor == ColorI( 9, 8, 7, 6 ) ); + + // The extra survives alongside a complete default set. + ASSERT_EQ( loaded->getExtraCursors().size(), 1 ); + GuiCursor* loadedExtra = dynamic_cast( Sim::findObject( "UnitTestThemeDefaultCursor2" ) ); + ASSERT_TRUE( loadedExtra != NULL ); + ASSERT_STREQ( loadedExtra->mCategory, "Default" ); + + loaded->deleteObject(); + Platform::fileDelete( fileName ); + + SUCCEED(); +} + #endif // TORQUE_SHIPPING From 7dd89c05e80aaa439cc21472beef86d5b1232c6a Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Tue, 4 Aug 2026 20:54:06 -0400 Subject: [PATCH 38/55] Gui Editor: cursors in the Profile Editor, and slots that only appear when there is a choice The engine half of this landed cursors in GuiProfileTheme. This is the part you can use. The Profile Editor grows a Cursors folder beside Profiles and Borders, with a pane built around the magnifier rather than around field rows. The name, art, tint, Nudge and Anchor rows are the ordinary field-row machinery; what is not is the hot spot, and the readout under the magnifier 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. The Anchor is set from a 3x3 of presets, because nobody reads "0.5 0.5" as "the middle" at a glance. The preview frame gets a range to move the pointer through with a target to click, because a cursor is judged by using it and nothing else in that pane can tell you whether a hot spot is a pixel out. It swaps the canvas cursor on entry and puts it back on exit; the target shows the cursor too, because a button does not override getCursor, so what the canvas holds is what it displays. That range is dressed in the theme being edited rather than the editor's own chrome, which is also what gives the target a hover state -- the editor's button profile defines no highlight fill, and a target you cannot see yourself hit is no test of anything. It is laid out entirely by sizing flags: a theme is free to give its panels whatever padding it likes, PlanetX does, and hard-coded positions slid straight out from under it. GuiControl::onChildAdded parent-resizes each child against the parent's INNER rect, so "fill" and "center" resolve against the padded area the moment the child is added. In the properties pane a cursor slot joins Variants on exactly the terms a profile slot does: the row appears only where the active theme holds more than one cursor for that job. Set Theme fills all six slots silently either way, so a Gui wears ITS theme's cursors rather than whichever set a project happened to install globally, and an unremarkable window shows no cursor rows at all. TypeGuiCursor stays "hidden" to the generic field walk, or the catch-all Other section would sprout an empty dropdown on every window. Detach writes the canonical name for the slot rather than an empty string. Writing "" through TypeGuiCursor does not clear the field -- it falls back to DefaultCursor -- which would put an arrow on a window's resize edge. At runtime AppCore installs a chosen theme's cursors onto the names the engine hard-codes, by copying fields onto them: a name belongs to one object, and a theme's members must keep their own for the Guis that reference them. guiCursors.cs therefore did not die so much as change where it reads from, and appCore.cs now runs it after loadThemes instead of before. Which theme is $pref::AppCore::cursorTheme, else the only one loaded, else Base, else the first with a warning naming the ambiguity. installThemeCursors is callable at any time, which is how a game swaps between themes that look nothing alike -- swap the set, not the object. Seeding gives each theme its own copy of the stock art and is safe to re-run: pathCopy is asked not to overwrite, so art that has been replaced or edited is never clobbered, and a theme that arrived from another project heals itself on load. A theme rename takes the folder with it. Two shared widgets grew one option each rather than being changed for everybody. GuiProfileEditorFieldRow takes a swatchWidth, so a colour row can be a button instead of a bar while a profile's state-colour rows keep the full width they need to be told apart. EditorButtonBar takes a TooltipFunction beside its existing EnabledFunction, so the two buttons that serve both profiles and cursors can say which one they are about to act on instead of always saying "Profile". PlanetX's AppCore is updated to match the library's, which is the intended path for a project's boilerplate and the only way anything in the repo exercises the new runtime. The toybox's is deliberately untouched -- its module says "historically different from the library AppCore. Do not update it." Two suites: cursorPane drives the pane, the seeded art, the placement arithmetic, extras and the rename; cursorSlots drives the Variants rule, the silent fill, and the detach. Plus a shots harness, because the magnifier is the one part of this whose correctness is a thing you look at. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qi2pZ9UzF5rsLCNVErpzwt --- PlanetX/AppCore/1/appCore.cs | 4 +- PlanetX/AppCore/1/gui/guiCursors.cs | 149 +++-- .../AppCore/1/scripts/defaultPreferences.cs | 6 + PlanetX/AppCore/1/scripts/themes.cs | 91 ++- editor/EditorCore/EditorButtonBar.cs | 14 +- editor/GuiEditor/GuiEditor.cs | 1 + .../GuiEditor/scripts/GuiEditorControlSpec.cs | 27 +- .../scripts/GuiEditorInspectorPane.cs | 97 +++- .../scripts/GuiEditorThemeApplier.cs | 168 ++++++ .../scripts/GuiProfileEditorCursorForm.cs | 524 ++++++++++++++++++ .../scripts/GuiProfileEditorDialog.cs | 146 ++++- .../scripts/GuiProfileEditorFieldRow.cs | 12 +- .../scripts/GuiProfileEditorLibrary.cs | 332 ++++++++++- .../scripts/GuiProfileEditorPreview.cs | 151 +++++ library/AppCore/appCore.cs | 4 +- library/AppCore/gui/guiCursors.cs | 149 +++-- library/AppCore/scripts/defaultPreferences.cs | 6 + library/AppCore/scripts/themes.cs | 81 +++ tests/shots/cursorPane.cs | 107 ++++ tests/smoke/cursorPane.cs | 224 ++++++++ tests/smoke/cursorSlots.cs | 178 ++++++ tests/smoke/planetX.cs | 32 ++ 22 files changed, 2384 insertions(+), 119 deletions(-) create mode 100644 editor/GuiEditor/scripts/GuiProfileEditorCursorForm.cs create mode 100644 tests/shots/cursorPane.cs create mode 100644 tests/smoke/cursorPane.cs create mode 100644 tests/smoke/cursorSlots.cs diff --git a/PlanetX/AppCore/1/appCore.cs b/PlanetX/AppCore/1/appCore.cs index ac0557488..856d192ed 100644 --- a/PlanetX/AppCore/1/appCore.cs +++ b/PlanetX/AppCore/1/appCore.cs @@ -26,9 +26,11 @@ exec("./scripts/constants.cs"); exec("./scripts/defaultPreferences.cs"); exec("./gui/guiCursors.cs"); - %this.createGuiCursors(); 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 diff --git a/PlanetX/AppCore/1/gui/guiCursors.cs b/PlanetX/AppCore/1/gui/guiCursors.cs index bd3331565..7eb14ab73 100644 --- a/PlanetX/AppCore/1/gui/guiCursors.cs +++ b/PlanetX/AppCore/1/gui/guiCursors.cs @@ -20,15 +20,24 @@ // 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. +/// 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 build a project's ~70 GUI profiles as well. Those are now a -/// GuiProfileTheme (see scripts/themes.cs), which derives the whole set from six -/// colors and is editable in the GUI Profile Editor - so a project skins itself -/// by editing a theme rather than by forking a thousand lines of script. Cursors -/// have not moved into the theme yet, so they stay here. +/// 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 @@ -55,54 +64,106 @@ } } -function AppCore::createGuiCursors(%this) +/// 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) { - %this.SafeCreateNamedObject("DefaultCursor", new GuiCursor() + %themes = ""; + if(!isObject(GuiDataGroup)) { - hotSpot = "1 1"; - renderOffset = "0 0"; - bitmapName = "^AppCore/gui/images/cursors/defaultCursor"; - }); + return %themes; + } - %this.SafeCreateNamedObject("LeftRightCursor", new GuiCursor() + for(%i = 0; %i < GuiDataGroup.getCount(); %i++) { - hotSpot = "0.5 0"; - renderOffset = "0.5 0.4"; - bitmapName = "^AppCore/gui/images/cursors/leftRight"; - }); + %object = GuiDataGroup.getObject(%i); + if(%object.getClassName() $= "GuiProfileTheme") + { + %themes = (%themes $= "") ? %object.getId() : (%themes SPC %object.getId()); + } + } + + return %themes; +} - %this.SafeCreateNamedObject("UpDownCursor", new GuiCursor() +/// 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) { - hotSpot = "1 1"; - renderOffset = "0.5 0.5"; - bitmapName = "^AppCore/gui/images/cursors/upDown"; - }); + return 0; + } - %this.SafeCreateNamedObject("NWSECursor", new GuiCursor() + if($pref::AppCore::cursorTheme !$= "") { - hotSpot = "1 1"; - renderOffset = "0.5 0.5"; - bitmapName = "^AppCore/gui/images/cursors/NWSE"; - }); + 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."); + } - %this.SafeCreateNamedObject("NESWCursor", new GuiCursor() + if(%count == 1) { - hotSpot = "1 1"; - renderOffset = "0.5 0.5"; - bitmapName = "^AppCore/gui/images/cursors/NESW"; - }); + return getWord(%themes, 0); + } - %this.SafeCreateNamedObject("MoveCursor", new GuiCursor() + for(%i = 0; %i < %count; %i++) { - hotSpot = "1 1"; - renderOffset = "0.5 0.5"; - bitmapName = "^AppCore/gui/images/cursors/move"; - }); + %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; + } - %this.SafeCreateNamedObject("EditCursor", new GuiCursor() + %categories = %theme.getCursorCategoryNames(); + %count = getWordCount(%categories); + for(%i = 0; %i < %count; %i++) { - hotSpot = "0 0"; - renderOffset = "0.5 0.5"; - bitmapName = "^AppCore/gui/images/cursors/ibeam"; - }); + %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/scripts/defaultPreferences.cs b/PlanetX/AppCore/1/scripts/defaultPreferences.cs index 37543dd9d..c4a015b5e 100644 --- a/PlanetX/AppCore/1/scripts/defaultPreferences.cs +++ b/PlanetX/AppCore/1/scripts/defaultPreferences.cs @@ -35,6 +35,12 @@ $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; diff --git a/PlanetX/AppCore/1/scripts/themes.cs b/PlanetX/AppCore/1/scripts/themes.cs index a85d48947..1a61994d0 100644 --- a/PlanetX/AppCore/1/scripts/themes.cs +++ b/PlanetX/AppCore/1/scripts/themes.cs @@ -65,6 +65,83 @@ 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(); @@ -95,9 +172,10 @@ } /// 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 (and, from -/// older versions, as a bare profile). Loading is all these need: a profile puts -/// itself in the Gui data group under its own name, which is how a Gui finds it. +/// 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); @@ -118,10 +196,12 @@ } %this.repairFontDirectory(%object); + %this.seedThemeCursors(%object); return true; } - if(%class $= "ScriptGroup" || %class $= "GuiControlProfile") + // A bundle is a SimSet; SimGroup, which the older ones are, derives from it. + if(%object.isMemberOfClass("SimSet") || %class $= "GuiControlProfile") { return false; } @@ -171,6 +251,9 @@ borderSize = 1; }; + // Before the write, so the file records where the art went. + %this.seedThemeCursors(%theme); + %file = pathConcat(%path, "Base.taml"); TAMLWrite(%theme, %file); 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/GuiEditor/GuiEditor.cs b/editor/GuiEditor/GuiEditor.cs index c5923cd97..4ee717b15 100644 --- a/editor/GuiEditor/GuiEditor.cs +++ b/editor/GuiEditor/GuiEditor.cs @@ -43,6 +43,7 @@ exec("./scripts/GuiProfileEditorFieldRow.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"); diff --git a/editor/GuiEditor/scripts/GuiEditorControlSpec.cs b/editor/GuiEditor/scripts/GuiEditorControlSpec.cs index 9c7e239cf..75a6426f6 100644 --- a/editor/GuiEditor/scripts/GuiEditorControlSpec.cs +++ b/editor/GuiEditor/scripts/GuiEditorControlSpec.cs @@ -55,9 +55,14 @@ // // Fields absent from everything here are never shown: canSave (its write // function is defaultProtectedNotWriteFn, so it never even serializes), -// canSaveDynamicFields, parentGroup, and every TypeGuiCursor field -- GuiCursor -// has no category and GuiProfileTheme has no cursor table, so cursors cannot -// join the Variants scheme the profile slots use. +// 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. //----------------------------------------------------------------------------- @@ -850,6 +855,13 @@ 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"; @@ -990,6 +1002,15 @@ %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) diff --git a/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs b/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs index 8388bb7af..1cb256108 100644 --- a/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs +++ b/editor/GuiEditor/scripts/GuiEditorInspectorPane.cs @@ -703,6 +703,11 @@ class = "GuiProfileEditorFieldRow"; // 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")) @@ -715,19 +720,49 @@ class = "GuiProfileEditorFieldRow"; for(%i = 0; %i < %count; %i++) { %field = %ctrl.getField(%i); - if(%ctrl.getFieldType(%field) !$= "GuiProfile" || %field $= "Profile") + %type = %ctrl.getFieldType(%field); + + if(%type $= "GuiProfile" && %field !$= "Profile") { - continue; + if(getWordCount(%this.profileAnchors(%ctrl, %field)) > 1) + { + %fields = (%fields $= "") ? %field : (%fields SPC %field); + } } - if(getWordCount(%this.profileAnchors(%ctrl, %field)) <= 1) + else if(%type $= "GuiCursor") { - continue; + if(getWordCount(%this.cursorAnchors(%ctrl, %field)) > 1) + { + %fields = (%fields $= "") ? %field : (%fields SPC %field); + } } - %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(); @@ -1381,13 +1416,49 @@ class = "GuiProfileEditorFieldRow"; { %field = getWord(%slots, %i); %row = %this.row[%field]; - if(isObject(%row)) + 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) @@ -1506,6 +1577,14 @@ class = "GuiProfileEditorFieldRow"; %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()); @@ -1530,6 +1609,12 @@ class = "GuiProfileEditorFieldRow"; %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); diff --git a/editor/GuiEditor/scripts/GuiEditorThemeApplier.cs b/editor/GuiEditor/scripts/GuiEditorThemeApplier.cs index bef108ffb..29f9eabf4 100644 --- a/editor/GuiEditor/scripts/GuiEditorThemeApplier.cs +++ b/editor/GuiEditor/scripts/GuiEditorThemeApplier.cs @@ -16,6 +16,29 @@ { %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" @@ -269,9 +292,108 @@ %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 @@ -384,6 +506,8 @@ %changed++; } + %changed += %this.detachCursors(%ctrl, %theme); + for(%i = 0; %i < %ctrl.getCount(); %i++) { %changed += %this.detachWalk(%ctrl.getObject(%i), %theme, %profile); @@ -392,6 +516,50 @@ 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. //----------------------------------------------------------------------------- diff --git a/editor/GuiEditor/scripts/GuiProfileEditorCursorForm.cs b/editor/GuiEditor/scripts/GuiProfileEditorCursorForm.cs new file mode 100644 index 000000000..223fc574c --- /dev/null +++ b/editor/GuiEditor/scripts/GuiProfileEditorCursorForm.cs @@ -0,0 +1,524 @@ + +//----------------------------------------------------------------------------- +// 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; +} + +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 = "GuiProfileEditorFieldRow"; + 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::onProfileRowCommit(%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::onProfileRowReset(%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 index 5859f51db..698642be9 100644 --- a/editor/GuiEditor/scripts/GuiProfileEditorDialog.cs +++ b/editor/GuiEditor/scripts/GuiProfileEditorDialog.cs @@ -38,8 +38,10 @@ %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"); - %this.toolbar.addButton("onNewProfile", $EditorIcon::round_plus, "New Profile in Category", "getCategorySelected"); - %this.toolbar.addButton("onRemoveProfile", $EditorIcon::round_delete, "Remove Extra Profile", "getExtraSelected"); + // 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. @@ -273,6 +275,42 @@ class = "GuiProfileEditorBorderForm"; %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. @@ -399,6 +437,10 @@ class = "GuiProfileEditorPreview"; { %this.borderForm.unbind(); } + if(isObject(%this.cursorForm)) + { + %this.cursorForm.unbind(); + } if(isObject(%this.borderChain)) { %this.unbindBorderSetters(); @@ -439,6 +481,16 @@ class = "GuiProfileEditorPreview"; %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; @@ -473,6 +525,12 @@ class = "GuiProfileEditorPreview"; %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(); @@ -520,6 +578,10 @@ class = "GuiProfileEditorPreview"; { %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); @@ -578,6 +640,11 @@ class = "GuiProfileEditorPreview"; 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) @@ -585,7 +652,7 @@ class = "GuiProfileEditorPreview"; return %kind $= "root" || %kind $= "folder"; } -// Drops all three member panes out of the Properties window. Each pane is also +// 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) { @@ -595,6 +662,8 @@ class = "GuiProfileEditorPreview"; %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 @@ -782,14 +851,28 @@ class = "GuiProfileEditorBorderSetter"; 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) { - return isObject(%this.currentProxy) && %this.currentProxy.kind $= "category"; + if(!isObject(%this.currentProxy)) + { + return false; + } + %kind = %this.currentProxy.kind; + return %kind $= "category" || %kind $= "cursorCategory"; } function GuiProfileEditorDialog::getExtraSelected(%this) { - return isObject(%this.currentProxy) && %this.currentProxy.kind $= "extra"; + if(!isObject(%this.currentProxy)) + { + return false; + } + %kind = %this.currentProxy.kind; + return %kind $= "extra" || %kind $= "cursorExtra"; } function GuiProfileEditorDialog::getMemberSelected(%this) @@ -799,7 +882,23 @@ class = "GuiProfileEditorBorderSetter"; return false; } %kind = %this.currentProxy.kind; - return %kind $= "category" || %kind $= "border" || %kind $= "extra"; + 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"; } //----------------------------------------------------------------------------- @@ -929,8 +1028,20 @@ class = "GuiProfileEditorBorderSetter"; { return; } - %profile = %this.library.createExtraProfile(%this.currentProxy.theme, %this.currentProxy.category); - if(isObject(%profile)) + + %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(); } @@ -943,15 +1054,24 @@ class = "GuiProfileEditorBorderSetter"; return; } %theme = %this.currentProxy.theme; - %profile = %this.currentProxy.target; + %member = %this.currentProxy.target; + %isCursor = %this.currentProxy.kind $= "cursorExtra"; %this.preview.clearSamples(); - %this.profileForm.unbind(); + %this.hideMemberPanes(); %this.currentProxy = ""; %this.currentRoot = ""; %this.currentMember = ""; - %this.library.removeExtraProfile(%theme, %profile); + if(%isCursor) + { + %this.library.removeExtraCursor(%theme, %member); + } + else + { + %this.library.removeExtraProfile(%theme, %member); + } + %this.tree.refresh(); %this.toolbar.refreshEnabled(); } @@ -984,6 +1104,10 @@ class = "GuiProfileEditorBorderSetter"; { %this.borderForm.bind(%this.currentMember, %this.currentProxy.treeLabel); } + else if(%this.isCursorKind(%this.currentProxy.kind)) + { + %this.cursorForm.refresh(); + } else { %this.profileForm.refresh(); diff --git a/editor/GuiEditor/scripts/GuiProfileEditorFieldRow.cs b/editor/GuiEditor/scripts/GuiProfileEditorFieldRow.cs index 7b9c000b5..2976759ea 100644 --- a/editor/GuiEditor/scripts/GuiProfileEditorFieldRow.cs +++ b/editor/GuiEditor/scripts/GuiProfileEditorFieldRow.cs @@ -96,7 +96,13 @@ } else if(%kind $= "color") { - %this.editor = %this.makeSwatch(%pad, %editorY, %editorW, 22); + // 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") { @@ -255,7 +261,9 @@ class = %numeric ? "GuiProfileEditorRowInput" : ""; %swatch = new GuiColorPopupCtrl() { class = "GuiProfileEditorColorPopup"; - HorizSizing = "width"; + // 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; diff --git a/editor/GuiEditor/scripts/GuiProfileEditorLibrary.cs b/editor/GuiEditor/scripts/GuiProfileEditorLibrary.cs index f56d79f62..a815630af 100644 --- a/editor/GuiEditor/scripts/GuiProfileEditorLibrary.cs +++ b/editor/GuiEditor/scripts/GuiProfileEditorLibrary.cs @@ -106,6 +106,146 @@ 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 @ "/"); + %oldPrefix = makeRelativePath(%source, getMainDotCsDir()); + %newPrefix = makeRelativePath(%target, getMainDotCsDir()); + + %categories = %theme.getCursorCategoryNames(); + for(%i = 0; %i < getWordCount(%categories); %i++) + { + %category = getWord(%categories, %i); + %file = %theme.getCursorStockFile(%category); + if(%file !$= "") + { + pathCopy(pathConcat(%source, %file), pathConcat(%target, %file)); + %this.doomFile(pathConcat(%source, %file)); + } + + %members = %theme.getCursors(%category); + for(%m = 0; %m < getWordCount(%members); %m++) + { + %this.repointCursorArt(getWord(%members, %m), %oldPrefix, %newPrefix); + } + } + + %theme.cursorDirectory = %newPrefix; + return true; +} + +// Move one cursor's bitmap from the old folder to the new, if that is where it +// lives. Compared as relative paths because that is the form a theme stores. +function GuiProfileEditorLibrary::repointCursorArt(%this, %cursor, %oldPrefix, %newPrefix) +{ + %current = %cursor.bitmapName; + if(%current $= "" || strpos(%current, %oldPrefix) != 0) + { + return; + } + + %cursor.bitmapName = %newPrefix @ getSubStr(%current, strlen(%oldPrefix), strlen(%current) - strlen(%oldPrefix)); +} + // 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). @@ -204,6 +344,39 @@ 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 - @@ -304,6 +477,10 @@ // 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; } @@ -414,6 +591,7 @@ %this.sourceFile[%theme.getId()] = %file; %this.loadedFile[%file] = true; %this.applyFontsPath(%theme); + %this.seedThemeCursors(%theme); %this.addThemeProxies(%theme); } } @@ -483,6 +661,38 @@ %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. @@ -515,6 +725,32 @@ %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(); @@ -816,13 +1052,24 @@ // 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.doomedFile[%this.doomedFileCount] = %file; - %this.doomedFileCount++; + %this.doomFile(%file); %this.sourceFile[%root.getId()] = ""; %this.loadedFile[%file] = ""; } @@ -855,6 +1102,7 @@ %theme.borderSize = 1; %theme.fontSize = 16; %this.applyFontsPath(%theme); + %this.seedThemeCursors(%theme); %this.sourceFile[%theme.getId()] = ""; %this.addThemeProxies(%theme); @@ -876,6 +1124,8 @@ function GuiProfileEditorLibrary::renameThemeTo(%this, %theme, %name) { + %oldName = %theme.getName(); + %this.beginNaming(); %renamed = %theme.renameTheme(%name); %this.endNaming(); @@ -885,6 +1135,11 @@ 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); @@ -912,6 +1167,21 @@ } } + %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; } @@ -931,6 +1201,64 @@ 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; +} + +function GuiProfileEditorLibrary::removeExtraCursor(%this, %theme, %cursor) +{ + %leaf = %this.cursorExtraProxy[%cursor.getId()]; + + // Its art was made for it alone, so it goes too - but only on save, like + // every other file this editor drops. + %bitmap = %cursor.bitmapName; + + %removed = %theme.removeCursor(%cursor); + if(!%removed) + { + return false; + } + + if(%bitmap !$= "") + { + %this.doomFile(makeFullPath(%bitmap, getMainDotCsDir())); + } + + if(isObject(%leaf)) + { + %leaf.delete(); + } + %this.cursorExtraProxy[%cursor.getId()] = ""; + %this.markDirty(%theme); + return true; +} + function GuiProfileEditorLibrary::removeExtraProfile(%this, %theme, %profile) { // Only an extra has a leaf, and only an extra can be removed - so this is also diff --git a/editor/GuiEditor/scripts/GuiProfileEditorPreview.cs b/editor/GuiEditor/scripts/GuiProfileEditorPreview.cs index 9bfc094c8..56324b78c 100644 --- a/editor/GuiEditor/scripts/GuiProfileEditorPreview.cs +++ b/editor/GuiEditor/scripts/GuiProfileEditorPreview.cs @@ -102,6 +102,10 @@ { %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 @@ -242,6 +246,14 @@ %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++) @@ -286,6 +298,23 @@ } } +// 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) @@ -641,6 +670,128 @@ %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. //----------------------------------------------------------------------------- diff --git a/library/AppCore/appCore.cs b/library/AppCore/appCore.cs index ac0557488..856d192ed 100644 --- a/library/AppCore/appCore.cs +++ b/library/AppCore/appCore.cs @@ -26,9 +26,11 @@ exec("./scripts/constants.cs"); exec("./scripts/defaultPreferences.cs"); exec("./gui/guiCursors.cs"); - %this.createGuiCursors(); 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 diff --git a/library/AppCore/gui/guiCursors.cs b/library/AppCore/gui/guiCursors.cs index bd3331565..7eb14ab73 100644 --- a/library/AppCore/gui/guiCursors.cs +++ b/library/AppCore/gui/guiCursors.cs @@ -20,15 +20,24 @@ // 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. +/// 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 build a project's ~70 GUI profiles as well. Those are now a -/// GuiProfileTheme (see scripts/themes.cs), which derives the whole set from six -/// colors and is editable in the GUI Profile Editor - so a project skins itself -/// by editing a theme rather than by forking a thousand lines of script. Cursors -/// have not moved into the theme yet, so they stay here. +/// 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 @@ -55,54 +64,106 @@ } } -function AppCore::createGuiCursors(%this) +/// 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) { - %this.SafeCreateNamedObject("DefaultCursor", new GuiCursor() + %themes = ""; + if(!isObject(GuiDataGroup)) { - hotSpot = "1 1"; - renderOffset = "0 0"; - bitmapName = "^AppCore/gui/images/cursors/defaultCursor"; - }); + return %themes; + } - %this.SafeCreateNamedObject("LeftRightCursor", new GuiCursor() + for(%i = 0; %i < GuiDataGroup.getCount(); %i++) { - hotSpot = "0.5 0"; - renderOffset = "0.5 0.4"; - bitmapName = "^AppCore/gui/images/cursors/leftRight"; - }); + %object = GuiDataGroup.getObject(%i); + if(%object.getClassName() $= "GuiProfileTheme") + { + %themes = (%themes $= "") ? %object.getId() : (%themes SPC %object.getId()); + } + } + + return %themes; +} - %this.SafeCreateNamedObject("UpDownCursor", new GuiCursor() +/// 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) { - hotSpot = "1 1"; - renderOffset = "0.5 0.5"; - bitmapName = "^AppCore/gui/images/cursors/upDown"; - }); + return 0; + } - %this.SafeCreateNamedObject("NWSECursor", new GuiCursor() + if($pref::AppCore::cursorTheme !$= "") { - hotSpot = "1 1"; - renderOffset = "0.5 0.5"; - bitmapName = "^AppCore/gui/images/cursors/NWSE"; - }); + 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."); + } - %this.SafeCreateNamedObject("NESWCursor", new GuiCursor() + if(%count == 1) { - hotSpot = "1 1"; - renderOffset = "0.5 0.5"; - bitmapName = "^AppCore/gui/images/cursors/NESW"; - }); + return getWord(%themes, 0); + } - %this.SafeCreateNamedObject("MoveCursor", new GuiCursor() + for(%i = 0; %i < %count; %i++) { - hotSpot = "1 1"; - renderOffset = "0.5 0.5"; - bitmapName = "^AppCore/gui/images/cursors/move"; - }); + %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; + } - %this.SafeCreateNamedObject("EditCursor", new GuiCursor() + %categories = %theme.getCursorCategoryNames(); + %count = getWordCount(%categories); + for(%i = 0; %i < %count; %i++) { - hotSpot = "0 0"; - renderOffset = "0.5 0.5"; - bitmapName = "^AppCore/gui/images/cursors/ibeam"; - }); + %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/library/AppCore/scripts/defaultPreferences.cs b/library/AppCore/scripts/defaultPreferences.cs index 37543dd9d..c4a015b5e 100644 --- a/library/AppCore/scripts/defaultPreferences.cs +++ b/library/AppCore/scripts/defaultPreferences.cs @@ -35,6 +35,12 @@ $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; diff --git a/library/AppCore/scripts/themes.cs b/library/AppCore/scripts/themes.cs index 2b59420b3..1a61994d0 100644 --- a/library/AppCore/scripts/themes.cs +++ b/library/AppCore/scripts/themes.cs @@ -65,6 +65,83 @@ 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(); @@ -119,6 +196,7 @@ } %this.repairFontDirectory(%object); + %this.seedThemeCursors(%object); return true; } @@ -173,6 +251,9 @@ borderSize = 1; }; + // Before the write, so the file records where the art went. + %this.seedThemeCursors(%theme); + %file = pathConcat(%path, "Base.taml"); TAMLWrite(%theme, %file); diff --git a/tests/shots/cursorPane.cs b/tests/shots/cursorPane.cs new file mode 100644 index 000000000..81e274460 --- /dev/null +++ b/tests/shots/cursorPane.cs @@ -0,0 +1,107 @@ +// Visual harness for the cursor pane. The hot-spot editor is the part of this +// feature that only exists to be looked at, so these are the shots that say +// whether it works: the magnifier at a few zooms, the anchor mark and the hot +// spot mark distinguishable from each other, the tint following the theme, and +// the try-it range in the preview frame. +// Run: tests/run.ps1 -Shots cursorPane ; look in shots/. + +setLogMode(1); +$Scripts::ignoreDSOs = true; +setScriptExecEcho(false); +trace(false); + +testExec("editor/main.cs"); +schedule(2500, 0, "sStep1"); + +function sStep1() +{ + ProjectManager.setProjectFolder("cursorPaneShotProject"); + GuiEditor.open(); + GuiEditor.openProfileEditor(); + + %d = GuiEditor.profileEditorDialog; + $sTheme = %d.library.createTheme("CursorShot"); + %d.tree.refresh(); + + createPath(testRoot("shots/")); + + // category TAB zoom TAB name + $sCases = + "Default" TAB "8" TAB "default" NL + "Default" TAB "16" TAB "close" NL + "Move" TAB "6" TAB "move" NL + "Edit" TAB "10" TAB "ibeam" NL + "LeftRight" TAB "8" TAB "leftRight" NL + "NWSE" TAB "8" TAB "corner"; + $sIndex = 0; + schedule(600, 0, "sShoot"); +} + +function sShoot() +{ + %rec = getRecord($sCases, $sIndex); + %category = getField(%rec, 0); + %zoom = getField(%rec, 1); + %name = getField(%rec, 2); + + %d = GuiEditor.profileEditorDialog; + %d.onTreeSelect(%d.library.cursorCategoryProxy[$sTheme.getId() @ "_" @ %category]); + %d.cursorForm.editor.setZoom(%zoom); + %d.cursorForm.refreshReadout(); + + echo("SHOT: " @ %name @ " - " @ %category @ " at " @ %zoom @ "x"); + schedule(500, 0, "sGrab", %name); +} + +function sGrab(%name) +{ + screenShot(testRoot("shots/cursorPane_" @ %name @ ".png"), "PNG"); + echo("SHOT: wrote cursorPane_" @ %name @ ".png"); + + $sIndex++; + if($sIndex < getRecordCount($sCases)) + { + schedule(400, 0, "sShoot"); + return; + } + + schedule(300, 0, "sTinted"); +} + +// The tint is what makes a grayscale stock set look like it belongs to the +// theme, so it needs a shot of its own against a colour nobody could mistake +// for the art. +function sTinted() +{ + %d = GuiEditor.profileEditorDialog; + $sTheme.colorForeground = "60 200 255 255"; + %d.onTreeSelect(%d.library.cursorCategoryProxy[$sTheme.getId() @ "_Move"]); + %d.cursorForm.editor.setZoom(8); + + schedule(500, 0, "sGrabTinted"); +} + +function sGrabTinted() +{ + screenShot(testRoot("shots/cursorPane_tinted.png"), "PNG"); + echo("SHOT: wrote cursorPane_tinted.png"); + + // An anchored cursor: the faint crosshair (renderOffset) and the marked + // pixel (hotSpot) should be visibly apart, which is the whole reason the + // pane draws both. + %d = GuiEditor.profileEditorDialog; + %d.cursorForm.onAnchorPreset(0.5, 0.5); + %d.cursorForm.row["hotSpot"].applyValue("6 -4"); + %d.cursorForm.onProfileRowCommit(%d.cursorForm.row["hotSpot"]); + + schedule(500, 0, "sGrabAnchored"); +} + +function sGrabAnchored() +{ + screenShot(testRoot("shots/cursorPane_anchored.png"), "PNG"); + echo("SHOT: wrote cursorPane_anchored.png"); + + echo("SHOT DONE"); + schedule(400, 0, "quit"); +} diff --git a/tests/smoke/cursorPane.cs b/tests/smoke/cursorPane.cs new file mode 100644 index 000000000..cc59a7097 --- /dev/null +++ b/tests/smoke/cursorPane.cs @@ -0,0 +1,224 @@ +// Cursor-pane smoke test. Drives the Gui Profile Editor's cursor support: the +// Cursors folder in the tree, the pane that replaces the other three when a +// cursor node is selected, the seeded per-theme art, the hot-spot magnifier and +// its drag arithmetic, extras within a category, and the theme rename that has +// to take the art folder with it. +// Run: tests/run.ps1 cursorPane ; grep CURSMOKE in console.log. + +setLogMode(2); +$Scripts::ignoreDSOs = true; +setScriptExecEcho(false); +trace(false); + +function cCheck(%label, %cond) +{ + if(%cond) echo("CURSMOKE PASS: " @ %label); + else echo("CURSMOKE FAIL: " @ %label); +} + +testExec("editor/main.cs"); +schedule(2000, 0, "cStep1"); + +function cStep1() +{ + ProjectManager.setProjectFolder("cursorPaneSmokeProject"); + GuiEditor.open(); + GuiEditor.openProfileEditor(); + %d = GuiEditor.profileEditorDialog; + cCheck("dialog opened", isObject(%d)); + + %theme = %d.library.createTheme("CurSmoke"); + %d.tree.refresh(); + $curSmokeTheme = %theme; + + // --- The theme provides a cursor per category, with art of its own. --- + %categories = %theme.getCursorCategoryNames(); + cCheck("theme reports seven cursor categories", getWordCount(%categories) == 7); + + %cursor = %theme.getCursor("Default"); + cCheck("default cursor member exists", isObject(%cursor)); + cCheck("member is named for the theme", %cursor.getName() $= "CurSmokeDefaultCursor"); + cCheck("member carries its category", %cursor.category $= "Default"); + cCheck("art was seeded into the theme's own folder", + strstr(%cursor.bitmapName, "cursors/CurSmoke") >= 0); + cCheck("the art file is really there", + isFile(makeFullPath(%cursor.bitmapName, getMainDotCsDir()))); + cCheck("the tint came from the palette", %cursor.color $= %theme.colorForeground); + + // --- Selecting a cursor node brings the cursor pane, and only that. --- + %proxy = %d.library.cursorCategoryProxy[%theme.getId() @ "_Default"]; + cCheck("cursor category proxy built", isObject(%proxy)); + %d.onTreeSelect(%proxy); + + cCheck("cursor form shown", %d.cursorFormScroller.isVisible()); + cCheck("profile form hidden for a cursor", !%d.profileFormScroller.isVisible()); + cCheck("border form hidden for a cursor", !%d.borderFormScroller.isVisible()); + cCheck("theme form hidden for a cursor", !%d.formScroller.isVisible()); + cCheck("borders pane hidden for a cursor", !%d.bordersWindow.isVisible()); + cCheck("current member is the cursor", %d.currentMember == %cursor.getId()); + cCheck("pane bound to the cursor", %d.cursorForm.target == %cursor.getId()); + cCheck("magnifier bound to the cursor", %d.cursorForm.editor.cursor $= %cursor.getName()); + + // The two color buttons are buttons, not bars: a swatch spanning the whole + // row reads as a progress bar. Both are the same size as each other, which + // is what says they do the same kind of job. + cCheck("the tint swatch is button-shaped", + getWord(%d.cursorForm.row["color"].editor.getExtent(), 0) == $CursorForm::SwatchWidth); + cCheck("the marker swatch matches it", + getWord(%d.cursorForm.dotSwatch.getExtent(), 0) == $CursorForm::SwatchWidth); + // ...while a profile's state-color rows still fill their cell, where four + // swatches share the width and that is how you tell them apart. + %d.onTreeSelect(%d.library.categoryProxy[%theme.getId() @ "_Button"]); + cCheck("profile color rows still fill their cell", + getWord(%d.profileForm.fillRow.swatch[0].getExtent(), 0) > 40); + %d.onTreeSelect(%proxy); + + schedule(400, 0, "cStep2"); +} + +function cStep2() +{ + %d = GuiEditor.profileEditorDialog; + %theme = $curSmokeTheme; + %cursor = %theme.getCursor("Default"); + %editor = %d.cursorForm.editor; + + // --- The magnifier read the art, so it knows its real size. --- + %extent = %editor.getImageExtent(); + cCheck("magnifier measured the art", getWord(%extent, 0) == 13 && getWord(%extent, 1) == 17); + + // --- The try-it range lays out against the padded content, not the box. --- + // It wears a profile from the theme being edited, and a theme may give its + // panels any padding it likes, so nothing here may be positioned by number. + // These two hold whatever the padding is: the hint fills the content width, + // and the target sits in the middle of that same width. + %range = %d.preview.stage.getObject(0); + cCheck("the try-it range is on the stage", isObject(%range)); + %hint = %range.getObject(0); + %target = %range.getObject(1); + + %hintWidth = getWord(%hint.getExtent(), 0); + cCheck("the hint filled the content width", + %hintWidth > 0 && %hintWidth <= getWord(%range.getExtent(), 0)); + cCheck("the hint starts at the content's left edge", getWord(%hint.getPosition(), 0) == 0); + cCheck("the target is centred in that same width", + getWord(%target.getPosition(), 0) == ((%hintWidth - getWord(%target.getExtent(), 0)) / 2)); + + // --- The zoom never claims a magnification it is not drawing. --- + // A stock-sized cursor must be able to reach the full 16x: the pane is sized + // for it. Anything less means the view shrank and the ceiling came with it. + %max = %editor.getMaxZoom(); + cCheck("a 13x17 cursor reaches the full 16x", %max == 16); + %editor.setZoom(16); + cCheck("asking past the ceiling reports the ceiling, not the wish", + %editor.getZoom() == %max); + %d.cursorForm.refreshReadout(); + cCheck("the zoom label agrees with what is drawn", + %d.cursorForm.zoomLabel.getText() $= (%max @ "x")); + cCheck("zoom in is greyed at the ceiling", !%d.cursorForm.zoomIn.isActive()); + cCheck("zoom out is live at the ceiling", %d.cursorForm.zoomOut.isActive()); + + %editor.setZoom(1); + %d.cursorForm.refreshReadout(); + cCheck("zoom out is greyed at 1x", !%d.cursorForm.zoomOut.isActive()); + cCheck("zoom in is live at 1x", %d.cursorForm.zoomIn.isActive()); + %editor.setZoom(8); + + // The stock Default cursor: hot spot 1,1 with no anchor, so the pointer + // lands on pixel 1,1. + cCheck("effective hot spot combines both fields", %editor.getEffectiveHotSpot() $= "1 1"); + + // --- Anchoring is a fraction of the art, and the nudge absorbs it. --- + // Compared numerically: a Point2F reads back in the console's float format, + // not as the string it was written with. + %d.cursorForm.onAnchorPreset(0.5, 0.5); + cCheck("anchor preset wrote renderOffset", + getWord(%cursor.renderOffset, 0) == 0.5 && getWord(%cursor.renderOffset, 1) == 0.5); + // 13 * 0.5 truncates to 6, 17 * 0.5 to 8, and the nudge is still 1,1. + cCheck("anchor moved where the cursor points", %editor.getEffectiveHotSpot() $= "7 9"); + + %d.cursorForm.onAnchorPreset(0, 0); + cCheck("anchor cleared again", %editor.getEffectiveHotSpot() $= "1 1"); + + // The readout is a function of both placement fields, so a typed edit to + // either has to move it. It used to report the pixel the dot had left, + // which reads as the magnifier disagreeing with its own numbers. + %d.cursorForm.row["hotSpot"].applyValue("5 6"); + %d.cursorForm.onProfileRowCommit(%d.cursorForm.row["hotSpot"]); + cCheck("a typed nudge moved where the cursor points", %editor.getEffectiveHotSpot() $= "5 6"); + cCheck("the readout followed the typed nudge", + strstr(%d.cursorForm.readout.getText(), "5, 6") >= 0); + + %d.cursorForm.row["hotSpot"].applyValue("1 1"); + %d.cursorForm.onProfileRowCommit(%d.cursorForm.row["hotSpot"]); + + // --- A tint edit is an override; the art is not. --- + // applyValue rather than setValue: setValue also records the new value as + // the baseline, so the commit that follows would see nothing changed -- the + // guard that stops a text box committing a field the user only tabbed past. + %d.cursorForm.row["color"].applyValue("10 20 30 255"); + %d.cursorForm.onProfileRowCommit(%d.cursorForm.row["color"]); + cCheck("tint committed to the cursor", + getWord(%cursor.color, 0) == 10 && getWord(%cursor.color, 2) == 30); + cCheck("tint counts as a theme override", %theme.isFieldOverridden(%cursor, "color")); + cCheck("hot spot is never an override", !%theme.isFieldOverridden(%cursor, "hotSpot")); + cCheck("art is never an override", !%theme.isFieldOverridden(%cursor, "bitmapName")); + cCheck("editing marked the theme dirty", %d.library.isDirty()); + + // A restamp must not undo the art, only re-derive the tint. + %theme.resetField(%cursor, "color"); + cCheck("resetting the tint re-derives it", %cursor.color $= %theme.colorForeground); + cCheck("the art survived the restamp", strstr(%cursor.bitmapName, "cursors/CurSmoke") >= 0); + + schedule(400, 0, "cStep3"); +} + +function cStep3() +{ + %d = GuiEditor.profileEditorDialog; + %theme = $curSmokeTheme; + + // --- An extra in a category: what makes the Gui Editor offer a choice. --- + %extra = %d.library.createExtraCursor(%theme, "Default"); + cCheck("extra cursor created", isObject(%extra)); + cCheck("extra sits in the same category", %extra.category $= "Default"); + cCheck("category now offers two cursors", getWordCount(%theme.getCursors("Default")) == 2); + cCheck("extra got art of its own", + %extra.bitmapName !$= %theme.getCursor("Default").bitmapName); + cCheck("the extra's art file exists", + isFile(makeFullPath(%extra.bitmapName, getMainDotCsDir()))); + + %leaf = %d.library.cursorExtraProxy[%extra.getId()]; + cCheck("extra has a tree leaf", isObject(%leaf)); + %d.onTreeSelect(%leaf); + cCheck("cursor pane binds an extra too", %d.cursorForm.target == %extra.getId()); + + // The two shared toolbar buttons describe what they are about to act on. + cCheck("remove tip names a cursor while one is selected", + %d.removeExtraTip() $= "Remove Extra Cursor"); + %d.onTreeSelect(%d.library.cursorCategoryProxy[%theme.getId() @ "_Default"]); + cCheck("new tip names a cursor in a cursor category", + %d.newInCategoryTip() $= "New Cursor in Category"); + %d.onTreeSelect(%d.library.categoryProxy[%theme.getId() @ "_Button"]); + cCheck("new tip still names a profile in a profile category", + %d.newInCategoryTip() $= "New Profile in Category"); + %d.onTreeSelect(%leaf); + + // --- Renaming the theme takes the members and the art folder with it. --- + cCheck("theme renamed", %d.library.renameThemeTo(%theme, "CurSmokeTwo")); + cCheck("members followed the rename", + %theme.getCursor("Default").getName() $= "CurSmokeTwoDefaultCursor"); + cCheck("art folder followed the rename", + strstr(%theme.getCursor("Default").bitmapName, "cursors/CurSmokeTwo") >= 0); + cCheck("the moved art file exists", + isFile(makeFullPath(%theme.getCursor("Default").bitmapName, getMainDotCsDir()))); + + // --- Removing the extra puts the category back to one. --- + %d.library.removeExtraCursor(%theme, %extra); + cCheck("extra removed", getWordCount(%theme.getCursors("Default")) == 1); + cCheck("a default cursor cannot be removed", + !%theme.removeCursor(%theme.getCursor("Default"))); + + echo("CURSMOKE DONE"); + schedule(300, 0, "quit"); +} diff --git a/tests/smoke/cursorSlots.cs b/tests/smoke/cursorSlots.cs new file mode 100644 index 000000000..3698b7bc9 --- /dev/null +++ b/tests/smoke/cursorSlots.cs @@ -0,0 +1,178 @@ +// Cursor-slot smoke test: the Gui Editor half of cursor support. +// +// A control's cursor fields follow the same rule as its secondary profile +// slots. Set Theme fills them without being asked, so a window is on its own +// theme's cursors from the moment it is dropped -- but a row only appears once +// the theme holds a second cursor for that job and there is a choice to make. +// Detaching a theme has to move them somewhere real as well: a GuiCursor* is as +// raw a pointer as a profile's. +// Run: tests/run.ps1 cursorSlots ; grep CSSMOKE in console.log. + +setLogMode(1); +$Scripts::ignoreDSOs = true; +setScriptExecEcho(false); +trace(false); + +function sCheck(%label, %cond) +{ + if(%cond) echo("CSSMOKE PASS: " @ %label); + else echo("CSSMOKE FAIL: " @ %label); +} + +function sPane() +{ + return GuiEditor.inspectorWindow.pane; +} + +function sRebind(%ctrl) +{ + %pane = sPane(); + %pane.bind(%ctrl); + return %pane; +} + +function sHasRow(%pane, %field) +{ + return isObject(%pane.row[%field]); +} + +testExec("editor/main.cs"); +schedule(2000, 0, "sStep1"); + +//----------------------------------------------------------------------------- +// Set Theme fills the cursor slots silently, and shows nothing for them. +//----------------------------------------------------------------------------- + +function sStep1() +{ + ProjectManager.setProjectFolder("cursorSlotsSmokeProject"); + GuiEditor.open(); + + // A window has three cursor slots; a frame set and a text edit have the + // others between them. + $sWindow = new GuiWindowCtrl(); + GuiEditor.rootGui.add($sWindow); + $sEdit = new GuiTextEditCtrl(); + GuiEditor.rootGui.add($sEdit); + + $sTheme = GuiEditor.themeLibrary.createTheme("CSSmoke"); + sCheck("theme created", isObject($sTheme)); + + GuiEditor.themeApplier.applyToBranch($sWindow, $sTheme, true); + GuiEditor.themeApplier.applyToBranch($sEdit, $sTheme, true); + GuiEditor.themeName = $sTheme.getName(); + + // Filled without being asked: the point is that a Gui wears ITS theme's + // cursors rather than whichever set the project installed globally. + sCheck("window took the theme's corner cursor", + $sWindow.nWSECursor $= $sTheme.getCursor("NWSE").getName()); + sCheck("window took the theme's horizontal cursor", + $sWindow.leftRightCursor $= $sTheme.getCursor("LeftRight").getName()); + sCheck("text edit took the theme's text cursor", + $sEdit.editCursor $= $sTheme.getCursor("Edit").getName()); + + // ...and says nothing about it, because there is nothing to choose. + %pane = sRebind($sWindow); + sCheck("no Variants section with one cursor per category", + !isObject(%pane.panel["Variants"])); + sCheck("no corner cursor row", !sHasRow(%pane, "nWSECursor")); + sCheck("no horizontal cursor row", !sHasRow(%pane, "leftRightCursor")); + + schedule(200, 0, "sStep2"); +} + +//----------------------------------------------------------------------------- +// A second cursor in one category: that slot, and only that slot, appears. +//----------------------------------------------------------------------------- + +function sStep2() +{ + $sExtra = GuiEditor.themeLibrary.createExtraCursor($sTheme, "NWSE"); + sCheck("extra NWSE cursor created", isObject($sExtra)); + sCheck("theme reports two NWSE cursors", + getWordCount($sTheme.getCursors("NWSE")) == 2); + + %pane = sRebind($sWindow); + sCheck("Variants section appeared", isObject(%pane.panel["Variants"])); + sCheck("corner cursor got a row", sHasRow(%pane, "nWSECursor")); + + // Only the category with a choice in it. + sCheck("horizontal cursor row still hidden", !sHasRow(%pane, "leftRightCursor")); + sCheck("vertical cursor row still hidden", !sHasRow(%pane, "upDownCursor")); + + %row = %pane.row["nWSECursor"]; + sCheck("row offers the default member", + %row.editor.findItemText($sTheme.getCursor("NWSE").getName(), false) >= 0); + sCheck("row offers the extra member", + %row.editor.findItemText($sExtra.getName(), false) >= 0); + sCheck("row shows what the control wears", + %row.getValue() $= $sTheme.getCursor("NWSE").getName()); + + // A text edit has no NWSE slot at all, so its pane is unaffected. + %editPane = sRebind($sEdit); + sCheck("text edit shows no cursor row", !sHasRow(%editPane, "editCursor")); + + schedule(200, 0, "sStep3"); +} + +//----------------------------------------------------------------------------- +// Choosing the extra writes it, and detaching the theme leaves nothing dangling. +//----------------------------------------------------------------------------- + +function sStep3() +{ + %pane = sRebind($sWindow); + %row = %pane.row["nWSECursor"]; + + %row.applyValue($sExtra.getName()); + %pane.onProfileRowCommit(%row); + sCheck("choosing the extra wrote it to the control", + $sWindow.nWSECursor $= $sExtra.getName()); + + // Re-applying the theme leaves a deliberate second choice alone: it already + // belongs to this theme, which is the whole test for "someone chose this". + GuiEditor.themeApplier.applyToBranch($sWindow, $sTheme, true); + sCheck("re-applying the theme keeps the chosen cursor", + $sWindow.nWSECursor $= $sExtra.getName()); + + // Detach moves every slot off the doomed theme, onto the canonical name for + // that slot -- not an empty string, which through TypeGuiCursor would land + // on DefaultCursor and put an arrow on a resize edge. + // + // Give two of the three names something to resolve to first, the way a + // project's AppCore does at boot. The editor on its own registers no + // cursors under them (its theme builds them anonymously), and the third is + // deliberately left unregistered to cover the other branch. + // + // editorMode off while naming: in editor mode assignName stashes a name + // instead of registering it, which is right for a control being authored + // and wrong for these. + editorMode(false); + $sCorner = new GuiCursor(); + $sCorner.setName("NWSECursor"); + $sHorizontal = new GuiCursor(); + $sHorizontal.setName("LeftRightCursor"); + editorMode(true); + + GuiEditor.detachTheme($sTheme, 0); + + sCheck("corner cursor detached to its canonical name", + $sWindow.nWSECursor $= "NWSECursor"); + sCheck("horizontal cursor detached to its canonical name", + $sWindow.leftRightCursor $= "LeftRightCursor"); + sCheck("nothing was left pointing at the theme", + $sWindow.nWSECursor !$= $sExtra.getName()); + + // The third had no canonical cursor to land on, so the field cleared. That + // is safe rather than broken: an empty cursor slot is what every untouched + // control has, and the engine re-resolves it by name the next time the + // pointer is over the control (guiTextEditCtrl.cc getCursor). + sCheck("a slot with no canonical cursor cleared instead of dangling", + $sEdit.editCursor $= ""); + + $sCorner.delete(); + $sHorizontal.delete(); + + echo("CSSMOKE DONE"); + schedule(300, 0, "quit"); +} diff --git a/tests/smoke/planetX.cs b/tests/smoke/planetX.cs index 983074196..1ec7c1ee9 100644 --- a/tests/smoke/planetX.cs +++ b/tests/smoke/planetX.cs @@ -30,6 +30,38 @@ function planetXSmoke() smokeCheck("AppCore no longer creates GuiWindowProfile", !isObject(GuiWindowProfile)); smokeCheck("AppCore still creates its cursors", isObject(DefaultCursor) && isObject(EditCursor)); + // Cursors come from the theme now. The canonical names still answer -- the + // engine hard-codes them for any control that names no cursor of its own -- + // but what they hold is a copy of the theme's member, art and tint included. + smokeCheck("the theme owns a cursor per category", + isObject(PlanetXDefaultCursor) && isObject(PlanetXEditCursor) && isObject(PlanetXNWSECursor)); + // Either the recipe's tint or one the project chose. Asserting it always + // equals colorForeground would forbid the override the pane exists to make; + // that the tint tracks the palette when NOT overridden is covered by + // GuiProfileThemeTests and smoke/cursorPane. + smokeCheck("cursor tint is the palette's, or a deliberate override", + PlanetX.isFieldOverridden(PlanetXDefaultCursor, "color") || + PlanetXDefaultCursor.color $= PlanetX.colorForeground); + smokeCheck("theme cursors have their own art", + strstr(PlanetXDefaultCursor.bitmapName, "themes/cursors/PlanetX") >= 0); + smokeCheck("the art was seeded beside the theme", + isDirectory(testRoot("PlanetX/themes/cursors/PlanetX"))); + smokeCheck("the canonical names carry the theme's cursors", + DefaultCursor.bitmapName $= PlanetXDefaultCursor.bitmapName && + DefaultCursor.hotSpot $= PlanetXDefaultCursor.hotSpot && + DefaultCursor.color $= PlanetXDefaultCursor.color); + smokeCheck("an installed copy is not mistaken for a theme member", DefaultCursor.category $= ""); + + // Counted relative to what the project's own theme file holds, which is the + // user's to change: PlanetX already ships an extra Default cursor of its own. + %before = getWordCount(PlanetX.getCursors("Default")); + %extra = PlanetX.createCursor("Default"); + %extra.bitmapName = "unitTestArt/other.png"; + smokeCheck("a category can hold another cursor", getWordCount(PlanetX.getCursors("Default")) == (%before + 1)); + PlanetX.removeCursor(%extra); + smokeCheck("an extra cursor can be removed again", getWordCount(PlanetX.getCursors("Default")) == %before); + smokeCheck("the project's own extra cursor survived", %before >= 1 && isObject(PlanetXDefaultCursor2)); + smokeCheck("the PlanetX theme loaded", isObject(PlanetX)); smokeCheck("theme button profile", isObject(PlanetXButtonProfile)); smokeCheck("theme window profile", isObject(PlanetXWindowProfile)); From f42c9941fd9f778176efe1ddedfa8b6842805313 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Tue, 4 Aug 2026 22:30:54 -0400 Subject: [PATCH 39/55] Gui Editor: ask before deleting a cursor's image, and only when it is ours Removing an extra cursor deleted its image file as a side effect. Right answer nine times in ten -- the art was made for that cursor and nothing else will use it -- but silently, and wrong in two cases that matter. An extra can be pointed at art the user chose with the Find button, which this editor did not put there and has no business removing. And it can be pointed at the category's own stock file, which the default member is still using, so deleting it would take the working cursor's picture with it. Nothing checked either. So the library no longer deletes anything on its own. removeExtraCursor hands back the path it orphaned, or "" when there is nothing worth asking about, and the dialog asks. A path qualifies only if it is inside the theme's own cursor folder, no remaining cursor names it, and there is really a file there. Confirming dooms it at the next save, like every other file this editor removes -- so Cancel keeps it, exactly as Cancel keeps a deleted theme. Writing the test for that turned up a worse bug behind it. Renaming a theme moved the cursor folder by copying the files the CATEGORY TABLE names, then repointing every member -- but an extra's art is named after the member, not the category. So a rename repointed each extra into the new folder and left its file in the old one, and the cursor came back pointing at nothing. The move is now driven by what the members actually name, plus the stock file for each category so a member with no art still finds one to be filled from at the next restamp. Art from outside the folder is left where it is; it is not the theme's to move. Also dropped a check in the PlanetX suite that asserted the project's own extra cursor exists. That was the suite testing its author's content rather than the engine's behaviour, and it failed the moment he deleted the cursor, which was his to delete. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qi2pZ9UzF5rsLCNVErpzwt --- .../scripts/GuiProfileEditorDialog.cs | 29 +++- .../scripts/GuiProfileEditorLibrary.cs | 131 +++++++++++++++--- tests/smoke/cursorPane.cs | 40 +++++- tests/smoke/planetX.cs | 1 - 4 files changed, 174 insertions(+), 27 deletions(-) diff --git a/editor/GuiEditor/scripts/GuiProfileEditorDialog.cs b/editor/GuiEditor/scripts/GuiProfileEditorDialog.cs index 698642be9..192743494 100644 --- a/editor/GuiEditor/scripts/GuiProfileEditorDialog.cs +++ b/editor/GuiEditor/scripts/GuiProfileEditorDialog.cs @@ -1063,9 +1063,10 @@ class = "GuiProfileEditorBorderSetter"; %this.currentRoot = ""; %this.currentMember = ""; + %orphanedArt = ""; if(%isCursor) { - %this.library.removeExtraCursor(%theme, %member); + %orphanedArt = %this.library.removeExtraCursor(%theme, %member); } else { @@ -1074,6 +1075,32 @@ class = "GuiProfileEditorBorderSetter"; %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) diff --git a/editor/GuiEditor/scripts/GuiProfileEditorLibrary.cs b/editor/GuiEditor/scripts/GuiProfileEditorLibrary.cs index a815630af..90f288d17 100644 --- a/editor/GuiEditor/scripts/GuiProfileEditorLibrary.cs +++ b/editor/GuiEditor/scripts/GuiProfileEditorLibrary.cs @@ -208,24 +208,27 @@ } createPath(%target @ "/"); - %oldPrefix = makeRelativePath(%source, getMainDotCsDir()); %newPrefix = makeRelativePath(%target, getMainDotCsDir()); %categories = %theme.getCursorCategoryNames(); for(%i = 0; %i < getWordCount(%categories); %i++) { %category = getWord(%categories, %i); - %file = %theme.getCursorStockFile(%category); - if(%file !$= "") - { - pathCopy(pathConcat(%source, %file), pathConcat(%target, %file)); - %this.doomFile(pathConcat(%source, %file)); - } + // 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.repointCursorArt(getWord(%members, %m), %oldPrefix, %newPrefix); + %this.moveCursorArt(getWord(%members, %m), %source, %target, %newPrefix); } } @@ -233,17 +236,37 @@ return true; } -// Move one cursor's bitmap from the old folder to the new, if that is where it -// lives. Compared as relative paths because that is the form a theme stores. -function GuiProfileEditorLibrary::repointCursorArt(%this, %cursor, %oldPrefix, %newPrefix) +// 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 $= "" || strpos(%current, %oldPrefix) != 0) + if(%current $= "" || filePath(makeFullPath(%current, getMainDotCsDir())) !$= %source) { return; } - %cursor.bitmapName = %newPrefix @ getSubStr(%current, strlen(%oldPrefix), strlen(%current) - strlen(%oldPrefix)); + %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 @@ -1231,23 +1254,20 @@ 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()]; - - // Its art was made for it alone, so it goes too - but only on save, like - // every other file this editor drops. %bitmap = %cursor.bitmapName; %removed = %theme.removeCursor(%cursor); if(!%removed) { - return false; - } - - if(%bitmap !$= "") - { - %this.doomFile(makeFullPath(%bitmap, getMainDotCsDir())); + return ""; } if(isObject(%leaf)) @@ -1256,7 +1276,72 @@ } %this.cursorExtraProxy[%cursor.getId()] = ""; %this.markDirty(%theme); - return true; + + 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) diff --git a/tests/smoke/cursorPane.cs b/tests/smoke/cursorPane.cs index cc59a7097..0a66eb533 100644 --- a/tests/smoke/cursorPane.cs +++ b/tests/smoke/cursorPane.cs @@ -213,9 +213,45 @@ function cStep3() cCheck("the moved art file exists", isFile(makeFullPath(%theme.getCursor("Default").bitmapName, getMainDotCsDir()))); - // --- Removing the extra puts the category back to one. --- - %d.library.removeExtraCursor(%theme, %extra); + // An extra's art is named after the member rather than the category, so a + // rename that moved only the stock files left it behind while its cursor + // pointed hopefully into the new folder. + cCheck("an extra's art followed the rename too", + strstr(%extra.bitmapName, "cursors/CurSmokeTwo") >= 0); + cCheck("and the extra's file is really there", + isFile(makeFullPath(%extra.bitmapName, getMainDotCsDir()))); + + // --- Removing the extra puts the category back to one, and offers to take + // its picture with it rather than doing so behind the user's back. --- + %art = %extra.bitmapName; + %artFile = makeFullPath(%art, getMainDotCsDir()); + %orphaned = %d.library.removeExtraCursor(%theme, %extra); cCheck("extra removed", getWordCount(%theme.getCursors("Default")) == 1); + cCheck("its now-unused art is offered up, not deleted", %orphaned $= %artFile); + cCheck("the file is still there until someone says otherwise", isFile(%artFile)); + + // Saying yes only dooms it -- Cancel would still keep it, like every other + // file this editor removes. + %d.doomedCursorArt = %orphaned; + %d.doDeleteCursorArt(); + cCheck("confirming dooms the file", %d.library.isDirty()); + + // --- Art that is still in use is never offered. This is the case that + // would really have hurt: an extra pointed at the category's stock art, + // which the default member is also using. --- + %shared = %d.library.createExtraCursor(%theme, "Edit"); + %shared.bitmapName = %theme.getCursor("Edit").bitmapName; + %sharedFile = makeFullPath(%shared.bitmapName, getMainDotCsDir()); + %orphaned = %d.library.removeExtraCursor(%theme, %shared); + cCheck("shared art is not offered for deletion", %orphaned $= ""); + cCheck("and the file the default still uses survives", isFile(%sharedFile)); + + // --- Nor is art the user chose from somewhere else. --- + %outside = %d.library.createExtraCursor(%theme, "Move"); + %outside.bitmapName = "editor/EditorCore/Themes/BaseTheme/images/cursors/move.png"; + cCheck("art from outside the theme's folder is left alone", + %d.library.removeExtraCursor(%theme, %outside) $= ""); + cCheck("a default cursor cannot be removed", !%theme.removeCursor(%theme.getCursor("Default"))); diff --git a/tests/smoke/planetX.cs b/tests/smoke/planetX.cs index 1ec7c1ee9..92cde54bd 100644 --- a/tests/smoke/planetX.cs +++ b/tests/smoke/planetX.cs @@ -60,7 +60,6 @@ function planetXSmoke() smokeCheck("a category can hold another cursor", getWordCount(PlanetX.getCursors("Default")) == (%before + 1)); PlanetX.removeCursor(%extra); smokeCheck("an extra cursor can be removed again", getWordCount(PlanetX.getCursors("Default")) == %before); - smokeCheck("the project's own extra cursor survived", %before >= 1 && isObject(PlanetXDefaultCursor2)); smokeCheck("the PlanetX theme loaded", isObject(PlanetX)); smokeCheck("theme button profile", isObject(PlanetXButtonProfile)); From c4fa94702b1e514b871092f154e84551a34fb7e7 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Tue, 4 Aug 2026 22:51:26 -0400 Subject: [PATCH 40/55] PlanetX: the theme's cursors, and the art that goes with them Saved from the editor. Seven cursors, each aimed by hand in the hot-spot pane rather than guessed, and all seven tinted to the theme's accent green -- which is an override, so they stay that colour if the palette moves under them. The art travels with the theme instead of being referenced out of AppCore. That is the whole point of a per-theme folder: a second theme in this project can carry cursors that look nothing like these without one overwriting the other's files. The seven files are copies of the stock grayscale set, which is what lets the tint colour them. The placement values here are also the ones now baked into the engine's category table as the defaults for every new theme, so a theme created from scratch starts where this one ended up rather than where it started. Two things in the diff that are not cursors. cursorDirectory is new on the theme, naming the folder above. PlanetXSelectedInsetBorder appears because the border category existed but had never been written to this file; a save materialises every member. And two category values case-fold on write -- WindowButton to windowButton, FrameSet to frameSet -- which is the long-standing StringTable case-insensitivity quirk, cosmetic and unrelated to any of this. The baked font caches sitting beside the theme are left out: those are a test run's byproduct, not part of the project. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qi2pZ9UzF5rsLCNVErpzwt --- PlanetX/themes/PlanetX.taml | 66 +++++++++++++++++- PlanetX/themes/cursors/PlanetX/NESW.png | Bin 0 -> 3092 bytes PlanetX/themes/cursors/PlanetX/NWSE.png | Bin 0 -> 3101 bytes .../themes/cursors/PlanetX/defaultCursor.png | Bin 0 -> 1659 bytes PlanetX/themes/cursors/PlanetX/ibeam.png | Bin 0 -> 1836 bytes PlanetX/themes/cursors/PlanetX/leftRight.png | Bin 0 -> 3098 bytes PlanetX/themes/cursors/PlanetX/move.png | Bin 0 -> 3232 bytes PlanetX/themes/cursors/PlanetX/upDown.png | Bin 0 -> 3146 bytes 8 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 PlanetX/themes/cursors/PlanetX/NESW.png create mode 100644 PlanetX/themes/cursors/PlanetX/NWSE.png create mode 100644 PlanetX/themes/cursors/PlanetX/defaultCursor.png create mode 100644 PlanetX/themes/cursors/PlanetX/ibeam.png create mode 100644 PlanetX/themes/cursors/PlanetX/leftRight.png create mode 100644 PlanetX/themes/cursors/PlanetX/move.png create mode 100644 PlanetX/themes/cursors/PlanetX/upDown.png diff --git a/PlanetX/themes/PlanetX.taml b/PlanetX/themes/PlanetX.taml index 6e208af37..6d8a06da3 100644 --- a/PlanetX/themes/PlanetX.taml +++ b/PlanetX/themes/PlanetX.taml @@ -11,7 +11,8 @@ colorAccent="33 191 132 255" colorHighlight="234 72 72 255" colorWarning="166 38 70 255" - borderSize="2"> + borderSize="2" + cursorDirectory="PlanetX/themes/cursors/PlanetX"> @@ -57,6 +58,65 @@ + + + + + + + + @@ -121,7 +181,7 @@ category="WindowContent" /> + category="windowButton" /> @@ -148,7 +208,7 @@ category="TreeView" /> + category="frameSet" /> diff --git a/PlanetX/themes/cursors/PlanetX/NESW.png b/PlanetX/themes/cursors/PlanetX/NESW.png new file mode 100644 index 0000000000000000000000000000000000000000..2f73696c83e32ff6d2d51404f9d28e926138608c GIT binary patch literal 3092 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 i8n+r+U1j?lcl87Io&uQXv>*on0000s#k literal 0 HcmV?d00001 diff --git a/PlanetX/themes/cursors/PlanetX/NWSE.png b/PlanetX/themes/cursors/PlanetX/NWSE.png new file mode 100644 index 0000000000000000000000000000000000000000..c952a3e455666d69ec70f158090daaaac5f25a17 GIT binary patch literal 3101 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 literal 0 HcmV?d00001 diff --git a/PlanetX/themes/cursors/PlanetX/defaultCursor.png b/PlanetX/themes/cursors/PlanetX/defaultCursor.png new file mode 100644 index 0000000000000000000000000000000000000000..a0d1ab9de745152aceaa56b54d2bfbc3c2b68a6a GIT binary patch literal 1659 zcmbVNOKjXk7&bywZQ2~7Qh_*Gu0Z8sd&av?>`_g-#0go1Lz-^ez=e*-v%6N-9%DP% zO{5-*5K;sZ7Y=|zsf6Hwc$}f2B3=hLpsF`cJroWc+TOT9fA7PDDrqBD>-CKN{onui z{`q%fVgA|iu}8)vNgA)uxr@aeu8F;S;CIhIk8Q$jUo!V%CQ17b7T0~!dz%kS(gT;m z#!|lIJx`d3G#Ut>YrQA|v?Lup)=MZ`=DF`Ld(b3WxHNw;)f8l*>~Z zRvAJ_p0YI`H7%dpPss){u#OEKSt_;(#>6n>;YR^?Y0xB#?(EPR>>Q*Da`ace!ww_n|*fy3Z zDqUpx%qpZYEXcO#tg)7GeUWEkC1reO6~a7RWRsBTl+#?K4Iw%sr4~jZ%LZgEA2}1o zf#_zBZ!zF5&AFq1YDh(ss!?qqlNj)=K8Y|vXaMy@5H{CJsG*{&ip&N^g}~+{Z2yHq z&jd85_XG!wG({RwNH~mWi|a|;QslCXWLk7Y3XUPVNpIG3FUql5Xy?~b`(~~ z$^&KiINYWJyUSoe+ii0oy^GufSSFfzm!`bdf@n%B-CEn~j#JN4n;2Yf*p7&Zr%Qdl??qzEKrxg1)lv!3z#TXCyoH)&R4L$qC~;nvxi@vfD%$JkDG z6Ct(gjS304a6p`qKmu{+0H_Bz8!MN#UVt#%LIm*9ErQVssUd-H=mc)gfzy_YM>OIOP0Ipy;&RurZ7an#=}cDwKR zOvVk$I_os1=?C^P~GkBs4bZZ)ox(KFt$|3 zYY^MCJbXhnuz_`A=-5Mq_?F|Frn>lPU@haJ-)nC!Y{4whMn#eOx;~jq8k1&2W<#BL zo~L6&Hw**_lpiJqouVYax@2gJoMlm3L^4rJMmmuDMWBJwxeIYx(I)vKOb{@AN>iOQ zaOu(v$XSI;_p|ZLIA^*Ti&!K@4p>rQ(~&G>K9c89SIhr00HN)6D;sBOiQ~#dUThqI z8;gLPiO&0nsnB~Om-`tL8wVisYUxetw=zKsnf0X{FB7%1l&l&?gQ(Z;MhTab{Q9X4 zqD>1CXpl96u!)G#pJ%OU!PLii4(&=Fg@+Z?K$sxX>=WB3u5Y;rTPIK`8BPoOR4`|L zD6^P?!%<9!LQj*SrdDC}TXHNjFbwLNr@wbvt({DU(HIu;-tBd@v(d7NXWIyToXPEW z{Z5h>G-0CC4m1c(BZ|0hb0(+(HAS%PBCBaS$n`>sT!+}gp{8NsaJj!N*?w69<^Fj$ zaLIt>*XM+dHrT-)KOX_ypI0}CXW$t+5lN4T|QdpC=qA?f%uYpIF@ph;b4)HA&*I^#UUn2vsPoaBR=?Q(tj_(dsMK?~P;p1RU+Gb2rR`HzwrrrW zvQ`Dx7N4Utvh_@HS?r&T1dv`(!*lDo42ucP#M%%t`_vxM&l8{1gNvmJkn|bvdl=2$)p2j7WewgFED#-&cNx z`=`}uul28e%G~s~AMd|?{MVBw_iLQg)W2VST(8&feRQMNtN)BUpIpbs4~~EO`ayPO r^YxeCdj6NMUieZOK6<$6Jz7zM-OnEX@k(o0-e;YS?e@3p_a6QOKRiD7 literal 0 HcmV?d00001 diff --git a/PlanetX/themes/cursors/PlanetX/leftRight.png b/PlanetX/themes/cursors/PlanetX/leftRight.png new file mode 100644 index 0000000000000000000000000000000000000000..c29b7a9f8d05a32fb83de2af5b8d2cfe571a2daf GIT binary patch literal 3098 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 literal 0 HcmV?d00001 diff --git a/PlanetX/themes/cursors/PlanetX/move.png b/PlanetX/themes/cursors/PlanetX/move.png new file mode 100644 index 0000000000000000000000000000000000000000..70f9cd5404b8a3978810b50a391c2550e2302bcc GIT binary patch literal 3232 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 k Date: Wed, 5 Aug 2026 13:54:15 -0400 Subject: [PATCH 41/55] PlanetX: a name is already a namespace, and a sound that was really a particle Opening the demo wrote two lines to the console before the title screen came up, and both were real. The first was Namespace::classLinkTo refusing to change the parent linkage of PlanetXUpgrades from ScriptObject to PlanetXUpgrades. The upgrade catalog was built as new ScriptObject(PlanetXUpgrades) { class = "PlanetXUpgrades"; } -- named after its own class. linkNamespaces links the class to the C++ class and then links the object name to the class, and those are one namespace, so it asked that namespace to become its own parent. Nothing broke: the failed link is ignored and the object ends up wearing exactly the namespace it wanted either way. But it fired on every boot, and its mirror image out of unlinkNamespaces on every exit. The fix is to delete the class. A name IS a namespace -- linkNamespaces links the object name whether or not a class is there -- so the second word bought nothing. ThemeManager in EditorCore has always been written this way. Only an unnamed object needs a class, which is why the settings and screen singletons two lines away keep theirs. The engine still has something to say about it, just not that. A namespace cannot be its own parent and has no need to be, so classLinkTo now treats the self-link as the no-op it is -- uncounted, because a count here would want an unlink that never comes -- and warns in terms someone can act on rather than reporting a failure that did not happen. Once, where the repeat is written, not again at teardown. The error that guard actually exists for, one namespace handed two different parents, is untouched. The second line was an asset id collision. sound/playerDeath.audio.taml and particles/playerDeath.particle.taml both declared AssetName "playerDeath", and a module's ids are one namespace across every asset family. The particle is scanned first, so the sound was refused -- which means the death sound has never played, because Audio.PlaySound was being handed a ParticleAsset. The sound is playerDeathBurst now, named for what it plays over. This module had already solved the same collision once: sound/steam.audio.taml has held AssetName "steamHiss" beside the steam particle all along, so the sound is what gets qualified and the particle keeps the plain name. File names stay as they are, paired with the .wav. playerDeath was the only duplicate AssetName in the module. Two sets of tests. namespaceLinkTests covers the engine half: the warning fires once, names the namespace, and stays out of teardown; the parent reference count survives two full rounds of link and unlink, which is what a mismatched pair would quietly break; and two parents for one class is still an error. The PlanetX suite gains five checks -- that the catalog's onAdd ran and its methods resolve through the name alone, and that each of the two ids answers with the right kind of asset. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qi2pZ9UzF5rsLCNVErpzwt --- PlanetX/PlanetXGame/game.cs | 10 +- .../PlanetXGame/sound/playerDeath.audio.taml | 2 +- cmake/EngineSources.cmake | 1 + engine/source/console/consoleNamespace.cc | 22 ++ .../testing/tests/namespaceLinkTests.cc | 211 ++++++++++++++++++ tests/smoke/planetX.cs | 15 ++ 6 files changed, 256 insertions(+), 5 deletions(-) create mode 100644 engine/source/testing/tests/namespaceLinkTests.cc diff --git a/PlanetX/PlanetXGame/game.cs b/PlanetX/PlanetXGame/game.cs index df23d9581..2d223f144 100644 --- a/PlanetX/PlanetXGame/game.cs +++ b/PlanetX/PlanetXGame/game.cs @@ -49,8 +49,10 @@ // 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. - new ScriptObject(PlanetXUpgrades) { class = "PlanetXUpgrades"; }; + // 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; @@ -362,7 +364,7 @@ class = "PlanetXUpgradeScreen"; } // Co-op: take this player out of play. - Audio.PlaySound("PlanetXGame:playerDeath"); + Audio.PlaySound("PlanetXGame:playerDeathBurst"); %player.playDeathFx(); %player.goDown(); @@ -392,7 +394,7 @@ class = "PlanetXUpgradeScreen"; // ghost bolts off-screen during the hold before the dialog. if (isObject(%player) && !%player.downed) { - Audio.PlaySound("PlanetXGame:playerDeath"); + Audio.PlaySound("PlanetXGame:playerDeathBurst"); %player.playDeathFx(); %player.stopFiring(); %player.setLinearVelocity(0, 0); diff --git a/PlanetX/PlanetXGame/sound/playerDeath.audio.taml b/PlanetX/PlanetXGame/sound/playerDeath.audio.taml index 136acbc1f..e7238a62b 100644 --- a/PlanetX/PlanetXGame/sound/playerDeath.audio.taml +++ b/PlanetX/PlanetXGame/sound/playerDeath.audio.taml @@ -1,4 +1,4 @@ diff --git a/cmake/EngineSources.cmake b/cmake/EngineSources.cmake index 8122d199f..750a14ba4 100644 --- a/cmake/EngineSources.cmake +++ b/cmake/EngineSources.cmake @@ -345,6 +345,7 @@ set(TORQUE_ENGINE_SOURCES ${TORQUE_SRC}/testing/tests/guiScrollLayoutTests.cc ${TORQUE_SRC}/testing/tests/guiTextEditTests.cc ${TORQUE_SRC}/testing/tests/guiTreeRowLayoutTests.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 diff --git a/engine/source/console/consoleNamespace.cc b/engine/source/console/consoleNamespace.cc index 325ef62e6..396c99fe4 100755 --- a/engine/source/console/consoleNamespace.cc +++ b/engine/source/console/consoleNamespace.cc @@ -150,6 +150,11 @@ bool Namespace::canTabComplete(const char *prevText, const char *bestMatch, cons bool Namespace::unlinkClass(Namespace* parent) { + // Giving back a self-link (see classLinkTo). Nothing was linked and nothing + // was counted, so there is nothing here to undo. + if(parent == this) + return true; + Namespace* walk = this; while(walk->mParent && walk->mParent->mName == mName) @@ -180,6 +185,23 @@ bool Namespace::unlinkClass(Namespace* parent) bool Namespace::classLinkTo(Namespace* parent) { + // A namespace already is itself, so linking it to itself cannot change what + // it inherits: it is a no-op, not the parent change it looks like. Objects + // ask for it whenever they are named after their own class - the singleton + // "new ScriptObject(Foo) { class = "Foo"; }" links Foo to the C++ class and + // then asks to link its name, Foo, to Foo. Counting it here would need an + // unlink that never comes, so it is not counted either. + // + // It is still worth saying, because the object said one thing twice and the + // second one is doing nothing. Once, here, where the repeat is made - not + // again from unlinkClass when it is given back. + if(parent == this) + { + Con::warnf(ConsoleLogEntry::General, "Namespace::classLinkTo - %s cannot be its own parent, and has no need to be: a name is already a namespace. An object whose name and class are both '%s' wants only the name.", + mName, mName); + return true; + } + Namespace* walk = this; while(walk->mParent && walk->mParent->mName == mName) diff --git a/engine/source/testing/tests/namespaceLinkTests.cc b/engine/source/testing/tests/namespaceLinkTests.cc new file mode 100644 index 000000000..556d0815d --- /dev/null +++ b/engine/source/testing/tests/namespaceLinkTests.cc @@ -0,0 +1,211 @@ +//----------------------------------------------------------------------------- +// 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. +//----------------------------------------------------------------------------- + +// We don't want tests in a shipping version. +#ifndef TORQUE_SHIPPING + +#ifndef _UNIT_TESTING_H_ +#include "testing/unitTesting.h" +#endif + +#ifndef _SIMBASE_H_ +#include "sim/simBase.h" +#endif + +#ifndef _CONSOLE_H_ +#include "console/console.h" +#endif + +#ifndef _CONSOLENAMESPACE_H_ +#include "console/consoleNamespace.h" +#endif + +//----------------------------------------------------------------------------- +// Namespace linkage for an object named after its own class. +// +// A script singleton is written as +// +// new ScriptObject(PlanetXUpgrades) { class = "PlanetXUpgrades"; }; +// +// so that every file can reach it by name and its methods can be written as +// PlanetXUpgrades::foo. SimObject::linkNamespaces then links the class to the +// C++ class, and a moment later links the OBJECT NAME to the class - but those +// are one namespace, so it asks that namespace to become its own parent. +// +// Self-parenting is a no-op: the namespace is already the one the object wears. +// It used to be an error instead ("cannot change namespace parent linkage of +// PlanetXUpgrades from ScriptObject to PlanetXUpgrades"), with the mirror image +// at teardown - a failure reported for something that neither failed nor did +// anything. It is a warning now, because the object did say one word twice and +// the second one buys nothing. +// +// These tests hold that warning to what it is worth - said once, where the +// repeat is written, naming the namespace - hold the parent reference count +// balanced across the no-op, which is what a mismatched link/unlink pair would +// quietly break, and hold onto the error the guard really exists for: one +// namespace being given two different parents. +//----------------------------------------------------------------------------- + +static bool sCapturing = false; +static U32 sErrorCount = 0; +static U32 sWarningCount = 0; +static char sFirstError[1024]; +static char sFirstWarning[1024]; + +static void remember( char* buffer, U32 size, const char* line ) +{ + dStrncpy( buffer, line, size - 1 ); + buffer[size - 1] = '\0'; +} + +static void captureOutput( ConsoleLogEntry::Level level, const char* line ) +{ + if ( !sCapturing ) + return; + + if ( level == ConsoleLogEntry::Error ) + { + if ( sErrorCount == 0 ) + remember( sFirstError, sizeof(sFirstError), line ); + + sErrorCount++; + } + else if ( level == ConsoleLogEntry::Warning ) + { + if ( sWarningCount == 0 ) + remember( sFirstWarning, sizeof(sFirstWarning), line ); + + sWarningCount++; + } +} + +static void beginCapture() +{ + sErrorCount = 0; + sWarningCount = 0; + sFirstError[0] = '\0'; + sFirstWarning[0] = '\0'; + sCapturing = true; + Con::addConsumer( captureOutput ); +} + +static void endCapture() +{ + Con::removeConsumer( captureOutput ); + sCapturing = false; +} + +//----------------------------------------------------------------------------- +// The singleton named after its class. +//----------------------------------------------------------------------------- + +TEST( NamespaceLinkTests, ObjectNamedAfterItsClassIsWarnedNotFailed ) +{ + SimObject* singleton = new SimObject(); + singleton->setClassNamespace( "NsLinkTestSingleton" ); + + beginCapture(); + singleton->registerObject( "NsLinkTestSingleton" ); + endCapture(); + + ASSERT_EQ( sErrorCount, 0u ) + << "Nothing failed, so nothing should say it did: " << sFirstError; + ASSERT_EQ( sWarningCount, 1u ) + << "The redundant class is worth one warning - no more, and not none."; + ASSERT_TRUE( dStrstr( sFirstWarning, "NsLinkTestSingleton" ) != NULL ) + << "It has to name the namespace to be actionable, and said: " << sFirstWarning; + + ASSERT_TRUE( singleton->getNamespace() != NULL ); + ASSERT_STREQ( singleton->getNamespace()->mName, "NsLinkTestSingleton" ) + << "It still wears the namespace its script methods are written in."; + + beginCapture(); + singleton->deleteObject(); + endCapture(); + + ASSERT_EQ( sErrorCount, 0u ) + << "Deleting it said: " << sFirstError; + ASSERT_EQ( sWarningCount, 0u ) + << "Teardown repeats nothing - the warning belongs where the repeat is written."; +} + +// Link and unlink have to agree about whether the self-link counted, or the +// count drifts: too many unlinks and the class loses its parent while another +// object is still wearing it, too few and it never comes back at all. Two full +// rounds catch either one. +TEST( NamespaceLinkTests, SelfLinkLeavesTheParentReferenceCountBalanced ) +{ + StringTableEntry name = StringTable->insert( "NsLinkTestBalanced" ); + + for ( U32 round = 0; round < 2; round++ ) + { + SimObject* singleton = new SimObject(); + singleton->setClassNamespace( name ); + singleton->registerObject( name ); + + Namespace* linked = Namespace::find( name ); + ASSERT_TRUE( linked->mParent != NULL ) + << "Round " << round << ": the class namespace must inherit the C++ class."; + ASSERT_STREQ( linked->mParent->mName, "SimObject" ) + << "Round " << round << ": the self-link must not have displaced that parent."; + + singleton->deleteObject(); + } + + Namespace* ns = Namespace::find( name ); + ASSERT_EQ( ns->mRefCountToParent, 0u ) + << "Every link was given back."; + ASSERT_TRUE( ns->mParent == NULL ) + << "So the namespace is unlinked again."; +} + +//----------------------------------------------------------------------------- +// What the guard is really for. +//----------------------------------------------------------------------------- + +// Two objects giving one class namespace two different superclasses is a real +// ambiguity - whichever registered first silently decides what the other +// inherits - so it must still be reported. This test therefore writes one +// genuine error line to the console log; the NsLinkTest names in it say so. +TEST( NamespaceLinkTests, TwoDifferentParentsForOneClassIsStillAnError ) +{ + SimObject* first = new SimObject(); + first->setSuperClassNamespace( "NsLinkTestParentOne" ); + first->setClassNamespace( "NsLinkTestShared" ); + first->registerObject(); + + SimObject* second = new SimObject(); + second->setSuperClassNamespace( "NsLinkTestParentTwo" ); + second->setClassNamespace( "NsLinkTestShared" ); + + beginCapture(); + second->registerObject(); + endCapture(); + + ASSERT_EQ( sErrorCount, 1u ) + << "One namespace cannot have two parents, and saying so is the point of the guard."; + + second->deleteObject(); + first->deleteObject(); +} + +#endif // TORQUE_SHIPPING diff --git a/tests/smoke/planetX.cs b/tests/smoke/planetX.cs index 92cde54bd..a93e3d3a5 100644 --- a/tests/smoke/planetX.cs +++ b/tests/smoke/planetX.cs @@ -83,6 +83,21 @@ function planetXSmoke() smokeCheck("the title screen is up", isObject(PlanetXTitle)); smokeCheck("title wears a theme profile", PlanetXTitle.getFieldValue("Profile") $= "PlanetXEmptyProfile"); + // The upgrade catalog is named, not classed: a name IS a namespace, so onAdd + // and every method reach upgrades.cs through it. Saying the same word again as + // a class would only ask the namespace to become its own parent. + smokeCheck("the upgrade catalog is up", isObject(PlanetXUpgrades)); + smokeCheck("its onAdd ran through the name", getWordCount(PlanetXUpgrades.keys) > 0); + smokeCheck("its methods resolve by name", PlanetXUpgrades.isEligible("damage", 1)); + + // One module namespace holds every asset family, so a sound and a particle + // cannot both be "playerDeath" - whichever is scanned second is dropped, and + // the id then answers with the wrong kind of asset entirely. + smokeCheck("the death sound is an audio asset", + AssetDatabase.getAssetType("PlanetXGame:playerDeathBurst") $= "AudioAsset"); + smokeCheck("the death effect is still a particle asset", + AssetDatabase.getAssetType("PlanetXGame:playerDeath") $= "ParticleAsset"); + screenShot(testRoot("shots/planetXThemeSmoke.png"), "PNG"); schedule(1000, 0, "planetXSmokeDone"); } From 30884c52d529c76c621d1b6a55e4041ebdd864d9 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Thu, 6 Aug 2026 08:51:41 -0400 Subject: [PATCH 42/55] GuiImageButtonCtrl: a profile's imageAsset was already the answer The control existed to draw four named images - Normal, Hover, Down and Inactive - on a button. A GuiControlProfile's imageAsset is already indexed by control state, so a plain GuiButtonCtrl wearing a four frame strip does the same thing with a class less. VirtualKeyboard was its only user. Its thirty seven keys now wear four profiles over four strips: keyStates, spaceBarStates, closeStates, and keyLatched, which carries the pressed cap in every frame so caps lock reads as held down for as long as the lock is on. The six single state images the old control needed are gone. The palette loses an entry with it: thirty drawings now, and the spec, the three icon sheets and the control table all say so. tabBook asserted two frame literals either side of the Tab Page seam; it now asserts that Window sits immediately after Tab Page, which is what that test is actually for and survives the next removal. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01JDKWjuEF5Nuw791mpU7aPF --- cmake/EngineSources.cmake | 1 - .../2026-07-29-gui-control-icon-set-design.md | 43 ++- editor/GuiEditor/images/controlIcons128.png | Bin 45502 -> 43678 bytes editor/GuiEditor/images/controlIcons16.png | Bin 2775 -> 2657 bytes editor/GuiEditor/images/controlIcons64.png | Bin 22024 -> 21142 bytes .../scripts/GuiEditorControlIcons.cs | 3 +- .../GuiEditor/scripts/GuiEditorControlSpec.cs | 11 +- engine/source/2d/gui/guiImageButtonCtrl.cc | 292 ----------------- engine/source/2d/gui/guiImageButtonCtrl.h | 101 ------ .../gui/guiImageButtonCtrl_ScriptBindings.h | 74 ----- tests/smoke/inspectorSpec.cs | 4 - tests/smoke/palette.cs | 8 +- tests/smoke/tabBook.cs | 13 +- .../1/assets/images/closeBtn.png | Bin 31410 -> 0 bytes .../1/assets/images/closeBtn.taml | 3 - .../1/assets/images/closeStates.png | Bin 0 -> 17112 bytes .../1/assets/images/closeStates.taml | 7 + .../VirtualKeyboard/1/assets/images/kbbD.png | Bin 28746 -> 0 bytes .../VirtualKeyboard/1/assets/images/kbbD.taml | 3 - .../VirtualKeyboard/1/assets/images/kbbN.png | Bin 30944 -> 0 bytes .../VirtualKeyboard/1/assets/images/kbbN.taml | 3 - .../1/assets/images/keyLatched.png | Bin 0 -> 16230 bytes .../1/assets/images/keyLatched.taml | 7 + .../1/assets/images/keyStates.png | Bin 0 -> 29322 bytes .../1/assets/images/keyStates.taml | 7 + .../1/assets/images/spaceBar.png | Bin 35754 -> 0 bytes .../1/assets/images/spaceBar.taml | 3 - .../1/assets/images/spaceBarD.png | Bin 32504 -> 0 bytes .../1/assets/images/spaceBarD.taml | 3 - .../1/assets/images/spaceBarStates.png | Bin 0 -> 41069 bytes .../1/assets/images/spaceBarStates.taml | 7 + toybox/VirtualKeyboard/1/gui/keyboardGui.taml | 305 +++++++----------- toybox/VirtualKeyboard/1/main.cs | 80 +++-- 33 files changed, 226 insertions(+), 752 deletions(-) delete mode 100755 engine/source/2d/gui/guiImageButtonCtrl.cc delete mode 100755 engine/source/2d/gui/guiImageButtonCtrl.h delete mode 100755 engine/source/2d/gui/guiImageButtonCtrl_ScriptBindings.h delete mode 100644 toybox/VirtualKeyboard/1/assets/images/closeBtn.png delete mode 100644 toybox/VirtualKeyboard/1/assets/images/closeBtn.taml create mode 100644 toybox/VirtualKeyboard/1/assets/images/closeStates.png create mode 100644 toybox/VirtualKeyboard/1/assets/images/closeStates.taml delete mode 100644 toybox/VirtualKeyboard/1/assets/images/kbbD.png delete mode 100644 toybox/VirtualKeyboard/1/assets/images/kbbD.taml delete mode 100644 toybox/VirtualKeyboard/1/assets/images/kbbN.png delete mode 100644 toybox/VirtualKeyboard/1/assets/images/kbbN.taml create mode 100644 toybox/VirtualKeyboard/1/assets/images/keyLatched.png create mode 100644 toybox/VirtualKeyboard/1/assets/images/keyLatched.taml create mode 100644 toybox/VirtualKeyboard/1/assets/images/keyStates.png create mode 100644 toybox/VirtualKeyboard/1/assets/images/keyStates.taml delete mode 100644 toybox/VirtualKeyboard/1/assets/images/spaceBar.png delete mode 100644 toybox/VirtualKeyboard/1/assets/images/spaceBar.taml delete mode 100644 toybox/VirtualKeyboard/1/assets/images/spaceBarD.png delete mode 100644 toybox/VirtualKeyboard/1/assets/images/spaceBarD.taml create mode 100644 toybox/VirtualKeyboard/1/assets/images/spaceBarStates.png create mode 100644 toybox/VirtualKeyboard/1/assets/images/spaceBarStates.taml diff --git a/cmake/EngineSources.cmake b/cmake/EngineSources.cmake index 750a14ba4..eed2addfe 100644 --- a/cmake/EngineSources.cmake +++ b/cmake/EngineSources.cmake @@ -46,7 +46,6 @@ set(TORQUE_ENGINE_SOURCES ${TORQUE_SRC}/2d/experimental/composites/WaveComposite.cc # ---- 2d/gui ---- ${TORQUE_SRC}/2d/gui/SceneWindow.cc - ${TORQUE_SRC}/2d/gui/guiImageButtonCtrl.cc ${TORQUE_SRC}/2d/gui/guiSceneObjectCtrl.cc ${TORQUE_SRC}/2d/gui/guiSpriteCtrl.cc # ---- 2d/scene ---- diff --git a/docs/superpowers/specs/2026-07-29-gui-control-icon-set-design.md b/docs/superpowers/specs/2026-07-29-gui-control-icon-set-design.md index 91c4c1887..acb9e3d09 100644 --- a/docs/superpowers/specs/2026-07-29-gui-control-icon-set-design.md +++ b/docs/superpowers/specs/2026-07-29-gui-control-icon-set-design.md @@ -46,7 +46,7 @@ Each size is drawn at **8× supersample** and downsampled with Lanczos. Corners ### The 31 icons -Frame index is display order in an 8×4 sheet — 31 used, 1 spare. Grouped by what a person is looking for, not alphabetically. +Frame index is display order in an 8×4 sheet — 30 used, 2 spare. Grouped by what a person is looking for, not alphabetically. | # | key | icon | |---|---|---| @@ -60,27 +60,26 @@ Frame index is display order in an 8×4 sheet — 31 used, 1 spare. Grouped by w | 7 | `GuiRadioCtrl` | Ring with a dot, label bars right | | 8 | `GuiDropDownCtrl` | Field, divider, ▼ chevron | | 9 | `GuiColorPopupCtrl` | 3×2 swatch block + the same chevron | -| 10 | `GuiImageButtonCtrl` | Button with a small picture **on** it | -| 11 | `GuiTextEditCtrl` | Field, text bar, I-beam caret | -| 12 | `GuiTextEditSliderCtrl` | The same field with ▲▼ spinner | -| 13 | `GuiSliderCtrl` | Thin track, thumb, tick marks | -| 14 | `GuiProgressCtrl` | Pill filled from the left | -| 15 | `GuiColorPickerCtrl` | Blend field with a selector ring, banded hue strip | -| 16 | `GuiSpriteCtrl` | Closed frame, peak, sun — a photo | -| 17 | `SceneWindow` | Heavier frame, ground line, ball and crate — a game view | -| 18 | `GuiListBoxCtrl` | Framed rows, one inverted | -| 19 | `GuiTreeViewCtrl` | Spine, elbows, disclosure triangle | -| 20 | `GuiMenuBarCtrl` | Title bar with a menu dropped open | -| 21 | `GuiChainCtrl` | Three equal unframed bars, equal gaps | -| 22 | `GuiGridCtrl` | 3×3 cells with gutters | -| 23 | `GuiScrollCtrl` | Content with a track and thumb | -| 24 | `GuiFrameSetCtrl` | Panes split by thick dividers | -| 25 | `GuiPanelCtrl` | Captioned header, chevron right, body | -| 26 | `GuiExpandCtrl` | Bare header, centred double chevron, body with content | -| 27 | `GuiTabBookCtrl` | Three tabs, first merged into the page | -| 28 | `GuiTabPageCtrl` | One tab and its page, content drawn | -| 29 | `GuiWindowCtrl` | Title bar with three buttons, body | -| 30 | `GuiInputCtrl` | Keycap with a pointer over it — the one metaphor | +| 10 | `GuiTextEditCtrl` | Field, text bar, I-beam caret | +| 11 | `GuiTextEditSliderCtrl` | The same field with ▲▼ spinner | +| 12 | `GuiSliderCtrl` | Thin track, thumb, tick marks | +| 13 | `GuiProgressCtrl` | Pill filled from the left | +| 14 | `GuiColorPickerCtrl` | Blend field with a selector ring, banded hue strip | +| 15 | `GuiSpriteCtrl` | Closed frame, peak, sun — a photo | +| 16 | `SceneWindow` | Heavier frame, ground line, ball and crate — a game view | +| 17 | `GuiListBoxCtrl` | Framed rows, one inverted | +| 18 | `GuiTreeViewCtrl` | Spine, elbows, disclosure triangle | +| 19 | `GuiMenuBarCtrl` | Title bar with a menu dropped open | +| 20 | `GuiChainCtrl` | Three equal unframed bars, equal gaps | +| 21 | `GuiGridCtrl` | 3×3 cells with gutters | +| 22 | `GuiScrollCtrl` | Content with a track and thumb | +| 23 | `GuiFrameSetCtrl` | Panes split by thick dividers | +| 24 | `GuiPanelCtrl` | Captioned header, chevron right, body | +| 25 | `GuiExpandCtrl` | Bare header, centred double chevron, body with content | +| 26 | `GuiTabBookCtrl` | Three tabs, first merged into the page | +| 27 | `GuiTabPageCtrl` | One tab and its page, content drawn | +| 28 | `GuiWindowCtrl` | Title bar with three buttons, body | +| 29 | `GuiInputCtrl` | Keycap with a pointer over it — the one metaphor | ### The eight pairs that must stay apart diff --git a/editor/GuiEditor/images/controlIcons128.png b/editor/GuiEditor/images/controlIcons128.png index 6428004036e79094cb4bee14f9389db081b56e89..6db50d89e27696ee23c4a128dde654cd494598c2 100644 GIT binary patch literal 43678 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 delta 2767 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�{so@bz(5x^=yB1PK$sfHBm#3>2thA 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 H*@gcPjAf10 diff --git a/editor/GuiEditor/scripts/GuiEditorControlIcons.cs b/editor/GuiEditor/scripts/GuiEditorControlIcons.cs index cda97bacf..f0c41c8f5 100644 --- a/editor/GuiEditor/scripts/GuiEditorControlIcons.cs +++ b/editor/GuiEditor/scripts/GuiEditorControlIcons.cs @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- // The control palette's entries and their icons. GENERATED - do not hand-edit. // -// GuiEditor:controlIcons16, controlIcons64 and controlIcons128 are the same 31 +// 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 @@ -57,7 +57,6 @@ "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 - "GuiImageButtonCtrl" TAB "GuiImageButtonCtrl" TAB "" TAB "Input & Data" TAB "Image Button" 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 diff --git a/editor/GuiEditor/scripts/GuiEditorControlSpec.cs b/editor/GuiEditor/scripts/GuiEditorControlSpec.cs index 75a6426f6..04af549a6 100644 --- a/editor/GuiEditor/scripts/GuiEditorControlSpec.cs +++ b/editor/GuiEditor/scripts/GuiEditorControlSpec.cs @@ -77,13 +77,12 @@ // 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, and GuiImageButtonCtrl draws only its asset. + // 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 - "GuiImageButtonCtrl" TAB "none" TAB "command" TAB "" NL // A text edit reads mTextWrap (it is the multi-line control) and // getAlignmentType, but never mVAlignment or mTextExtend. @@ -171,9 +170,6 @@ %this.addSection("GuiColorPopupCtrl", "Popup", "Popup", "popupSize barHeight showAlphaBar swatchColumns showColorValues valueMode valueBoxHeight"); - %this.addSection("GuiImageButtonCtrl", "Images", "Images", - "HoverImage DownImage InactiveImage"); - %this.addSection("GuiTextEditCtrl", "Input", "Input", "maxLength password returnCausesTab sinkAllKeyEvents"); %this.addSection("GuiTextEditCtrl", "Commands", "Commands", @@ -288,7 +284,6 @@ %this.headerValues["GuiCheckBoxCtrl"] = "stateOn"; %this.headerValues["GuiRadioCtrl"] = "stateOn groupNum"; %this.headerValues["GuiColorPopupCtrl"] = "baseColor"; - %this.headerValues["GuiImageButtonCtrl"] = "NormalImage"; %this.headerValues["GuiTextEditCtrl"] = "inputMode"; %this.headerValues["GuiTextEditSliderCtrl"] = "inputMode range increment"; %this.headerValues["GuiSliderCtrl"] = "range value"; @@ -936,10 +931,6 @@ %this.label["fullSize"] = "Full Size"; %this.label["clampImage"] = "Clamp Image"; %this.label["tileImage"] = "Tile Image"; - %this.label["NormalImage"] = "Normal"; - %this.label["HoverImage"] = "Hover"; - %this.label["DownImage"] = "Down"; - %this.label["InactiveImage"] = "Inactive"; %this.label["baseColor"] = "Color"; %this.label["BaseColor"] = "Color"; %this.label["PickColor"] = "Picked Color"; diff --git a/engine/source/2d/gui/guiImageButtonCtrl.cc b/engine/source/2d/gui/guiImageButtonCtrl.cc deleted file mode 100755 index 9ffd158a9..000000000 --- a/engine/source/2d/gui/guiImageButtonCtrl.cc +++ /dev/null @@ -1,292 +0,0 @@ -//----------------------------------------------------------------------------- -// 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. -//----------------------------------------------------------------------------- - -#ifndef _GUIIMAGEBUTTON_H_ -#include "2d/gui/guiImageButtonCtrl.h" -#endif - -#ifndef _RENDER_PROXY_H_ -#include "2d/core/RenderProxy.h" -#endif - -#ifndef _DGL_H_ -#include "graphics/dgl.h" -#endif - -#ifndef _CONSOLE_H_ -#include "console/console.h" -#endif - -#ifndef _CONSOLETYPES_H_ -#include "console/consoleTypes.h" -#endif - -#ifndef _GUICANVAS_H_ -#include "gui/guiCanvas.h" -#endif - -#ifndef _H_GUIDEFAULTCONTROLRENDER_ -#include "gui/guiDefaultControlRender.h" -#endif - -/// Script bindings. -#include "guiImageButtonCtrl_ScriptBindings.h" - -//----------------------------------------------------------------------------- - -IMPLEMENT_CONOBJECT(GuiImageButtonCtrl); - -//----------------------------------------------------------------------------- - -GuiImageButtonCtrl::GuiImageButtonCtrl() : - mNormalAssetId( StringTable->EmptyString ), - mHoverAssetId( StringTable->EmptyString ), - mDownAssetId( StringTable->EmptyString ), - mInactiveAssetId( StringTable->EmptyString ) -{ - mBounds.extent.set(140, 30); -} - -//----------------------------------------------------------------------------- - -void GuiImageButtonCtrl::initPersistFields() -{ - // Call parent. - Parent::initPersistFields(); - - addProtectedField("NormalImage", TypeAssetId, Offset(mNormalAssetId, GuiImageButtonCtrl), &setNormalImage, &getNormalImage, "The image asset Id used for the normal button state."); - addProtectedField("HoverImage", TypeAssetId, Offset(mHoverAssetId, GuiImageButtonCtrl), &setHoverImage, &getHoverImage, "The image asset Id used for the hover button state."); - addProtectedField("DownImage", TypeAssetId, Offset(mDownAssetId, GuiImageButtonCtrl), &setDownImage, &getDownImage, "The image asset Id used for the Down button state."); - addProtectedField("InactiveImage", TypeAssetId, Offset(mInactiveAssetId, GuiImageButtonCtrl), &setInactiveImage, &getInactiveImage, "The image asset Id used for the inactive button state."); -} - -//----------------------------------------------------------------------------- - -bool GuiImageButtonCtrl::onWake() -{ - // Call parent. - if (!Parent::onWake()) - return false; - - // Is only the "normal" image specified? - if ( mNormalAssetId != StringTable->EmptyString && - mHoverAssetId == StringTable->EmptyString && - mDownAssetId == StringTable->EmptyString && - mInactiveAssetId == StringTable->EmptyString ) - { - // Yes, so use it for all states. - mImageNormalAsset = mNormalAssetId; - mImageHoverAsset = mNormalAssetId; - mImageDownAsset = mNormalAssetId; - mImageInactiveAsset = mNormalAssetId; - } - else - { - // No, so assign individual states. - mImageNormalAsset = mNormalAssetId; - mImageHoverAsset = mHoverAssetId; - mImageDownAsset = mDownAssetId; - mImageInactiveAsset = mInactiveAssetId; - } - - return true; -} - -//----------------------------------------------------------------------------- - -void GuiImageButtonCtrl::onSleep() -{ - // Clear assets. - mImageNormalAsset.clear(); - mImageHoverAsset.clear(); - mImageDownAsset.clear(); - mImageInactiveAsset.clear(); - - // Call parent. - Parent::onSleep(); -} - -//----------------------------------------------------------------------------- - -void GuiImageButtonCtrl::setNormalImage( const char* pImageAssetId ) -{ - // Sanity! - AssertFatal( pImageAssetId != NULL, "Cannot use a NULL asset Id." ); - - // Fetch the asset Id. - mNormalAssetId = StringTable->insert(pImageAssetId); - - // Assign asset if awake. - if ( isAwake() ) - mImageNormalAsset = mNormalAssetId; - - // Update control. - setUpdate(); -} - -//----------------------------------------------------------------------------- - -void GuiImageButtonCtrl::setHoverImage( const char* pImageAssetId ) -{ - // Sanity! - AssertFatal( pImageAssetId != NULL, "Cannot use a NULL asset Id." ); - - // Fetch the asset Id. - mHoverAssetId = StringTable->insert(pImageAssetId); - - // Assign asset if awake. - if ( isAwake() ) - mImageHoverAsset = mHoverAssetId; - - // Update control. - setUpdate(); -} - -//----------------------------------------------------------------------------- - -void GuiImageButtonCtrl::setDownImage( const char* pImageAssetId ) -{ - // Sanity! - AssertFatal( pImageAssetId != NULL, "Cannot use a NULL asset Id." ); - - // Fetch the asset Id. - mDownAssetId = StringTable->insert(pImageAssetId); - - // Assign asset if awake. - if ( isAwake() ) - mImageDownAsset = mDownAssetId; - - // Update control. - setUpdate(); -} - -//----------------------------------------------------------------------------- - -void GuiImageButtonCtrl::setInactiveImage( const char* pImageAssetId ) -{ - // Sanity! - AssertFatal( pImageAssetId != NULL, "Cannot use a NULL asset Id." ); - - // Fetch the asset Id. - mInactiveAssetId = StringTable->insert(pImageAssetId); - - // Assign asset if awake. - if ( isAwake() ) - mImageInactiveAsset = mInactiveAssetId; - - // Update control. - setUpdate(); -} - -//----------------------------------------------------------------------------- - -void GuiImageButtonCtrl::onRender(Point2I offset, const RectI& updateRect) -{ - // Reset button state. - ButtonState state = NORMAL; - - // Calculate button state. - if ( mActive ) - { - if ( mMouseOver ) - state = HOVER; - - if ( mDepressed ) - state = DOWN; - } - else - { - state = INACTIVE; - } - - switch (state) - { - case NORMAL: - { - // Render the "normal" asset. - renderButton(mImageNormalAsset, 0, offset, updateRect); - - } break; - - case HOVER: - { - // Render the "hover" asset. - renderButton(mImageHoverAsset, 0, offset, updateRect); - - } break; - - case DOWN: - { - // Render the "down" asset. - renderButton(mImageDownAsset, 0, offset, updateRect); - - } break; - - case INACTIVE: - { - // Render the "inactive" asset. - renderButton(mImageInactiveAsset, 0, offset, updateRect); - - } break; - } -} - -//------------------------------------------------------------------------------ - -void GuiImageButtonCtrl::renderButton( ImageAsset* pImageAsset, const U32 frame, Point2I &offset, const RectI& updateRect ) -{ - // Ignore an invalid datablock. - if ( pImageAsset == NULL ) - return; - - // Is the asset valid and has the specified frame? - if ( pImageAsset->isAssetValid() && frame < pImageAsset->getFrameCount() ) - { - // Yes, so calculate the source region. - const ImageAsset::FrameArea::PixelArea& pixelArea = pImageAsset->getImageFrameArea( frame ).mPixelArea; - RectI sourceRegion( pixelArea.mPixelOffset, Point2I(pixelArea.mPixelWidth, pixelArea.mPixelHeight) ); - - // Calculate destination region. - RectI destinationRegion(offset, mBounds.extent); - - // Render image. - dglSetBitmapModulation( mProfile->mFillColor ); - dglDrawBitmapStretchSR( pImageAsset->getImageTexture(), destinationRegion, sourceRegion ); - dglClearBitmapModulation(); - renderChildControls( offset, mBounds, updateRect); - } - else - { - // No, so fetch the 'cannot render' proxy. - RenderProxy* pNoImageRenderProxy = Sim::findObject( CANNOT_RENDER_PROXY_NAME ); - - // Finish if no render proxy available or it can't render. - if ( pNoImageRenderProxy == NULL || !pNoImageRenderProxy->validRender() ) - return; - - // Render using render-proxy.. - pNoImageRenderProxy->renderGui( *this, offset, updateRect ); - } - - // Update the control. - setUpdate(); -} diff --git a/engine/source/2d/gui/guiImageButtonCtrl.h b/engine/source/2d/gui/guiImageButtonCtrl.h deleted file mode 100755 index 9d45f073b..000000000 --- a/engine/source/2d/gui/guiImageButtonCtrl.h +++ /dev/null @@ -1,101 +0,0 @@ -//----------------------------------------------------------------------------- -// 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. -//----------------------------------------------------------------------------- - -#ifndef _GUIIMAGEBUTTON_H_ -#define _GUIIMAGEBUTTON_H_ - -#ifndef _GUIBUTTONCTRL_H_ -#include "gui/buttons/guiButtonCtrl.h" -#endif -#ifndef _TEXTURE_MANAGER_H_ -#include "graphics/TextureManager.h" -#endif - -#ifndef _IMAGE_ASSET_H_ -#include "2d/assets/ImageAsset.h" -#endif - -#ifndef _ASSET_PTR_H_ -#include "assets/assetPtr.h" -#endif - -//----------------------------------------------------------------------------- - -class GuiImageButtonCtrl : public GuiButtonCtrl -{ -private: - typedef GuiButtonCtrl Parent; - -protected: - StringTableEntry mNormalAssetId; - StringTableEntry mHoverAssetId; - StringTableEntry mDownAssetId; - StringTableEntry mInactiveAssetId; - - AssetPtr mImageNormalAsset; - AssetPtr mImageHoverAsset; - AssetPtr mImageDownAsset; - AssetPtr mImageInactiveAsset; - - void renderButton( ImageAsset* pImageAsset, const U32 frame, Point2I &offset, const RectI& updateRect); - -protected: - enum ButtonState - { - NORMAL, - HOVER, - DOWN, - INACTIVE - }; - -public: - GuiImageButtonCtrl(); - bool onWake(); - void onSleep(); - void onRender(Point2I offset, const RectI &updateRect); - - static void initPersistFields(); - - void setNormalImage( const char* pImageAssetId ); - inline StringTableEntry getNormalImage( void ) const { return mNormalAssetId; } - void setHoverImage( const char* pImageAssetId ); - inline StringTableEntry getHoverImage( void ) const { return mHoverAssetId; } - void setDownImage( const char* pImageAssetId ); - inline StringTableEntry getDownImage( void ) const { return mDownAssetId; } - void setInactiveImage( const char* pImageAssetId ); - inline StringTableEntry getInactiveImage( void ) const { return mInactiveAssetId; } - - // Declare type. - DECLARE_CONOBJECT(GuiImageButtonCtrl); - -protected: - static bool setNormalImage(void* obj, const char* data) { static_cast(obj)->setNormalImage( data ); return false; } - static const char* getNormalImage(void* obj, const char* data) { return static_cast(obj)->getNormalImage(); } - static bool setHoverImage(void* obj, const char* data) { static_cast(obj)->setHoverImage( data ); return false; } - static const char* getHoverImage(void* obj, const char* data) { return static_cast(obj)->getHoverImage(); } - static bool setDownImage(void* obj, const char* data) { static_cast(obj)->setDownImage( data ); return false; } - static const char* getDownImage(void* obj, const char* data) { return static_cast(obj)->getDownImage(); } - static bool setInactiveImage(void* obj, const char* data) { static_cast(obj)->setInactiveImage( data ); return false; } - static const char* getInactiveImage(void* obj, const char* data) { return static_cast(obj)->getInactiveImage(); } -}; - -#endif //_GUIIMAGEBUTTON_H_ diff --git a/engine/source/2d/gui/guiImageButtonCtrl_ScriptBindings.h b/engine/source/2d/gui/guiImageButtonCtrl_ScriptBindings.h deleted file mode 100755 index ffe938018..000000000 --- a/engine/source/2d/gui/guiImageButtonCtrl_ScriptBindings.h +++ /dev/null @@ -1,74 +0,0 @@ -//----------------------------------------------------------------------------- -// 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. -//----------------------------------------------------------------------------- - -ConsoleMethodGroupBeginWithDocs(GuiImageButtonCtrl, GuiButtonCtrl) - -/*! Sets the asset Id the button \up\ state. - @return No return value. -*/ -ConsoleMethodWithDocs( GuiImageButtonCtrl, setNormalImage, ConsoleVoid, 3, 3, (imageAssetId)) -{ - object->setNormalImage( argv[2] ); -} - -//----------------------------------------------------------------------------- - -/*! Sets the asset Id the button \hover\ state. - @return No return value. -*/ -ConsoleMethodWithDocs( GuiImageButtonCtrl, setHoverImage, ConsoleVoid, 3, 3, (imageAssetId)) -{ - object->setHoverImage( argv[2] ); -} - -//----------------------------------------------------------------------------- - -/*! Sets the asset Id the button \down\ state. - @return No return value. -*/ -ConsoleMethodWithDocs( GuiImageButtonCtrl, setDownImage, ConsoleVoid, 3, 3, (imageAssetId)) -{ - object->setDownImage( argv[2] ); -} - -//----------------------------------------------------------------------------- - -/*! Sets the asset Id the button \inactive\ state. - @return No return value. -*/ -ConsoleMethodWithDocs( GuiImageButtonCtrl, setInactiveImage, ConsoleVoid, 3, 3, (imageAssetId)) -{ - object->setInactiveImage( argv[2] ); -} - -//----------------------------------------------------------------------------- - -/*! Sets the asset Id the button \inactive\ state. - @return No return value. -*/ -ConsoleMethodWithDocs( GuiImageButtonCtrl, setActive, ConsoleVoid, 3, 3, (imageAssetId)) -{ - bool flag = dAtob( argv[2] ); - object->setActive( flag ); -} - -ConsoleMethodGroupEndWithDocs(GuiImageButtonCtrl) diff --git a/tests/smoke/inspectorSpec.cs b/tests/smoke/inspectorSpec.cs index ed4528140..5d078055a 100644 --- a/tests/smoke/inspectorSpec.cs +++ b/tests/smoke/inspectorSpec.cs @@ -144,10 +144,6 @@ function sStep3() sCheck("check box hides easing", !%s.isFieldVisible("GuiCheckBoxCtrl", "easeFillColorHL")); sCheck("radio hides easing", !%s.isFieldVisible("GuiRadioCtrl", "easeFillColorHL")); - // An image button draws only its asset -- there is no fill to ease. - sCheck("image button hides easing", !%s.isFieldVisible("GuiImageButtonCtrl", "easeFillColorHL")); - sCheck("image button hides text", !%s.isFieldVisible("GuiImageButtonCtrl", "text")); - // A menu item calls SimObject::initPersistFields, not GuiControl's. sCheck("menu item is bare", %s.hasFlag("GuiMenuItemCtrl", "bare")); sCheck("menu item hides Profile", !%s.isFieldVisible("GuiMenuItemCtrl", "Profile")); diff --git a/tests/smoke/palette.cs b/tests/smoke/palette.cs index 2459a5781..0f98dc007 100644 --- a/tests/smoke/palette.cs +++ b/tests/smoke/palette.cs @@ -76,13 +76,13 @@ function palTableChecks() palCheck("third group is named \"" @ %awkward @ "\"", %awkward $= "Input & Data"); palCheck("a group name with a space and an ampersand still keys its entries (" @ getFieldCount(%icons.keysInGroup(%awkward)) @ ")", - getFieldCount(%icons.keysInGroup(%awkward)) == 7); + getFieldCount(%icons.keysInGroup(%awkward)) == 6); // Every entry lands in exactly one group, and the groups account for all of // them -- no entry silently dropped, none listed twice. %all = %icons.keys(); %total = getFieldCount(%all); - palCheck("30 entries outside the fallback (" @ %total @ ")", %total == 30); + palCheck("29 entries outside the fallback (" @ %total @ ")", %total == 29); %sum = 0; for(%g = 0; %g < getFieldCount(%groups); %g++) @@ -216,9 +216,9 @@ function palPaletteChecks() getWord(%group.getExtent(), 1) > $GuiEditorControlGroup::headerHeight); %tiles += %group.tileCount; } - // One fewer than the 30 table entries: Tab Page is refused, so it has a row + // One fewer than the 29 table entries: Tab Page is refused, so it has a row // and an icon but no tile. - palCheck("29 tiles across the groups (" @ %tiles @ ")", %tiles == 29); + palCheck("28 tiles across the groups (" @ %tiles @ ")", %tiles == 28); palCheck("no Tab Page tile", palFindTile("GuiTabPageCtrl") == 0); palWidthChecks(); diff --git a/tests/smoke/tabBook.cs b/tests/smoke/tabBook.cs index 9fac7b271..beaa0735e 100644 --- a/tests/smoke/tabBook.cs +++ b/tests/smoke/tabBook.cs @@ -107,11 +107,14 @@ function tbStepPalette() %icons.labelFor("GuiTabPageCtrl") $= "Tab Page"); // The two frames either side of the seam. If the row had been deleted rather - // than refused, Window would have slid down onto Tab Page's art. - tbCheck("Tab Page keeps frame 28 (" @ %icons.frameFor("GuiTabPageCtrl") @ ")", - %icons.frameFor("GuiTabPageCtrl") == 28); - tbCheck("Window keeps frame 29 (" @ %icons.frameFor("GuiWindowCtrl") @ ")", - %icons.frameFor("GuiWindowCtrl") == 29); + // than refused, frameFor would answer 0 for Tab Page and Window would have + // slid down onto its art. Asserted as adjacency rather than two literals: + // the frame IS the row index, so removing any earlier entry renumbers both + // without saying anything about the seam these two are here to guard. + %tabPage = %icons.frameFor("GuiTabPageCtrl"); + tbCheck("Tab Page keeps a frame of its own (" @ %tabPage @ ")", %tabPage > 0); + tbCheck("Window sits right after it (" @ %icons.frameFor("GuiWindowCtrl") @ ")", + %icons.frameFor("GuiWindowCtrl") == (%tabPage + 1)); schedule(300, 0, "tbStepDrop"); } diff --git a/toybox/VirtualKeyboard/1/assets/images/closeBtn.png b/toybox/VirtualKeyboard/1/assets/images/closeBtn.png deleted file mode 100644 index ef3fb84a162e4c4f5188151bc7b810e1af4c62c0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31410 zcmeFZbzD^I7Wln~0fq*pL0|w8>7g5jl5UWc?(UAEQ4m3qkXGrI?hZvlq@+tJ3F+p? zIp>~x&gI_gdq1D|_x|&KoXs$^p7pG?_Vaz#v-X}nY#ys9Nn>M>VE_PtEh{6bcKyop z+Y=A<`uFr`b^7%Sx|58KD*#~P|Mmm{uTn_>07KbYLPABw%F)fy)ymNcDk~uYb#igE zu(mS?0I$h3bxRHPH6r1o*?n>ONJPB6qZ%O^R82ezK@fM3ks2LOE|NOy3!!S;Eh#Bv z^4`oya7+v$noyM)>j}mLWSRPPOvszaCm+rheBaoPH6DHIzpNS;St;5}s~Ci|qG2V- zaH?`6u=2$zZ_S7FbapQ+v%-Tg?m7W@XccDEt{1c*;5a}?h>4~Z(hPvSK4YQ*t%@nF zusab~xa;D{`XEFosMRfwPaX|H0Kk1>#B%`n0}vuD>7F`}i3}L^nVKvCDlC8zec-px z03z);*%t)p#Zwc4GUEU!zImi1U?m6?4L*D#1!%(n0t@+0eqe$HfXQlE$N*(EKxY`&d^0Sd}X#c$9x7Bnd1h^ zY(zl5aV&X{Ou>ip^my8Ct&2whu-o9+HgX>gjIat>81%f_7QK+pqyrEZvd^6Xz(DdY ztHy^4(H=AakjxBXEE6N&ZzExDMZMF8GTnxGZp0HRez&t-98Vm>3~|TBh@mK0{C;?U z83m&e+X$S3t5wx1{Dl(^YrAGGj<6H%sWDo5+x^BMbY!t!G<;*~$w-jN6K&cUG~(p2 zW9l{;2;A`az;v+O6Zv+KR4Y0UvZpxIm{LqeTR~fCLM9J-=PuW+ zNpxOxaHwiKJEl5F`~ zjp5tF4~8AoXxEtYQ@CWvXkWaaG_!pN;g4sAGWCWnr!99Zb1suFQ}60wq?y2--yW+k z-dFozBS-ob?>Eid$+*dmn%a!jjDCc1A243Zy}b95oRLil>(~9ZF8j2foZ{^l0 zy-+GlAG7Qz`%(SAI=_ar?CS*TR7tgcwZE~Y39)5OS4qd#i>4R;_5M#Ugdx}wUvLU3 zbSVN{D_sZ#9tzC98c5v|_3zqGSYxx)9>g1*;9RQ4?_=n+PE$|$ENw|_S?3zE7a8?f zKDduHQG_*xynCj%X<5$g!1>XF&+tK7N<~WhX5Lik7SERTR>XA1N3INpbRACDp^jdO zdDX9I9vtu7W>XsN$fhqIDcV9Ne zqBHBn2*sWS7YBcBE2g&U{T^FPeYBD^5I10r6Q&ZdIyM9`{$?E25r9P>77?ZuE*j$$ zsZI8YJ(1h<)3k)^oaCuwg5+$9CA%_zDT^F8qeua(jnE`-Jy*SGH>-ut$5HJxZCN|{ zJN&GSB|Hvx&2#6Qdl8FiigACd}1h-XzY&S49 zF}{ZRZ%mJGK2p|Z)0wL>J`GPJ7a@H7q=aCKIFT@L;{LSPSMmtQ4=u^l@s;_7opok| z7Q+^6-)33T^>7jj62B$lIADL7ti+ysSh;y{C^f~F*nfYjA*5QO?oEqOZKHYa`+VjJX^yvRmS7T4xhHjeH#zCzK>4nH6C4vylN! zb&~X6UOPgcJEFuEnDmJDEn0wlrt+Y$q+qiVf3^GSL5ljTGd&Z%dgYHIpT^c_ea@w) zsBlxcgmk>+_B7sGPp$=jeqp&g7@hVq%|C$kWZG!zb+zw`>A;)~uH$w+p4-B()K%1q zV58uG{%)Nc zjwa@;UiMDcC!zo#B$<5l)0s5O?V-rVrH(?r@-va&f@sDxYJN+||gX=%o zT~lQ9GInBPXN9r-7b8W*pPSj+|3^z#Hz|+nJ^9DT{$oH_4R0rNHZ^lsM|T%fb14sV z2RGXPv=dH$ev4qn*efS@zfGr|XYTzhxEl zzLvFvnWLvGgAm(IlfUNsFK_0O#%|^!*OojiFg_M`4h;^N05|{j4-<^{IxGL?= zd?;S$zL~L`@&DrGU;5tk<7j4W;r%~(`K#x@`cPC9kaci%Gj=dFmz5N`9y_bGwV8m4 z8K;>kjE{$fm)FdU#ms_>gT>gC*Mf!9n8$?2nAe1%E##P%<(f9=*kE&pL={qNKHzajX;|KH62dkjx2bBCX$ z{~xY?@AcoExH?+6c^bQzi&+W;fjnvHe!7?X3Slm{M_{K>YJv2S!z01yNPi8BifDTzghiuULx>2 zXZ7n-XmgS4q1|k{*;~ch%iK;|()#+y>H6CdfcyFo_`A(b#eX!^`EQ1QQ~bm5kL3E> ziSF;K;g8nq)9C9nc(#9@!vEVs`R8%x|K^*&M)ZHv>kVJOg4_V|bMY6hpYyq){e|lW zke`dcaQ&Rm4ec*nH-P+H{Dtf1d~Rre;kp6j=i)D1Kj(8p`wQ0%AU_v>;rcnB8`@vE zZUFhY_zTz1`P|U{!gT}4&&6N3e$MBH_7|=jKz=U%!u4}LH?+TS-2n1)@fWV2^SPn@ zh3f{8pNqe6{hZGY?Jrz6fc#wih3n^hZfJkux&h?p;xAl3=W|2*3)c-GKNo-D`Z=E) z+F!VC0QtH23)j#2+|d5Qbpy!H#b3C7&gX{q7p@yXelGsP^>aQqw7+oO0P=J37p|Z4 zxuN}q>jsdYi@$LFoX-vIFI+c({9OEn>*suKXn*0l0p#c6PjO-VbyKXl!}Xo8p4Yd$ zf-v41Uf(VYHI-3Q1OOj806>HQz{%D1>ly&K!vJ922ml0<0D#!>xnb7>0N{EdD=DVo zHTgZ&Uq5~&dAnt>uaN`XfryNhLAxy;$sYQeI#f-9r99lc*qyID1SYznpU1 z{ZXR!xI0Ua?)Z}K(!1`elJXMw{HjhoIc#=hbZX>Vx18_Hh8tTXC0eGhoP%U#Ow~WT z9=cxgABm3prF$D}ot+Ag`x}mL*KbMl?KC<5=oD!RaFFg6UEaF*^vuGuKdSFlgUw6! zVKvfGqnZfU_=-z;JXTe^Ac;)UD)U#^ZwnViOz%ZXcQ!5Jz@?~rdvvW-@Au?FvUTb> zMsOx|Hj6y46q0j3@)JVeqA{3YT$wBK7pFc{67y2@db$CP%vvX^)cdJ(tQ# z-FzZ~RBt!q*M}Y{6InLch+*==OZLgx(h@dat06Wj7K!Iv&xsb2);SIu720;n1jZ!F zX7tG&lBH;8RVFdA*z;ZMO!VW>6{e_osVx22lx6w)oHO8F2o?+eYNmwM6`GOHM`$Pd z+Ffw|K4u?Wn;|W6EF}K^%0?k{S}eN^hlX+U@ofJm4;JF~nU^m+us+^7?haK>s5sEV zP7AD?Ec@s;ekx=Y5kEQ4il%d3Jo^Am@LmGy2gJ)Z#bHWVxBOD0Twy<1aFdXn#cfkk=p)g0>mJrk7pWNE;kTbh7|1_&T>x_ z>j3-UNEBn1a7n}yoaDMMXo3ol-eIsHIDtikJ;Iw@`TT4D_Z&EY+-0 zgmrfFL?v?LMvs395(&?&@X!?v@(4Wf$`NjN+qUba(FCLC`%jt7*aY6Z=jdebk$Dg} zkfkZIP+@H4p$4`*vbhq~iqoXf$IV@5ktY zS2s7p0-zi9a|_|`|XFa*QK%Pmsi;^Cisa{7QfT?>#JT0Y+7#iBeEfs};TKd?J$rMt(?)$_bCp1oA zSKOI5SRf<2)GLdhlzWW{Z&B8LEsAtWw{Z9C-Ao(9k=J5l1FG39sKwg;y8dXDlRISQ z_<0TOB|GnMS=PS|S0~I3geSGfWfYd<`A(P7HxqY8Wof&Mk`%;mW^u(vg)1}JXQolU z->*XQmkVY`cQSaq>W87S^BP_g*4KRtuUZjDzr_}#Yzy~cNbX&F_&oO#T2c-{Hq)#4 z=R#izyej)9&Cx8Th4=)3whP_>k#l=A?!pJ7NX9zqYbe}GdantCtBtt$8j+AQ=80~L z<@KI!>&!t;dec0&8nJWw2lOo|S8*c?yEKhRp5w?%!)Na?6DM?dEo*GEDttIbGBbld z{D{v2Pk;Dk6Ny?cn$r<9_ln$*au(wQ&q_nk%j^HY@xorrX`~h_%dOUZVJk#y7NqmE zI3o>nnDj?pBLQ-pKru1b2$nmW)k@y*EGlWl*ns&Et4+tG+>ptaw_m}9l>#ictmu^W zz$FtwbJOP_p(HzpWG%E$O+hnReVNh7^&z(KHLS4+HE3CTnur0`haXP4^^lUDclr&; zr}pu~XsRo!Dn=$BI5$eukTPA=d~%qr#b0#eoVS(zlJI=ZCdcWH+Y`%B%=tQ@{cOGWFjJ$1wIEKTNZx2kKWr{R z@+!HZ@Jg~`;?++kRzNb8Qoj|g$qk?pt^_%tk^p{uYv5vLH~Lki1Ct?QA=D3bPBjkL z$PE|Pmo{iUC~Tpu@Y+G6&!ymlw>8r#kTJ^5`#sbnPhL<*S|uSXv;?Z%QdQ^h`Ege8 ziE4xj`Re5O>5rH5W%`cwv&dUP=AwEY*jy>)Gj8Z4-kZ{jYyi_N=~fZ=cyo?75!9Lf zK4;H8NTZM12lA4s7@Ad?_(JjIy8ND`$CHu$4tP&H}FBz>A zJ5a_gg6dk&EpPoWGBB#gQw37z`HIzRE5Tw|LLd%C#`Wdz!Bi?aZw2zTH8coRv~2V$ zH(_K{2De$m(B*fm@@|J`g@A>$1&R!0$?1|>G9dfm7hD(zjAQ3ki(7dk z3EWe|lfbBzBNiDh(uJolE-bclzfH5P!-@VA_xvQ5v+OQm9jOJMdsRI?-nx2CVK#ht zB~bI&w9L-mX{C{xM3(BufdNv2d*?<5ME%X$yRV{R)AZes-)AzsdX191z!CQ19Mbdo zfuKF?mEbGE=V%Ld#Rz(MKQ4X5QSdriE&~MW_b^|r1o4V+nq&gJe!gQ}g(;r?_}$~D zco}r12~6KuuKk-Kq36z zNQOuqT-Lpp83tbfv&MzGS6D{hxlnM-*lv4Jl#&f>A1=_dde@uGH!?5>K3YKbI zow7&GuVeZ7tGJo0V|RaPP%ZVL*R*9T*W03p4}-srRILlN3Ynyvdw^H07myUVj(3Ht zT1n?SE5}!H)i|*AUxVJd{RnO(SCfas1ydK=%!A zhlkP)5fgjlTXF?dpsC6|3M`}4N}xpe%MWxsZKsD z(CRZWi^tM&Of=YejDyg6Xkc&!N&!~;Pt7td#M4NC4kA6h5h=7zlorZkc0|7S@ZNrb z!GQs-UqhuSZR_`m&?q1-h4u&tB@QJ9YE5T+0B)uJiUzOAalQJa zhaWs_^=&X~n#Pvxu6d=)*1^1TfIl837R|+qixo^lr3>>R>NH;44t5> zVLhSzu}X!ckoWev9PIvsu3C)HhxOjzWxvf>59Cgca`lg(6t)7Mg4>-XaQFE;p|}zC z;*9qRYn{a^`G>wahSc!f=l+0(mF`sJQH1m`W{avAB{q<5BS?v*XcV++__+mjNv~+` z=``}rgFN#!;rF@ea9sFQVDmx7{0!lR%|0DaoS=)yZGL&C4B(6Ih~nQA8@}|SD z^g>*Y)@XssPI;*A`|HZfyn2acG-W=8p1C=C2x@e#s`!eJRcm1ThJr88-P*GC%PpIS zJcu&(S}qkV)GuXn@}PxyC#BWYdW*w|#0@pK_oo_3fau{G);N_lYC~H|4M;4c%LqjC zn2v}!O-I!GJQr&3tJ_PAFtU@gWBh0}K3wG{^W=!E$-=9&y<)C{3bpVB(Z@7gee6`s zRIrudfd?sk<3r6upi-+w3*6Y;V9wi`{Uvb`)U7+CB|1ze?1OmRe`$l*dcHviH0dUanl>EVc&gVLBjnR(1Nn zHSCs?po7bufWIOzaVdxH)H;EL`J97!+3hf=j1;Q$z7rW$jLB^X*Df9mqyC_Dew`;} zL)7mg3P$5YQi@f25P6G3CM<5Cml|GWf-|11#Fp7!a? zzA6LmZLh%_RtVpG*buTOMpS~P70Lt$wfj5!#Ec}W4+2(&m71B{`e&DVxZ+3UV^>y> zkZ0)qOCH}qdlFMtp`inaCWc~kP>5ekVQFKVlAVrQ10JqL%On6R6#Hvdh&43QuQk(h$kNE|KMk-*tn9i}& z`&Rvy_OZs76; zS*|2``?QLNwihDJUuIBE7kjJ4MDvJW#v2K1A4-1tE>C?q5mNQ6t8FRwL$NIqtikOh zI=2SQ6wM@MehT(Wf9%_wiTjo=7xd9fgcZoN|6C1nyE<($1Y;4*==e~fYZIu(1qrN= zaA%;FWqLld#EqVifP_RC4GD>GiR>@TeFIY=zfIudgpb7ZrXAG|j*JkzNV}qX7B4Pm zrgXqL7-Sx0AE=M1K%hvVL?X6JuB>fp9K&S0^!Z+r|9YP&vmYr**%tmFlD1y4J&=s@ zi~taNC>*#~NxUf2sgs5B?K{)u`^7+1JwuZ*i4|yURwd)fyt{Wy3=8OMaey~ptj>Gk9+(YcPcjTslaO%<{Hwnu&mpSl3i!U=i5`ENN~Q~8M_C5W}7pp~q3&hz#5T}cW? zk^#F=9eFDEm2W~jJf30RH1;yR*NRo`BkyUT;F z*viqD$czi#n8w593aHP!r$O+DKo=4eCH6i8+w{j9YuY`rq8IX560P^LGN|!FpQBPdMFOa65smq03b(n#ttz{sl9Dv*4X~fVEtC~@4&#&atQ#e5mFBT|VMI#}U?+vWNRtJ*76@t_voYbZk)S)RC zKbkQ}N=c{K8$ruRF^qIZ%<=1*!rBj|ykG0Kz2hF{Kf?-D*Jyf;^t#M|4^pqsFXae+ zMD$pk6wTf&VmzoIyz?;_&jDQvLlzGUby&P>Uj9|@jsfa;4*Qo1A_XOh*?x`Mru~^q z-W4&&#VSujU!3Xmpsh=Dqhiu$3KN*Y62c_k)2c_5QQC5F86P~~c(DOOjRqrxUP3kZ zD3jCT(Ap6zbahw?QaLnB+_A)@tjV@wvxS29wn15H@n4@JLev)Kb9ZldG>lj~kL+la z75jgrqfmCE_d^?jD9L6_!t`*1J{oD(k~`S1d#zRVIqe-+Rg}uYJZ9@MuV8tja3t8%|TFaJwy*sg!6BE>x0+%LHY;%~82zpOf&ll?tN&1 zZ?{$@lK0PxaN|wY5El9C&PwQsG1gdg7Bx%nSIi$h!?Guz3}Ev=Z1OLps0?ildhAPp zkDlAbxoAC9=rihuij1sUv#qX^v@&sA{{CFmjIPrwN5NZ8$xoYiWNqV>(y)?Zv9yDL z1HHK9c!&ElJCs3?vf&ikw3^^#KnhY2ZIHp0Cv}i_+!x&7coVBCRwS)2z2%=QVm~gD z*<&VM+)Eii5zkrIP=iiyD({;k5eDfRqUZHz3FG$|i*GxB!aS|;Nn{`@x=6r&NW2<> z^#!%H!jZXa6uTRVTW?QHL#lJk2|qe__R;?A(Y(@5lXq!ih;O;=K^aAnrDf5_LFBde zAt_~MgqvysM0?)L4jZ9{KZ`w7bN0;vc+YkK_Db-5mBVgB)jUz{OpX}e+&?!i#?;h6 zABA(boJ;N=Ju8u=YYDvTy|r_I)8Kfi zTpRRoa~2>qY*8{K7-R=V!KlV~v1z#o4(`4fX6L%0!;#u`| zHL0P?^kAchGs4ngY`i?;(7C{YCLE-ddp6QXK+OB-dX>8xXo7&hSNU_?j`yDR4%)`^ zPDgB|A7CEdLqbECV=M&1sVKXXg;BHR;Qf1t_W&=(#;<^ONI<}>~lc3if^&TbQMxBtkZDEvQ6!$dc(8#xuE}yMo zL`es?wBW^r#J#eaToLP|d#&u=<;J8W14)NEzO8~V=ldF1A@mWZ($Byiw`D*H?-J6W zMfj<@q@ktA>VC9%cakPxs#s%jkS!rSkV{G|5j%^YDn@SYQuEw+#|~x(8$!4EVWH-H z%`r;=Cf3LS@f;(_>aypX6(XnQcry`j5Ol-^I_;_)(=715EZA|AcSEi>kDy7?Escg8 zVG9%{ZGP-Br^3k`<`5M*7?uSQSn_t04 zcip>`^2uAr?zo#zelp3DypNi%z8)_iVy!HNetnLYM?X?OZ;i+5qRI@pVDDgGvcTKu z^2rLAOg%GZWlTx+eMoxHb%-=ECS43cdm?rVF2H7RjGRffZ1xFTbx-u{X|#lqZN|*S z&GjFg4Alk?2snBY0=q*$04ofkE1RL=k@UHa6E#U`rX^A(7Gof$^y#4CHfO3 z#i)gYl^^$Qowg6mywm0@p3>Dl1f@-qYLxYk84<)fxuvsI;0$|~TpqDqQ6yF8oGmo~ z3ewTifE)2LCh+*gb?g1WRP_Vl}dC9E~9#NV`+Q!r7%kIGAq4HAWRv!e+CL;tUAExyV}X3n*E^WSz;`atKJ zL5zqUwJJ0t1OIg?#P=NfS3r6t+Y^)~N8`Q_+1PdOg3Dw>@$-^8Y}^&G&Ih*A`V2C? zmTq-;t9!)WW5M3VKc2sBw_|UH-op;1ZCB+m;^RwL--7hh#H_N0aQ9TRek15CiHcIT z1G!3%bm?e`$-Ue>eTgTB!=fpaSxoG=s}re9&>~ESws!f>@UjaR%5fA`lTis^q%a_@ zdQQLE`WBaVCP~@)8DH5~w0*2c6c6__H1|rt5S%3VTnJ-+a9 z+e_$@EeShHIoteUw90)*nk%(slU=Vmb$n|wfQt_a%KPCdpq!lk1W2k2v|xd2J=7vX zc!q4Jb8n8l?Uh6%!4-f|q}>^bo%zYIobTJ>`&q}c5T#&u2{XMB^CaVBZ;>{foQqpg zG%YpUq<0#Uw`GlM@#z@Jxh=S8$hbTkd%;|aVjQF(;-iRf$eQ{>Oowhjun z%A%s`XELX<2H8RQ441-}7(4o8^6ul}P=;EjS~LaWLvBuW>L!|8?<|1*v2vQ?AkqW< z(KqH)&W+D(q{ZfJ1{Uk= zXPBs?ZDtFb~pc`ogBe0GV`;s$As1)oN z*h*6x4o?jXXhm!D30z{K#rk?cHg`gy5YLxhe<3P%m_lO@+TnY+un+tYEn3JYMLFQ4f1`JzqGX|F&R(gt8 z@>gX0N9T+u>-@0K$-4sp@{3|u79~VdL6QyN+@cN99h`VwerJAnM-=C(hJmmsBc`S- zvFWo=up6C(EQF%9w{mZtZ{WunS}Ls%?I{fr;M}QzbQs{8pk(2BK#rvd+$yAb2o@qd z(71~@(fVcxu56jOq=65XlS_-~Eo_i61-;D*RZGnVgOi=lHk0EQW0N1ptui93 zjEbU9_0cB}hsush7rJA;xX(#Cv+t6ubas6FiZ18Jk+eDXC_x-8L8iQ&a;F4KvLJ1~ zFHM_x9a0=aFe6(QuM?l-8XK^?_r=f;sXZ)9INcG!w0R)ypcOpC*e$;&rob!Y=tbfb zp2I9(qu7Gon&5Dz2c&;HtgLEaRUy(x2|5WH2aiMJi|8OYLDfpLc}D<>{t#IZM{`fh z3?%XX;S=FS=_eKPv%- z%z_G=u1ufIuh7`Dbiti27pX~Y}mJ4w!k!Dn-X`Uga zh+~THiS<9MsIrUo>7wMP2t}qgs02w7pv#L$B~SIF@H5fiw2l!pp+iqzLJUyK9_kc= zas@4g$Wb>S8)*l?D+yP)m$h9M$_~>m%AgC0h9gx&0MV&Q!{Y2Lc1Htb;AkVreBq;t zK&YpZKE`)YDMvuNFY-O2x}^t8ZO+Y(c^fOR%G~}@3Tx}c~zSD z!0VY~&~o9jY1+aGppJ&K!|Jn4&n3N+QX!;)w<(-+Ye7!>$BAm_dFVE;UjZ(O)A<`n zLmG-GtH(Y)%!otY*#P|3!xwoWnLGo{><#3rXbxz0faw$VFG*NqO173}cao};Yf`S^ zP_I!r^t16aA;C7za4*PPqfa<2i?44Q@e-hZQj7FhcYB`rXz7$tJU*E zrL#45&xXJjNAzfx=B4#>QodL9 z5{_=cgA4BuG}^(r={QAC5pBKID-D;}t>fP)kQnG2AAAVwebNv^!W+%@c>rfKC`hPy z--axdt>At8ok-baU#9>Jney=Gy{vfO`mF6xi;Ydbtn%#KaZ8%p49SY(O=vm6Bypcj z9<2bx-+Qv5>>Krw^N;30q^EcM4(Nusg~yI39s>FL2Ix+lZhM9~Jl=K?_u?MDbjZTn zNVd>Kpli~0-5W0ky@wB#`E{!A%ceT|4f3n|M!NYAogYj0T?ai+Z9&62G6~?b-PB2p&KfB~MGXF5XH%XCTUW_0L2jHfLi>XC8j&vD zwi(CeVGESxWRGwYSV9iQ++cSOEMMUU*V{ce9>20Ez_2eUkC3aZs=!8#Gu(!QBZ_jxVoweZn#_!8?>xKo^PWI6 zHM!lP!v%Y)!HBjgdVzJo!gF8?+4%(>zYV{3vg__A!gL zfO0XnlmW5j2hyqi=+zf;aSoV67iScW6{F>Z^Efg4YeYRFhafC=EHpucLYY7q@)X^U zu0=f!lM!E4OVb5V7FIav06RW+{#IzjL|XV*2QvK5DM$Z!;qwDO_14CWg=67E3BiHb zZ=tPgo}1gJVurN7%NFx_Vm!TwS?=#-NS#JnWg9I>WirtuK_d}{vMpFxGmp59k-gQQ zeeI?*BIco?9(qd3={2bEwLiz?@dWO~<<){oxS zZH5wg)mL3x^kTa6y)G;$##gk(V@QZv(WnOW`9SI&B=~ZRj$v@`rH-%VW|)f7eZhDz zK3L0Liw;q}H$DK?eopfo_%3xw17$5^iiO;oPN(8S>mH8WM_~&3+%2DTJGKCF>Ocj; z0f!wy=S5_s=sf8?!F&{+X_?t4H1r?mMKg2qIePtHKU#%QzZ9MbQn(F6BAqQXpfXbL z8$DtQbev^!6w-O4%>PyQBp0keht{4xkSM4eG(=bWBp)R!OrsjXU?78!0jaUr?*T)rYGdv5TGh&bRo~M@=Wkfsze!DsXSK*S@9E(cpU0 zD2TTE=&b{~ng7UI9w(kA@%09Am@Xhs%A-8%p5PVX*cEY{dyCo_k%Oc7!3AJ&FSx8H ztEH#2+$cW)YSr|qPF$27Z5$axm7VkjA(sU1*#f|8`DywsMVnDihM8jx!7*;Pd74P3VQBE?d^#F<~evvJ;8WmH^-+Unms z!(MDL%YTxeV^lhp%_E^witUWn8MhB!OM{Ld^PY`|_^~?-M$5$tKNG#`JZkl@^XT~A zG+#>6-{6fCYTwjwFc;8bV?;)JCqDQ&Ri{4t^_K>^k$CART!=K{f?i~Sa@#Jbx%^o+ z!4Opy>$^+TcH7~W_+yetpRaburEuGE$tonQ^6<3q&(N(~A!^*A7!pwuQO4~{m_F*% zBc;*zW#_11K;I|S!+u42G$eSsHW4I@CyIBR#|@@pG{AP4(~10?uju%s{vE7?Z$ zrV-us1sh1xlm4X7P@kpfy>kC~9j3qsr(MzygZg)g5FfFVQGn*A=EnC`+XKs21x3KL zj)10|aM#->6wIKi1dI<*FY#iPD6!s8)lV2n7Q;+QokvY&0F}imE%(}UC5n4`ojUy5NFrmR6bO>=WDU=u|Hr04ocn*t*Nu$ z!a_SjL0bl|bYU;b!WQNh29awoMlS;)b6J9OZp(|kQ%=8<$BXmv8^OyK~Y%C4@~sI%N+c)0wlRRJS1%W zXt~r37#W!AV8-fmdJ-e{s8>2(xEpuyXIfajoD@(=usy3{$$a#Qk*l@LKrNdS@lk0Y zIJVv4J7S`xu?5-enQ6-s->Ncs3~5ZT>^Bs9l+~k)trSJvkifXD?ZzLO7WqsN-Q0aY z|5vkfJ;oTsZS=>;qP?r-M&R#vUw|g)Tf!voqH6=3&Ub?rpPhSzqI9!*={t~);MA$x zhdM3Ud-kBH5ZynM{s8Yw^Ea(Vdi8mZBoC?o&AA3Y^6|)#hH&QeW#JSP(CrX?!3hYS ztmKcsM}qa4L?NHR7nf8E#d>cYp+Cm^z$2P)AKY1!Q~}KTt@Ps{_B<~S3>U(^%Z@qM z;MR6M(HcV(Ttz&0!c-(q;o@L~Uuy#xy4AK?443Vs+@!$k>v9=%P}%D+e^UWxX*_G% z!cbvSxS$ryHPq2Qei~0A$!DoW%6vWhng%%W$*ZfXuwt}b^YWl@f=p0$sDDp@cosG_p^Rp4MkrgTIw-9mu9DY6_J-j zsKqi+f~9qfMN!uUhOhs8PDzeI?Vb}g0hJumVboVOl(Lab5&}SZ#%o3xQ}d{|0Yu|k z6m@+ar5^=KCnuksohhJ^`wdx9RlOr0@Jzt+TNV+2HU!2~r_kdj4P2w|>lL{EPJexO z#|4njaEC;?4?nP`wTjbCC4$&Qu+3^=yG5to5kblnEfEE~1o~|iIm8OTlRiWYlWt*o z2CFT`^)La>UR^u4TRAj)5@;QsR|gFh(G^(W$<;CI13zMomN~45vZMroOYHgsUDA*1 za+hNrJ@X5g$|RWE!Hpvm#LlGqmi?n~QuFIn#mx=GC21F%g_d)u3)&-lni*8XmCOfgnN?}zY(#j~ zGGqDKeQl5|Mxs5au8ve9HtyhfJpJT$Z_m@O7Q(9-PdC4DLLc!sl(V9G=G)HAxYXt} zAD;KILovG+DJ7_TA^QTR1d$gmueJyxSKP%UFl9CuP!{;+NeZ3Y-}JfeW?cAsRdTP) zye6CH5}ij8IZp4%&|e|JLX{9p;`=VTI2ig^9Y0sd5mDDQZQ@N>)@9Lj`fMq?d9)-4#v@F9R%E zlXe(z(@*!G4w!aPyQp1)ut%s0xj}bTxGJ`Sx>odX6^yawmM8_}K0}|W7NiD!gVq2# z*TiOTBann1BWT%ToRkZyMYPD-z!j<1upZH>;4Fop^{J|>_IHHsU zyC8Cr`I0sMJ^+g(kyIdGs1h1LTr9Oo7eKtAyK1-!a;?6}Q~>H`u~Z5|;Ico%w*x}Q zLxuj0Ap6qC*Nz+*8F4JV`Pq&K>~<|xogcmm@^Lv`-vp{zafWt++7;dr$^~siHHWtX zW)`p!aA^Zt&(vnYAh3`9><2bZem3Hdwjw60(SNV*QCuP{VIz3*JsBgd;633v!3hYq zhc5a95by^w$DaZ7YSXIde*oQT!)nO~K8wfViiFXQk<#(IpgSn9H2nVejf36&uXX(u$TsOYoeJ7PEmHj@P^dkp?FH>5YqXnyS-Z`XfQ*;t00#~npl#Ov zQnP?y5dy+*{tsT|tNa2JsM%V!x)8KBHA1-#j0o2R3kZ!gipxOE5xb->0<<59 zNCaP(+c8oG{(8^Gv8Ka^h21Zd?wzwzKUJsyqaRJmznpR-qV50RP56J=MyHtA7mrWM d?|=V)4*=(W \ No newline at end of file diff --git a/toybox/VirtualKeyboard/1/assets/images/closeStates.png b/toybox/VirtualKeyboard/1/assets/images/closeStates.png new file mode 100644 index 0000000000000000000000000000000000000000..695c5b1977e16b790ef400400b5b0748a7772d2d GIT binary patch literal 17112 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 diff --git a/toybox/VirtualKeyboard/1/assets/images/kbbD.png b/toybox/VirtualKeyboard/1/assets/images/kbbD.png deleted file mode 100644 index 66999b2f8b92bf579f8fcac776f7eb96e4ddb068..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28746 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 \ No newline at end of file diff --git a/toybox/VirtualKeyboard/1/assets/images/kbbN.png b/toybox/VirtualKeyboard/1/assets/images/kbbN.png deleted file mode 100644 index 922ab567050a542bb36741c4a8a61169f9f1836d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30944 zcmeIb1yqz@_xSq^-5^rZAqYqfLkmNPbV>+Fr*!v7Nh2X$0+Ip>N~cJ-lr&1Wbi*CL z-}hU*@9X`o_5a;<*S*WpVdk8D_CC-4oPG9l&NK6@2~}2<#>ODU0002{p^Sto{8jYF z6AuOcch;Qn#|yf>jJ6X1VBY@m1OX{&BmjV+WGODLtZZTDZ0BTQXHWf5T%6k8(ay~B zsVM-sPo}GxtE;UM2_4TKh{=V(;^pjA3DKxk#lm3(F$~N!=y>R@0t%-|{d3 zOFv2W0s)WXX$V1CF#z>#(+~;3;sNkx;8B<)pv3_Yn8|(O2fnZZ91k_kWPrC-KyyDH zW;uX{1#qZ@1Tg_fUVuR-9i2N6m<$j|?P&_`-Y>;lVTO}RE*Gr1%P;xtF&e7_s-`9z zC1d|X;`; zj25nz28!)P0N|^$@6Z(sdj%=%0Se6KDw}>A$wD79%jG!StQ=S51CX^gskv|egN@{? z7c~8Y>a#qr^M^+cwO+FBd z9%wYZ94&oOg?W8JF--gEp;3(%Gr@uizC)BmO2j_rYuZq;Sf)1m&(|j3=2*@Vp>y28 zTPrBYD~7c|&lvGY?jD|&bMv=j0NAUuYZ+lgLxfobd>wGR-Vwf(&Y}lkW)EW>06<^j zE}MFHg>VNN07zs#V}2`2cF;n?(u_jhf;`=Vd11g4D0cVLM=?Ax3=8HH)lMWGQV z2c6Kg$ROc|-=*#fAyA=}i{X|5tA?plYsgUS2{|J0Ki8G2jd{=saQa^a*Gq%rpOvc! zjbMEcuPNXq4V22p95itTr$iHFr4Bm2Cl!j|d9^ZFY=Ju&4HfU;XkH~jg?xI-!Ped? z_KuGQJ7Gt}NX{X}BK34X36!$LQPAAOr0&nray6YpL8P;t(CH7^hz zzROkrjQ?5cv#l!K8cSg+mkcRgT-T(DbvY`3JPY;x&Y;Eg#nwg0BH1F%-eZh(BN2z( z6SZ%Qs@+zycb4#$xRy{%lQ=~+a#B=h3SOz-;*Qo4D|uC^TBh>6;5L^_3V&Y!(L=e{ zCD|{BNiB#ijxEbpoojE+yrfi29Mt^WvnIWoxQck-hJ_tW7|;=7&Uh!41eL`6jvGl? zl6>Bs;qW^h>Kwe_jii-i++;fqE#^vQZ^D>vjF+-68D5exv#S>tZ|#bN@+mU77Ecv5{fm7(v>&V$kfaW9~3+E4qzkM2iKUs0P-o5{z>CsGg+I4or=vVKkWs!qbK$u@sGDWg#zmLYGKKkL!3 zaz^gMeyM(GbC!Q7jirTBfpr&U3ag()u#MZnPLP7VV`^7LT(4C`{I%gr!^E$FF*QSi zIipgMQn(S8?Yawh7i`*X+H)9C7-$&e6xJ2CQ&bgd@|f}-$Ul(p=iV~RT=2G>#$T7R92flEML^g)84MEw++(a*M6;~p`%wJRgzvpKUP;#mwzk2 zMlnvYIAhGb_3dtDS7l+aesqv3ZSCz ze$va^T-90Cqtt4b@g!(Y+>h?sFun;BZfW+gbuTgz1|+|FjE-8-t4uJ!P# z963K5b19GQ)5f`rt^McU(!XIYco8SDck0=Dyz2jW5R^|;$cE1%kRT!-qR1EjT@;VG zfGC1~@3tF#6o$t~ijU^B>_a?5Az#FGnJE?bWSA8^V>e^!xjpW-L0+>g)db!vd{Hlt zEkBbgkU^BdySAXY@95LaowZoX{h+VjRVsQjwUHoS_R@+E4Qb;XM} z$l9zJm123T>NJd&JZPwZoystc$TugPf=tgbiB@9OV+ zAgrFNexQD)&a?e_=7Zt*iS5ufnOFT0*XHQdZ;LW_cx?*I;a}2(s$fgv3amHBGcNx z(b-FQEUq1+&mB`@3rxC3c)j_GI!xtCVNStrC6=<6B9*F^a{k!pajjA>bZBgS*7HJo ziV8Q4OHkWGc3-{Aa&paoIL>@;AR_%`x{oj0>9oOAcBR*fasQkZuH8;8p7YnwlGouY z59%NE>+V%M@0Cw2-PWwuOeqt1VzchNE_f9;tkhALR+my*qibEW?b-0b%j|fKEQ7ay z=Agl2m*{$IG=pDgreWXH?zrvvaE@rk*YVzEe^1?n+r90QdZqd&2O?Xd6XG+^M};3> zO@G;$_Oohs%i15BmOe%a3K+PGIgG{?M~+U8?g_>Y-pJsC^7DTacD!7>$QK_l8DPjD zzRJGpDJ3Ri_dOZi81ElS8hVwuml)S%Qa@JCi7cjQ7VKX$b zGcsj!x3PyWMBy@FcY8x)Yg1=xBU5urTOrz=x+Yp`OA{elO>PAa1$%K*3riVKM^iOV zMRj9OYh!*BT45-Lpt}IvfQ_lMA+@{BQ(GqicOlx}>S&cb3!PLC`Y+!C~US3WXYEBL?7dr=-9SmUw^9yis32<;w|LZ{u z#elyPbTly&P?eDSS9kD|5Uqu?v%LU2yPKOEn;V49&e5D5%+Jry&cVsf$;k?D!Rq8; z>ul)GYU@PzyOBTaNSHbqJ6hU1TiV%D|FCOlWar{6L`(alqdy=2^2^5l&yH-J{>2VX zk=@e{vuBNum zbpPof?ElzF{AY82ivOz@CdPki+PgSD{SB~*F}vwgQyX}nPH;W&fAqoG((JFn`G*TX zB>%NLQ+Lb%qV_}byV`G|@cRe}!sAas+|ks~+0Iej&h9Dn_bB^o{{-JY{fH`RMomjw z6FWC2CPDU_Du0dlU)D?|44qA(a7`Xo4nFvQbxyDVm|FnMd!K_B9+iJH@_Wl0HWc8o zZ(`_d_#dor!lRn&?^gcN_NE;>6H7CX|B;oyTK?09f`Y(9TPJ5jTVvCQ5>UABY?hWL z0$?sfF#KV}Y6{`tXEik9;$-FHlO#|UC<0zU{|ex5%%`kS$T z>qyDb5}ua~pZ=}Q4_{2+opBlR@bg3X3|Wo1j3BJs5H2%TJ|0sOR~O8G_&K|4seB_u*z?YWuVB z|J~GYt^T_aCp$A|H$z8LQFC~N{7+`)?^ge*cGJ8d`;T1x)bjs$IDJ#D*7dNXBp9u#mFPDiihoK=D!p8-V#Q$Fm!(UNsY+-0?ZfXK$|HHFC zD*nGTa{se@_c5Jp1oCRPe8Jg1FsNJ4XdO6H_Qe zko`YZ{}nlZ(}G_KRsQ`#DAY++T8#Rkq&N?lpNEH)lMOzp{xb8ocK^}NA7>Cz_!;p> zqIdddCiyMViy4~#v-StHf90J&PCJ}n9x)y;1kAy~D=Nta28&AYa7s$>aq&y>i1P4A zi2mOts^8AVKh9rbQVaQqq`z?80P^$VFI+$8b3^(I*9{;)FaE;yb3Qkuzi{0E^7G;^TtDY?L;4HX4In=+ z{=)ThJ~yPlaNPj%^WraDKj(8p`U}?$AU`ku!u4}LH>AIC-2n3Q;xAl3=W|2)3)c-G zKQI2m^>aQqq`z?80P^$VFI+$8b3^(I*9{;)FaE;yb3Qkuzi{0E^7G;^TtDY?L;4HX z4In=+{=)ThJ~yPlaNPj%^WraDKj(8p`U}?$AU`ku!u4}LH>AIC-2n3Q;xAl3=W|2) z3)c-GKQI2m^>aQqq`z?80P^$VFI+$8b3^(I*9{;)Fa8u4#$UHHo7%$fVRnPxtbDeX zNesWync7%JRRI7z=>Y&1005`g@Ygi}aNz)ebprqpNCE(2yVxgfQUCx(eJCNS?moHM zV5qJ;<-%~XQwzg~Ez!_;8`H>OnHg=AoB+1V1`A?iM(mNZI>2_WXoz~zW3x+n01@j*s5_k zWcpT5AM|5p1@+sIP}5|LMN*L{Ut!NV4J_!@Y78Kde?y%<_y%<6Ib* zWzO$dGlSvVE-|ithTQf=VP>|S-@^6PX4Cm6s>>K1PPU|d>1Z>QM{bRRh;L?nZGEpP zUu-{2xO;_f&kWhOB%24D4`v`$>r-MO4@O|qy}0P+l$sOvuc64IDN;rRf=4O2fzAj- z`v#K0azyG?=-a1t^Zg>)h*Cq*QP@ozSCO8LWn`ehHOkW&RNsE?>2?soSJ|i<$m6H2 z$Fyjxl3jaw-p92EA)ceQfDNZTqxF|ffC&!%Yp`!kll1matr3E98KN4MbbVJ%sYLV=E)cc zz-&k8-d_1>h#96JuaXDhAvxdfxYd1x$bnE4hyyCh#IV3q9#BUP2FL3(KRjdavN9n|&pZtREVIYHlJdq*Vqy^S4i`sllH z%T`WCJKxwF^yjBP;JnTN_23#+%#k6->d8IM7n)XrUQ%m#4wt z;OlRg?jhYG5mR4zb(`h@WFd)Qk8wbLuex_al#uat&SY6-AxEEp!aBxFgH9$?W&hSq zNa-E5_27!ci=~@ju={Vhj~`aq(NtcbK)LQltssdWx#7>pZ_$3JtIpD zCRHeQ3bI|gotEii0N@PKGb64-*@i7)?Ae)KAm;4H>S*)KRN@b^+yw4H=wXz@dIH36 zcgz*dx&a?8tWUgr&+uSIQEQ<`Ql~PkT>vhEJIDPj{^JNi7Ut#t73JodOx9Mkw68vlAh8$UI2%3}O z!DHqwX~nNHA^7ObK##0+}aX%vp)SQVQ=42QeNydF5e>zn8`~ky?s;M^r6Q#gDrM+CA+zb)f0zKK3cJ zk6z*|cR8M{<5t4AmKf^T(ru}`d-4rWX@kKnY-e^gdn`a~DUnyD1L#MwIe$lW&g(7w zV4cqODj3I;*03^g1%}AU(-UB0k8yLbNKA-y^G$S&rD@r8ON0uHf;83)_g|o zE|88PgKCJ3s~I)C0*^)#N-9p`KKnN9tzzC-*rJM5#w9D9Va@i8A#wGQP+e`f zzq)9mrV=m`-c!sF3{OjH#1@ol>Xo&L8-=pRxy|62Jo9w^z!5H~EnV%%FD0ImiK|v4 zD%|N=_*+s7-th>FQXSr~fjt#-*}3^tK;so9BH&rD=Tn-M>8*}tk1HmC_MLqEyD_ZT zowN@|szQTNjWRdy|49V!Ch76*zg26dSWqB1d~vQD7&E9vST#&HTD)zxJx?pSX0sArq@TIMup*oV0!VZR;?CIm?UQmQ2hzFNMPqMCq@sv) zbY_EKl!bEb1!NMZfR%c}JEZSPjq~ydg4^m}JOJSZ;24psj6vg$$W(#WDX8=-N~uof zLw7NyRc*l7Jxuq4imS|OL`WdOL!v9%jylYj?XJDV@dnPJ9_2!QS}~W{*)v<7E6nI! zHQ%S(%{Ewu-TTO;pYUXb?tL0x`C{=(e|_R}fT8;Q-k2=#lJA3!42p>FhhCGiCPZP7 zd&49DP5=7@C;*g^EylL9S61g6y+4+W>Srq0j=et_u!1Sjd<^Jyh$;ek0yzwQOk<3Y zxZ3DflR{Mu58gkzy=-28SQ)9xmEB#A_f<&ng)C@Qa$*k}WuKa%P}xmz{tYj2{#5|+ zxurZdsp)`qwf@{VeQ2Fvuy2O<)fB-p^ODkq6Wh>K+H<+NrMk_Pw#5N{;sXe@GG!$E z3@6@RGWTd=dGEYku_@ecf>pZ+3_J`qCM-Bb(t-%Hy`4QNM0tO z0AVz51@O^1cC;KbNATA6&Qr^`+t(AJ$VTNtQl=v04*&=kNC`pSQ~~5Dy>;#MF8CaI z5m0eZx8AZ!vTPxlGZngS7A4l+3pG>R7eO!9CF5!(7s%%&U0!6bzjJte7-|h^_|o-~ zd>Qjamd=rG>}#wP_W}UQ~Y65cQ&@ z3+AkT{^P>-x4pOe^9^N-h`6JX(xq{iDxR#YtLXUSGVR4>JLnH83@*nH6)MK<33BC) zLU}`ku?J`TF6?H{H`%tnxUoL5QrQ@tLSN7xsf?43n@V8MXSH3Skt z<4PJm9&2(^$}b0L9;b?GWaIE_l&{Yd3FXu2c0U)w?StujB6J(&q=S-C3rFT)XWd!P z3d2_M;CI_UI9nHgshYIRBY*iLBWlmyA=@Lfeixi7vf!Dq%pfwp2HXQ?deLn~YU*SK zw&mA6eBx|ngqcEYF;qnKIHVY5k&BeQ-@nUB1ir2#aGa0&FP{vYOuHP~sd3+kKGXQ6hxd&R%F- z8R?3Z!>3{KRP8XnNjJnhPdWCM=mYh?yQskl>MGkeCCG9@WsYb=5-OjzH-TY+=A7YymjZZa^ zb+%cx7}P@q`@Wi2}A(@T=N_xXNUR)N26+`lM#0BQ$%M>|sE}%Szfr|Sb zCW9=Ig^ptxQT2P{u{t>^0T<0|^Pc)+cG~n>El;wHYf><4ly-bdmCN#Lm>zW=UWo8s zZVe!OiKL5m8|2thK@30UcvHJ}QRfAkmZEhC{TT9j=A~2YybOi7vXWDr&oeqSNBJiv z7JTRLB?IcEaliY{zV=hd6a!C9Zj5|mw#Kn1XS(F!R^#0n(mOjoBw=ZY^ioDjWg8}$ z1Y80>62PSJGDU!S@D*l1vPFzKRcN+tc|9a!oH)I_mnC|w zr$2XL8NWg47Pa!gSU`Kd_Sc#V&)eD~McsQnhOY=49+fy`P8~e8W#iq8h)S+Zf!KL1 zYfV(8>78*J_EfJ_PGGq=8D(f_0&2v3_qOmwG#}jC#7A)x&^RKzr$ps=HHkAN=^fJa zdS8u|vAN3LXNke6v+a=haJsNKZSsqfeW>m5c;LY}U zkj!syT~Iz$Iv6BC06-#u z$b+8smK`>-78)Fc+MdGlV-QRYS;YVC7BD(QV`V5w=p1GCU7eV(!IcSRVi^|oRe<3X z+tx&w!2vK=dCmb}_^5xTsf^qhQ2?KjWApK>Y&;orWoWCYk38Sh3;EbX;Ta+4oVv|h zWJHLJ5VFoVO%y&i!QQeqpqZHHS$o%PWXKfR<5EsLKLx# zgA{*_h*|XQ(IDXRbe!$-JT5$HVY9C{-)2g&ul}uZdhzmldg8T`(Y&OHcVFFjS2Q@V ziI=W%?H$v`*hD7r+*P{m?nJa(7Ag(W0>$(va9}J{+s>S_%kwd!P=PcIR~0vi5%$;- zYGj+&>pH%3Hq34Aw_*JxpxrOW=IN^D^_#N?%H-nQw6W15Wh~M|C`;{p9^2z?0!{)S zO)Zv4tngm>#QJfpjE01%iq1~?2i1Q~RNqtNUArpO?h4pECFgZY6E}6cYxd<7O^86O zxhe93MA|)^%*!2b!^?w~P}6D|bB@*17x$#{uu6el4DO|!?pbdaO2<$@^8JJXF=o3$ za6)SL6C2;t!n+lY=QdBsyw1m=mzE}KA7!!7j}@c?1nszGX4I8H z1NHUPWgYZKr;Es4E8gq%NM(d&U6ysYomf@YVvSGK86bya=lEQnZ?x8W&I?t=FIVQg zJg+_MeW;U)dgVQ55tQ2>vH>HA3;y|P#7RLJN*?*?yq4?=!$ZA-s1rTIYZM{bR1^c( ztU+MOs}nROny!3(auH$%2e%q5n$r26WDH8Fps2MOfEjr2lytv_Y}d-$_cXfcaxU|$ z@E0h@!djVMDJSF?-vd|nIZ?MM#~{NkeB>s8U|hHU)qup-KEdd!1)U2hc-Ts=lVw` za}*1q_2qN?*~bpwOqKf;32Qs_VnaWmrWZ$PQVq2!zP9~5v)SjbT{M7P>|H+Xs)tV? zT_YSmo`b4iNIdV&`|>fpTDX(KkKnrV*m0*P(LF`U;dIYtB?QNjmLguaVTQT%nOjZc z)mpxJ|5e}kEc~E^EsD{7zGg;T^F8JLAe1i_JXim*JL$W7Lye+Wf84ZO&C=TIX*rK{ zkCvC=kK6Zr%{G>nqS9(vuWhZ}CJ!Q(eJwY{+IQ~s3B|&9%_$!;7FX zm?q0waS4g?!7Fur$YINx)@|*c@BO_%O5Wv()Fqch3iv}5l+9l{YBEoirOhw z)&kc|v-p7E?DT9;PvQi(#$Xg1x-|u$fY(OkUSWFG+y>zwj8UA&G-7BqEpV+%i43fv zX@aa+ohs$3_$zEBdm34~+uQJc)ZHVIV_oe+z?|`fnV5u zP;ua}(s%H1qnvxqS*z0YG0*v?V%_jIl5>^Xjyi%O1uEY=L&9MN8VnOuG4FG z)Gv|x9{FBx2okdvz6HG5svMsvRqHq21q&#trg^snnX9GdUEI?9^pW_gZ1;&sB|vYc ztEr|AYn?@gn2|f_IQ6@~kLiwITJ~Cx40BfM&pnRF9~RgUviLZ7gl;UMpmLQ$(NI2} zNSx;NdhmG;&{2~@2Scw4et{uhW=_c=P`_0305sg6rZnIPE;V2`^LQZbE|Iwc639o1 zw7NQO$Hqc9QY*xqM7-pCN~aM^<3&G0sHG&)(|*?a6ymqU@B zB^$yJ$tyl=f7YuFKjw2g6r5W`K#06GwnxlSTFl9e_aYJn@yrE&CBiS6Ww6Z%=OmI`)$Ux!6NK*Jf2@s7J!zyI4mi3?X&j}r_bpG( z-Bh8d#iojcdz}dF&?%70UY8g^%WCCkL1sU)(XfgjX%ozn#m+*e&Sd7q0D0y<8nLA= z8)bGlLU?@j`nV_tss5f|RRq4s#DHqPrY&Of#c10iJ@x)rcON~bme2dMM^;2{!#=Nk zZWCE{HhUrf6YxsZNU4SG_dm3jt_@?a&WzSxDRHeM?>gw}z3nxXcd4(j;ONuIkK0Fc zDZ*>4kME+p9UT85R?BhuZtLpBYnJgXGrg6&>Gxq3!p7Q6)cYLiiM2>~-AaQ?%$xUK zjnH`^XeBG|RH(ro!q>j+$;1|YT!4Vugxfo%eSx^BaP%p45M%=8D zF^>*w|4vXNXp0o7hs0HcvR7yXKu%2St1LUIYHCDtNXWtP3x3ax%XP=Y%imO;(^ro~ zF48H-H{DNu*x!>^UD4LQnD2mr*d8*ea1VeexqP;K^O~c&ljZRKMA0O+ zbXkR7KCCzE_E@w7J;Wq)Q0Z+fH1m#|qV434#)n>8P!MOp5&9*`?)zkPevRpLS)JCS z_l;J1oe>Z6-KQsQ&kwn&)oka6gJ7S~Hs zc{ID0Wan@mI$+w-{!E0w8k4jItq9)*?Ykfc^C+ab&Z2#ER{^oH$ZXqb1I?^g_CrW~ z)lfu%(R9KnBAYvDZ-`27)sJQIjD0`p_^2MURz9Ig_*44bdqLa+Uzc%Z$Ax*Zhlk_5 zD6?AE162@`cpd8!9)tAe>pKOJ?J<|Wpi^hM(O}r$k@6rO3n8mW*WV12rznQ#jM z4}oVKTi8<#vmJf^-Q5fKTSTGZNuqdD99@Z)`jMojI_O6Mx5yonHIHYKuWWoY=1}q4 zAd83@=4QPDD2v{skX(jm({6$a?a0lG!~M_)Xse>kZnuzC%CjPc;gemJ76nftw29w$`fWs%(bZAasx&{95uUXaQ^heSV9x6W%{LwrP)f7=!EcJGTUUte?gu5(!fsti3uyr1jsq}Avb zpHviYjjc?*i}CAk8%=0iXP#STNDC88Gd1(-li@IzF#)qJ)2;$t2noDm{tFEs3s zpQ-x_BsZeuCUCa)a~XI*X!MY^rfiy=T*N6s_Z*M{VA}Cc!aEf3-&}y>z@pBSy+a4k zhE(ge;B2CBL{i8gOy@pu`KsfLDIn=Ff^1+h98y+(XQdZ~4$VH3ZE&NlDv__B2FISh zZDxJ|m1g#U8mV(`WSJrUBP_oit;neP{CNhH8{J*aEJp>G5knc0E>7fwguHf%_3CNQ z=lVA35^%7E2!&cWjFi_kRT^05Waz6)4fa~<^SSare9Bi#F5~niq}a2$!esf53oO6) zHqQ#MBc#-zf}XTPVpntt)3)v{B!hM1yXm5gleU;fmSsMdS}fhavdr+TKn%hF(Nr6x zQwS;@W6?BWAonSakrp4D^KKn?tM+Y&-$P2oX4(55{493#2_sT@pnMGv&E}aN^3$n7Q+2>(|0{sD-ohcYIcib!y%t}2+1zE;r52fEQL0>=nak@5q(}4@2 z+vaq39JVj3<+nMS=xuOm2>5WvX!?uISjm(eeeZrojY~t&8*9x4E72toeBY>+jv|N~ z3DeQNSIxLmx}?MkB$E|pzXS}ld)V~_kz-d9lvs99^ow%+_|fl{yexWoDYDnw-S$$g zxhM4HRw2O)a{V<$VWb4R((Ni71bAKW=(a*@!@60tz-wL1FX!oOmpCsJXr}5)5Xbnb zFIiqcvIeXxT|+=d0|Gsry@V z5i~X!o-ROB4g6+@wwtLI9e7J7J*J0dl-lm~dJBx*n^gVu(Cxt}s z7;`Cwv7@@P6u!uMo{AYIsNfMd={bma#=q)|pp_s<&@P#--BpA_8Fl2DOS0gx`=`DK z?iHa?eMeTQ_m^K|iPD8D5FhG^`$@9Fe?`V@ny)fLL=~G$oOL7go(40m(Q@FBWID`C zC`r4u*mEE>tqPPg5%;}OAz`5&X)BdV*#EKT{qcarlb89Hwd31uu^}G%t$xi+dprRvdNpzQ$fP{s5_qf_}a& zv=qTl7_#Spl?B>Sp>SQmAVTuV)k!bwslht87%Lo&=D|KG(itPH-t*KyUf|SFe7-8k zZ3vXq%P1*}*>o%UkQf6e_y9Wti0$hmC^Gf8+9ZDBH)lRVeY_yA7N()+przi)sW);| zK~Pn(L21vst8_EB4`RaLy-eBq(|*g2MowK_D7Nv`cV^W)01q5)^|76~x7KXvIw}^? zZX~$$)V>b;+_q-@=$1l$nU?;>cY(8NS3%Zd)?&%{>*-f> z1Q=H72r7b zB}DJ(D|vqh$02EPIlNU%qJyCeNl_|Gok3|?v@@BpA7gVLn%1FouJT2^Quj-E6D-1QE2ld__V9u7+HE5MsW;m;s{XbSGkr|XYD zY!Mx(&?EQl>E1p7=WD5U@Y9G=?c<`IRWAi*MQ6~%eF+JNCPXb9R9X39S`EhvPg=Fi zptRgurNhQJvk;5}#ST-i8gEm+ORSO}l&(;R;~8_=8p93w&Dn5hQEpbWmDHfLQb|^H zjha8ywuWJK!CKBAy8H<9%%Znu)p&gGL9x@?W9!cxsas9uB~aNXJ)5v{W=Lzl(B`d! zm?N|^i*39Eu({}9N69H%F9S}*7~`xU15P6$D>~Ot`9KBptiToh%t8_4ENP*8!zP*d zVE52fluEc#W3Xc!2kwnCCJgtoUo>WXxMg?ZiQMP~>$W=X<=8-^55iGi==BxPawNQtYPTQ#(!&q@5aX=+5PsmEdIu_znDCMCqd${@hd?a6 z$`<2nRfuubcoy#M6`?5AOZ7Fm4)}ztn!W=)~rCRyYlt=!H`+J@RL>@wim7N7bBmM!{39*@Ob#V$m_Y1D1esm6P+4EvQ@@rwp^?PEl=FGHy@z~`KJxWNL?^?VMS1ZjKXR*Oz@JP#o~26-lwm!C-{IW9izRXq^CL|LBp4y=0`wDoaH;4%H> zwaY|9*~_L&<4j*f*~Q~$xSbnJi5a%Fx}Ut8pQ(lF^`}MIQws0}lgbx8YL%O^=wDT# z>z0et_Gs|W$yPlMPMti?#D|X(OD*AJ1>cW0NppMfiTy#scl#R$jOG(p?&pUcyjjtu z!Lc=uUQUPIhcEmoxnna~tn$io`g$S9Dj^8`<3j-k`- z9$QqY2a&3TQZh>*#ugeUe8lfL5Ia2h*9qniMd3fqf_PnkFS@-FMX}TALvWeb+(|Ws z39S0q=r11DkE?`nV5sA76-bLru(kCqs+Ni}0rOCQGJL|F#Bn-JeI?77akcxjW#;hl ztlT85PH3_H#yH2!aiF^@gskKKQ6h)^r(JVU) zXqpZjLice6(w(=ww6R{D-;yTmU$gx*FUcw_6^JMZ!)j`vJ zDTo>F#rI zVX4}@1$T%tq+b7PR_BIKx#Z%ZD|tNV?Pav2{(vwW+%3w``i!;P9zt~Vd!CDg9ztg_ z9zsqE`a7kwD=a{N*kyuCM3&l7sC+2=x_Gm8bdUfRP0Bu!5Z|-rN6OrNpJ6EDyJ(5U zm)V8!`!HEXSzMpP=}-C9-xsLj$ikxy5Nwj%OKVa$JkCzDNxq%pBU_@rj@9WSYmnNN zJJ!>C$y>$ZgJ~G$=+?f>c)ZWQ^0w|tH;g1L+L3L*A-;}DQe|CgM@RdZ;A{}dUPgFu z1vu!!VVw?UIsrzv8oO@WN@}>~b*i2iS>GM+mGFWMad zW4?Leped4%TYl=BiyPEbv&qYJ^|&z<8o(g}lvs805jC5UTwqx_v2;8>u_%Y9hH@K- z8~)iA(&{}6@_WEaRMdAuSl*S|9Mz?)aD~kGa>c`@>J!`880guaQl{pE%J1JHl>~gb zYP4%9vJXMs-C;1Q`}ebVD|FdZ*$Xc@35D5-B*zx%KL$t>Ohl%KCtx2aozDK?wzK-Iy=+%74Hxw_ zZ<&1q{5x8aH1Y6Hq5Cw-y;3`{&*~q#1w`Gt^P;Q!-%qp;po%7jUoCu0844P{OA#rHI-XS zim2cqk9y;s97-vK|G2EBk?FCYG!PdUjd+zzKu9+ydXob_B4*#5zASz z+esak=XT}L3)K7}qV36d+e4jflBColq2=y@jc%0h(VvL=T;nL^M zT*oxg<+!t~LH%zU(`%BaKDOxw&_Y^{?GbHis?_PWKBZZ9=NZXLGzH`q#0eDMR=!k^ z7HuB#RCEJfu-BlR15Xk05LO4${Pu&s)0;{6u-O%&vTwRQcy`da&d%cYHi0)|rf_s- z%ud!?^O2p;KD)}3@on`IQ;&kGhwpn&w~sMQ>I53p4&k5Jdggbl?OO@D1i`oJ?=s!$ z>T0%JQAcu-UhC$P3JUK;?z;wl{oFL3w+yvip_SlEnqj=14@5aqxy|n|BD(S7l&sh} z#;NPBF$^VpKY3~IN+q4Bvus6DQWr(@LHcmGVzWIZ+bls(W6*#4o{-bAjq~Wxs1Or} z%5k~?#bTfX8HHd(K?>0`%v$#1Tioo$F=QB5ts(pM@NbRDS}7an z-hD*f$(gC{dfh2`nQiz1CFyi4e=2hhwQ=}-b1@csF!QQq=Zf7<&(O?`3c=cJDLwI2 z#B6H)IIdwLl11CRsw0loj&CFU!~L?Vqbh5X!8?cf%LF1It5>!*rFYsJZl9(#?D5yV z?8*g{P$!_Mu*sRyDx#a`1;Hzj0Qq|aqw_4_`T3C34CrWt+yj0%L1(h!0Q=$w$ z!;k<$+eDE-(w4&aK(#W&%FEFF80@fYR3;c80*z=vQ7;Trql3RwBLjFh7hO+Im_@*` z2sq8CZ&onprPFw^0-xSmozz#Riv{h@a%5MdzIYhfjbt%I!rL=Vy$@=xa;|YA5TSD^ zcFI)=zFUP@Fodm(Y>z~I=e;6ZRFE&o%uj2e^_|o#hKn#7F%cK{V4fm2 z@5{gT-B~~)22Do^M1a~uQnC?XQ8W(?;ny=Oxa!!j3crm==>uEUAw}4s8GL%L?!(5W zd$@rQIg~*gc5EFJ&W9jdS-qwZ>6ko}Dm-zTYUu4O9Pt93vTY4|f_6F)rBGF-$z2kK z?c*N6C;B$Iquu(&d3P&&W7(!(5Zf!V!HR7&gHp3wuAl<{&knkh?9CO^0em7t)xq-u z+zO|6iC!Tui9@pRw|z{iL=+9!+kyUVu3`WCu_t2K9l%Nmq@xOUaM??rPo#D6?Q@?x zH^+Gs+j|D9Y1U^-fP3&vGp}vR&RN1m;qGu#I>jH2AuAxWvpY$}JAgy}E%h|xr1S1X525rTHn?mW*e4Gdk zyfZ#J;|_v-!{|i@94MR6?IBHFgn*~=vnt^1p@1|9<`+x+1z|Ai4ml!UyU;4~chf)v z&|(f^l5Sabi;@wD2sYP(5QS*bffNQh)9_6F2)KR6i$*x%WS-?cJzFi|)>OtS5YFSN z(%%4pRrq(8?~GG}&|)NxN*eZkGoMkoBYfWw3_t;{Z(ZUg4of)!*9cFZkSTw~URw4g z75%Uw+YMYL37Xe$%#PRD(!QbaNJ2QMa+lDZ)bVqjN1uON5tj)R&y{;_2{n6}$C-bg z0$Ep4jL}Ufw$EJBC|(PIArQptFx(4`iR*(d3MCyQ%~&ON?_IrX)uHviBIcQ1a9~&% z&u+`L@O$%tMZPr*; \ No newline at end of file diff --git a/toybox/VirtualKeyboard/1/assets/images/keyLatched.png b/toybox/VirtualKeyboard/1/assets/images/keyLatched.png new file mode 100644 index 0000000000000000000000000000000000000000..da8b08dace328a9ff1799bdbb145b8b70b613c49 GIT binary patch literal 16230 zcmbWeby$?m`Z&ITbc)ioNQuBwOY9;oozmUi-HifDclQDU64EIp-6b8;-6194^*!f( zkLUVb=kv!8uH6S_=Dug&`Q5`CQC+0u#8CIpP>`9~+&QT#;VrUWDjF_IhVDQICwntX z8&jZ*r-Lcb!`1-+@W?pSm8mM_#mclMa7SNdWez1X_^!%-uz4Me78r0t+pNh3h z^uZ?6_#uJ|?csc8<>LHY`F29ZgsLuDm05(u<|noQRQbs>pRind$+yrY!y%GR4>0oCPA5zXf$ozJ!wPE>LkT1 zw z0Z@B_EoDD>9-`47ilT-oATYNC+A(v{e3Ry3DjBsll9Bo< z1{-5g*l^;WiO9iWAqFGw!>clzQ}PQ&VMNhr!F^v}8Q1j`DT*?rbAqh3Ni+outWH;&j&)G}}tgY_w+^Y*_`Pl{vb$Dgg(OkkShd~ceP(4&bP^EzCZSTW()f*8y9gBjPU=bM8SHjmFU z7Q`sN@9EhY52<5YW_dvvYGE#MT5OAM!B2E*l0=qcEW}0-@KyYOGS24nwx_}r#hJKJ zHc`bma}L@O-a>)OLLqGx4X=?Ep6&2I^fvT=VVeIbjoZ~;zZ4*e^#s4a0^wBxoBS^g zx8jVlOT4YyWsJ9@S7w+?8t#lMR2Y7=R-puzp2NC0w zYp3#l_y*gh{TI3JOdcUM zX=EopBbK3wXKLBWPRbq%6t;VO1SRpDW6eB1n^<{SR~P9#zNedhCT znq=dB?Znm|NNs|mQwmKm^MZOhnv`{-9Z;`scD+V+&SpmOA% zfofdcnDI)fyq~(E50@fzEMigLEt!7Bg>7|u*V^d0TI#V}f28|FUk6t9Ei@1GYud1e zdY4~81=K$XD@?b3bWF$~4_kjJJ?EPk#KS?-c06UTDePmK}Tl|}h<%jVF-Uv&V4 z`Ey3bFC{vP4T3~KNaoh^WL#f(6zdrqfDzBp3GL@|k1(c9m7>nt_BVIoP;`aCv?Q6= zrVMWJ3^vky0-cUmtAV4TD1NtL(0$|D2!(!6){ zKG^jvzbY|O_{r2!u$VNhhvdsexN))?UzZ&8lEDX)D<0e$PWANkGKo}rZw_xoS+t9B z+bgBF1h@n#_BghRn*e{bPw{yjka`2DtMAIKrIPDXs$p;gL#*^D8XE+Bx})-u{w!1UAT2!I6pKN&dYIeU&lAe2ujnMTQ{}yFlvXrE{9z#31nwcZ;>^>k1;xA9 zCeM|dNgvZb9cJ9DM9&+#Kgdr-wH}C131E{%hOex<9{31{q9F{GZa^h5Gs$CpXk!IoH zto_|o_PdD@-Zb^QDe<4C{~q-Z;Qy~t|DgYC#(!}AuSNV{qs}4V8{^iqb@a1p7(0$& zjm#tBN2YSw^Rk<8_)&{s?#LuQ|Y~nwz z44&hZbWJ<4S9Ut^l7|EXBRV^PuZSXq+1Y|(xiE%}h~IUcRjHs28!XfW8Fr8}6DC=v znenE`lNDMJBQ_n+2_AVb;hBiJ`6*8Q^n2X(dT7#bb3ZC7wOjHYx%pU9^2+9JKKfy+ zOYn3D1@Loe#j%3VtzqWGw*oiq1y`IA>44rVipVV~8CpoShR#)gC+Zx~~z6&CEqEgPp@;$zu+%Xa8^kPXy7>=#pr4^*=N6U~XvyXJiM|4MdCzA za-{2C&y_fDdQ19J#b#5{_fD(Q?Qx8bm;QGRfv}FDI7q} z_Jf`h34Zaj<+8b5qe-T0!6c|imP>9y9G8iu*>b=OpdO3h9!KRSc>-=ZwIWHx8H;G{ zNW>VGVMg=8lk|cm^3KABn-DN4TNu#Z^dM_h&%=E2AjT7gB_0&RR-7xx*J_Bw_ag%< ziB===h)@zU@|tI;lDK8xk||KL`!cI*o5;Sp8?&d&1TAf^>3sE6Wrm)-pM2cX ze|Wpb3flT}-{XO-8A%F54?`RI&0Aj|oWs-YRqZvERUZrFjB!^m03Mf_NM=@Y(uK+V z2FXztUYZ7-?PX~1z=J5704XUgqjug07ew^=T?D1Lq}q-<)W8}X3kEjkg(09~K7|`b zjrw{Tf+e?YECrBri%uC8Mv{XW77rt%6;_gG#?%m5Jbw3a62vc*=69&$)}r*|#?kef zOu(HLeYoMemb2ke<5Zxg=V(*`p_Oz-a}1sTlXh^bwyooqI)Lj5c*^@*Gg?#l-GF0^ z8>jhv(9bN7ZQlgh#D^}_fSo2)i9z?});n$ehpuCtmiSRmIG3}KMg!qYSNwW@v}9pp zE!JCdkye5&_>AuXP-)iCQgN6W$gd*Gvsmw|6i3uB!-$`vPhs0OvE`Y((5b=c^J&P)^m4n_G zPfBcp9_1h@vWjpcsKufrFxfML5tWv|-MFO>FD5QRg(!r=fJhF=IbpsRATzue-tJw zcsB9lM~fN=2g`QOMLeBcA->71Y$^gz*Vr?mv>KJ*DC;)LCt8Qa_ZqzGiu-2oK4SF` za@b&Fw*;!rDhT%0PRZ}XxEDA)GrjCAR4N+V2h1g_)g!@@$**yBa8O>B8f$5hvw_%# zxN}00j{?jjzmPQKcbN#G6r>p%w36|rPtoH9w}>#V1MEX69w}^Shyz@On)wq1p**GLsGs%b2IT+-lygMU<#S_Uuk^d>s?B1wb4YaDc3| zP8Cq0FOCFI&4M3E`QF0WM9b~BG5NQc;W;Qa*}O!Ez!)e^_bim#n)HP^Ela=?DGqvc=o2L-MyWt%$ue*- zJ-A%8J3zvXlX*LUvM?37nA4XY0p>c!Lec75EW^N6t{LOFTZADwCdMx*?KWV-mUH(K zwDb-msRvw!M2a}h=w8#^V~a970GjxLH9gd)+X!MEAhR(gVol=#mku{ELUF*8+GlCa z7B$~>dAzi}xys)hH3-7KxL9SV8P`gr?GVtk_~J&0lUhDGT$f8nSQ1x>M$9h!_J<(F z1g|}zUL?avQbdcRq1}ojT}0AiTAyI0;NFW${@!HIN=FN_EY1=vx3pE?;ymgg+?p#Q z(IO(XA)>ip!D?$!G!vP@7}; zeUCETh*VaVV+gsQ-Cd~KOJybqOOgQfqhk!EWg>6NXE5gz+I~bf1HJ<`3)z|d*R#W< z#!qULuXhl}U?bxgh2Mwzz$u>>VOS}vK-EwTf!gJEH;=79bRp9fU;g`wZ8w2n4!pUZNf~oFDGKQ_HuZC?M z=L%LdkC*+=xr)sarqY*=ewKyP)8|jb?#Ed{6nZCa4APuBOilORuew*Xs@GYhxKPt5 z*~>SY5Tqjv&nh+ZN%|F|h{N>cS&)o0_J&uhNL1458PaQZ-ub%Cxdsa=x4d zE#n@zo|fAw0qaxD0`*e0m`w-Ww7;9vVZ%NyV7tASve_UiCk??%PBfKkZlSR{nShak?< z)FVvbc@D}+^h_`7Xv%3n{@ct$C*k?cH$5?NOiKKBB{oJnP`e5mYWLlnV`UH~_-+tp zId#MLIAH2KqQ4enQs8w;MhnaCO`LEWi&>QyVee!CBYNiDBRWg#t(wUSRhq;;5P zE+cY($>MA$mw=(BjH8qxDY+$)^2Stwx>i7Mzok@MIYtJeV4MChOQFOQ>M6|>P;T6Q8aI@wO+BgI*D=nf0804 zPTnoRLbrq;D6H9rdZezh>so}2`Kmm{iO7+=L0L~lxx&40VDrr4dWg;fRRtSi3(jEv zf7`j=Bz{8vyo$A2tA{H`-T4+PCI&(Nlc~;~YZ=#k9yYYL=B>!sn@M*aB}9@!D%JnoK~t*%Fki0O3OIYd0Kd8>34j7ro)zJ|Fvq$;Xte>#q{9Fk=sl@G zW4%cdQWtJ0kY)nJUohI_lyW9dQ7c+!HyjUIj`sQ1&8N-CSvWei#Q&K@x`0praE&E2 zi2qaXfyb{mUum>Ty-8qlSPYg1+_P`JC~L%I4%io!lrS#h|Df=5NZ8hO!$x$s{T9ku zMW>Q&IhX&lA)Mkk3Zdwlt##62{d)9FZLb0O1wDRFK^lh5*QnLn1^N>c=VhX=Q=_%N z{!;bT9o-$@$G*b$O~kIZ8m4aSJE`xT#iBim>hl}e+^sJ$5fLXDu}y?qL))wJbi>^s z?T>5k?52@woKh(!I^Vu>4d>n^?*lK;;l#y4Za;cUJbi*7TdiVE)yMT=kU520>I*td zo799*`c?ZBQuE@(N33+)Ej9t({_fWpJ zSH$dh!NvqAP zk<{0`WBKezF{Tg01RFD)H&eGstV4JI?F9U$_BxQXE+m3hHWDo4VsDa}iV%emDMZ6g zdY)*|W2wtae#qWhwR>dewPQM|pP9Hc@nZYe*(iLv`MY6+^r;#Z-}9LMou`NCnQLGc zmDa{3?AcCbA=C}`bk4ZE0lS2EQ+=SaRqo4-oE6$nvEr?Si$~0f5##7i+{Bfi?54uo zt2r1!1ZFgcb^#Gg%~BSGwA8^$=tGW~;q91lvBYAgp0SKE`8siqV~!{nK*(3Hk6E+tFec~v7q4%U29x|{8&xYE^$HOI9Qs-dYHJ6 zdXD~*gL#;ItVZ4{8NBmpd5XiCA&rfsn#oJFq#!x!<^i;fiK^4$T+!r_&S0xlU1fcV zGi(ugRpvR2)n^E~7!&S+C3=Xp9+-uA%t4Ze$>@uSyX2*0kYd2Wh2%p;bmGbXby!)Z zE{xyAicLp9b}G;b`e1MiG}q;xkqY;|vexC^=%jZKZ1Wm&ZMp3+bANp!rhdhKYUqBi zAAH^B8JGYofG)^$1CUY|>1&Ij{B?$$+^$x=@vQ zO&{HzV$SD1YdVgV=Q-E)<$9UUzFALsYft6%bRbT3lwS$wQQ)jmpWm7MpF80y!~;eS zyZpAk#D!E6>3^$iO0|qjrswG~ic}FASp6N@>Fe^+*g-&ZZYt2l|E*X>57+r^{t-f< zeDm>=6RzTe8D3Mt%%bN4-Zypva99H&^U@IVCb%&tqbFn&{C-5$5YmI;{L!5vK#@sH zmC(xz)ycKCrBn@TmCwt%4XGh9r<}Jfuiz7UmY@xS5wOo_HllG z+&{YAFK>>XNow+#d5q)xwIfe-oQ+i#FjKI!%`^f5MritO=IY;!`DtY8oG;Q4j9ria z7|kw5`OCBVmpe^a47iyp?qTrD)44p&F7|fSMY6H*{`m!!z6%ALTf^(;5ze(&=^XaZ zl(U_|*E9R2cAw2OPXC|&GtG4QbJEv0$C-}w_&1v7__>!I!Ft$3F9?HLUo_uYTyKwV zCm+7Q53_LjIjL`TOG^K8lwJ5t&eOq5=0xIW-e5>%hMOakcV}k#^7h(wY|-MWvHRl9 zPx}U^+-&Kl)RX*zqv%V6icUU5e zxniAAM|V(NmH&%JeY+Q=e7;&!Q3H-}qw z#mRvMWE_mTO7=6TGc0MTEl*QYKU$N(AIL4HhL06Wj*jn)ZZ1;-2; zj$U7sN7I2X-DLT*UI21yNVsy0(6DmZ>;AYo!Rq_of-N3rjU^nkJYKXEQQee?BS=ioq~;Aa(bhL=W1h#y&Bc~svEB0>2rs(!R)(g!LNu}!FMuQ zNUU&@Lzb*@@(O$A#KrxoW<@%PJj6kB)tv7B)CSOWrgniD^V*JnAO zevdEbGXL6tNX=5)Q0cAYUoP$%pN<&Zsajubp` zTHw^&bQy)bLfD41tXbDk$)ek+SI2Rqj?<~x|Wqgcj`>ZGG5vM1DO5!Gk&T^??^ObD;1 zzyY9{?cLxc9~fo3$pm&gGN&?mR&8+NdHPK&mxcnJ3Sefc&tIcn7s2Cqa9wk1njwFa zJhEwh7$4&e7PRXX=9wcp8%6e(R~C|vu|gYA;BNRV^C0UWN|lrVGgTuZNwPJIiI0mB zfFR6^wE3!Wg3=SUyhuE>xr=6S>614}kGh9r=|`A}bXvHOZIJlUb*6pm1`u)*g*C@y z03&X1Dy;CIR|c?*^0&Dl*{IsxDPc2b4*K_V!|9~`H=Uw+!w8|Gmkw#XmC3;o2%HlP zuI;?Lo2~^H6Qk8ly~DB>$-4Y>`csC@0yDj0c9rktzFp=BC{SSvLq3>>m0cjAdc^x* z`$=3Q!3Zqb=p_1L7D25pFEg$JNFLp}q2B>J7P;zFJ0b zhx3+-t5ojx<5ybIDd~)N2eD7eK{crY%oc=xF4b=z>%8_83knxy3{YTNA^nzW>5ZM_ zfL@_O-If9^abyYyDTb>@YSCwocd{oEuhkv+Ff;p?V`^j(D#k~iu2P-Qm7I_-7M+oM zz(Y`jyLygz*#2x6*GcG&FKO_=pWCO>&4TJMzyL^bvQtcJmBmQV)t)UMnv-r8cYPqk z=EoMa3|Bhh5go*x>4p)+E}ydpoFi1JQcxl#3k2)Tt3wa`Wl=BT-9d6Cv*me}M_X<< z6mz~XJ`I`Fk{0Kktyr(8as^Ul=KHl{&p%|kf;TJ;FCcr-emmtIOu()Y?)azk5Y>BMlH?llCnSzC6hPk$#I-tbTe^{XIa_M? ze&}S_)L`*iE-)Oi?`<*9bPngmu5b)#TpgL@PQVqw(twb-NVKTE@GwU2)yVx}?){Yy zBG|a-5OJf;)V!wG*x#U;34?j|2FQv0>GU&Kuw@b(m2W27pSMm0k;Ig2 z77{42Y z(GR#3tPKHUi1Q}^W_s8-|6p>i-^7LxBI z5t5vfFIN1a1d-5qEa|W0AJQ4an8i8Amj?Y_(q+fiwmUDHGR4G1TmaVRD>8+sSjPZP z+9Rn@pw-@{*s5vkr+Ojn-|)%1nwJStcsp{A0 zHG9I|m>0;y7Fe|14H3;h7lg-nrMzwDRGVb=k$0tbw*w z`-eWKAsN{h*PcOa;nVx}bl&57HAKOJI>%Md3^m_s^x)r9chLzBkG(m*ZZx2Y1UEU+ z+}WHojruM-J3Rg|6Z$9->ju>PJrX(hz{`Z9cfQYpTJt;iKc0O=eAyBCzW=$NV1|j1 z5rhubro8Hxy-~k@tbII_^*hI?FSZ$ z_mX)Vi4*8y0%~VQ{Zm!vKTeA$k|wY|&(~O$d`2384jd)X<=TAuZ0bs=cF1DQobKP& zb=xu7+}#W-ztGt=dugq4X*37^-1PP;wW_R}SE5H+T4u0a$nUk}fJ?E(ftvF2pNfALFf1x@z0{=x7x=DRaU!xIs zft;0ANxR8K*cC#NWiAFxLW>UkzV9yzeY%*lsF?evPjvkG0P(jY8%Pq<<%CA;94TwU z!}%>-iUs*Fp#}_);(86qTLXeSQUr6?E*Vp+Xb;n&=`+De2wyY0S#pL2J>iG0{~jQ+N}Sa2=1|NGhiKOYHo z8g4fk`-NdjP>gcjNulV7Dw_}nq_{XkPt_Q(s!Q4vM$N4kst$+He9IDyOwwVFnQ|)c zw%|MAIP`c)tEIeU{o^=#sqHno7s*4MW=X;A&h6e!ZtxSs9_%z9Aw!qo1D$x7 zX>x1KPyd|gMv^GMJx`~rQu)C#6ZaCxh@hMEYJBcPI~)A!G4SN%!+KFbu#+Z{Xmf3);{7)ITXH*yG-lk*YFT#Hm(l5 z9>s5I0e*B!|Lwvt;rt4*i!#e^&h3lfJdQC4!>lOI@i9_9d2M>^E+qH~L#J6*F$8~v z#Jy=m!|59xx|HTgsEq^t%dK|{#vBzb>p}iSN48nP-gJKp8v&{yEIt1wkB^&dY56UT z&!hDajzeS2i6LeDHA#b;Dss6lN(Ggqf12+Pvd5j$l$=;GOSZ0s1D)oLPw=fU1UX7I zIEItEuW&~msp{cYT)Xct!?jBNAE|OaL27~w{N6TuG8ajzBS>l7aC=@=>39*Am+h!o zvQ-1`3T&?}1vwnPx?694blOe+x)$iciVU)(?^B9w^cai>jVSHvG!Mb~M8G~$%LLuj zX?`&SCzR~{Qd2$rvQ_){-C~CPKE^I8mU^316}mo>yyV(z>O4|g3SNf;@WBBN&~=*Q z0@J6O+NYUr{ZMcHrv0-Z_xlRJp3*x=F=6U7)giAtE>onp*D_gUSgTtOHM%7pIXZ$! zT*`H0i+=en!r9^fkH+|}ucA6TgGu1a@?CCpD?4%qDdGCeHA?cIgg75+`F?{#Z)Jo} zO<0fA4i;uuCky-)b&trXxQW!a`SrXy?K{Gu5xYZr)%&n_EWo8W6~nDpOEq|?heq#Q z#uZu=iAblmCl0-Q(uF^;W8ts2xCr?z$}eCJj?74-P6GLBq{t5<7Mz)O8f4a1k;UP~ zAPZAroQ*q#?t+JD({*dd?~Y}-@?k)y9#{cJXBFk|qJrmqv5hwRG?ttO@^8zz-2DAM zLVpL-qn?45w<^QkUEn(|+k@|R(-yQ`+N?`1v*J{)I}Vpj0+NSjNBi@?Dmvi7+>U4r z)Z-spFX0m0xUbVJ9R-;wuhJe7+v`vs5i8l-hg1Em-1+c!L~DD#)}z1CZB89dPyx^# zV{(yeschfNE-abongMRJAcQP;8EWP|HGUzcq~DS7F_w9lGjP%-nO9PLYlkV#_f03| z)UZ;SFK~tyRBbR%+f5&1XBngUvKXL@dtemtr9JB6uQ<5q7fubd7ud8A6!LB-M&fp> zn0+&5C$d@04CJyex;J(}5ZA=UGSZlu_h1qNLc3eGYdgK`a-Cs{K>|`jYCOGJ=%=8T z+*I4YLg1s}USiu2WBEo8iHZh~ig~!eQ>HkU>*7+n!@s_6=rj*J7Q-!r=CMvQ?Ona| zpOhg*eapFOrWE_GI5x!H`H;gs(^|d%?e_d-dxLZAM=mH)v4r1(sj|StmQM505gch_ zFwnr}OBqwseMAL!zIkPA-?>&K*IAoi!)Zjruc7KcmOZ($n1 z=_inq`J5Mfz8EwMCHz!yBiaV1|JR2mxIitVY|j@s{vNri)4ZiHp7-qbU`* zY-B$GXWmeWcGsjDGX)1lhGix-RzOpx&_FsirUuY4q5MPO@Y7+RE@J%#qE`w9ZNL-0 zS)2&xfV8!^XWA|d3_>NFbWp?{U5N^rgkZZZh?%3`W;L*4LRz0aswsI}GMLg$#?dJl zXH6)&dJ?A0=Yg>k>$nEg-Zev{rAPZlY#hN}7ym*|HY=%twm=y;4A3=eT};BpdV@ko z7mlxe+2VZYQeYmpWwjJp-zgnx=%UM#d(?xX=xfTmft@zS0 zk&&zZnEn-wmIU@^m|J5cuqMl+rP)lV_Ef>YNWRs&){6;Z=^FD|JJ9`%kdfa$@P{rj50Sb`n${xK}Tx$+K17 z#{NU#F}7Z(MG*fW&i1qG>8~+H_xshi*Eex|2zX|fiJ^X*qIZU_PQ%X8Dncs~p@RN} zr2iG?N6dq}@xiD*iBHD*jKPHQLd2W&X!EdrR{^hZc`XZ#hZ?e!*)Xz{dK|0b@)@M7 zoQS~)otL|X?^Bdc!_&si4#P9!<0lvwzVffz9W?W~ib&XcQST+CBnjkZ!s|f17Z2hi zF6Nvk7`t+cMS#HwT+j znjBS!QY3r1vKn|FpwT*x5Od6hgl`2 z-+^->)W37OOrT0woR*0tS{CJWx%)*u?4aXieX)6t3&tnZ`1l6e%`o;M$?Q3Vv!I)m z;TI>*Q7sU@4jjk-VU*e(|Bs-)FbeFW2!5~@4`+WO{A#(Y@i`V=TBv2=%$eCNoFC37 z%+a5YWT&t>tVWb_Lt6j*tTm^4QhQsMB*M4z=I!=2xLDJ6S~&T2u6ZzBVX|oYKxYKI zWI39aTwaF|VUl0a^2B=tbOLx{>|~@M`*(0QoU^?%g7vK=^n-QXPsbUwC-AUZ604kL z!$smW{UK9Qzs2`jkSm7*ihpx!$G)bsR!_mJiM2H{xh4|*i$XrXoSyFHyyfn@K=GNz z&u1xjew%LBw?e_*EZ)v% zQqJ%kG)?#xJz&!AgSZw61`@M4eSt)|I1#+GPewK`!uj1uANt|6T4eH6GucafM;Q>V zt)WI;f4fMwcDZH-ptYnw;|}?ZZ&4PUotMa~=`Dr=)5obAo_5~jjQj4zGP9&rit0-G ziB625F9=pZk^&3&Vq=_RAxj7>1l$fCagtTbV$%Xma7A?ZATT{-+5^>@L;BvQoK z2qU20na=V00q?o;{u;e`O{z2oi{zXwN^nFFcU_LeK97YQz$DdULabB zY+@6yB{fbx6F@8)>uE*Dq8cmkJE>8-p-ObD$`1dO*@-umj1IR9-s-l<$Tx0>jUcyg z{*~2W2g*^0bSO;}ZIDYgYk~>>m8AR2RCPGvY-At({F#;Qq3YU8^_{NQ1RNAY7$+y40w$l0=zheGl#2=#y?U7>0_tf z+Y=wq&3eVnQsOC^d75L~MC*ZH=N+2ApnRNN%o8cdTjum}(&iOVh2Wv7Caz86`qAW~ z(`gdlk?1%&4^FH$NH6v?{7N{|t?eZ260{92#G5e)VdgxGn?o$s4|~{a3Rk?++Pn%? zKAAZ7Kgyk~|4+?tR&b~?-Z0lz^d5U(=HTdp<5==R`TecIs3nFZde!UVHCq`rBU(Dk z)X4Tq8_wXg_;(lm=)&4N^`qiQ0n*s5SPmjGros~w4|V&~s59DG#vh~j)QdL*UsNCX z*!y{d5VQ&>p%`xSYCEe{Hs2ZDkampic4=oU_%7MU4mXiz&p zDlvMu-|pz;={I*>N;mR;jS_chqLcSc)3@51h}5^K$j&kxXIKu}9Iug_!m&=*vIu5) z`Mr5Et9uoyHY z7K-d`@Lo{pcdjUB-`3e~jxycuyBT)?xR00O`JT0UNhPW~*oo1LWTQ4LQYU05T1?;- z8UKF@#n5;)l(p<+j(yBQw+vgAkeWhi@5?Gs(9svoJC2PaTXAh+)_rn5Vf`N=g~Gqn zMZy!^Oh9<5h}xIFONnIX#X*wErxAf}g6C<~SMCu#&IsVGU7_gVArv!(-w zKMf&s7QfPr0mYD*%byA<)$KINii9BO+_!_uR+FMq#wc*- zQZ+g$JESTEb9`Y){`aCfQ^++rjdVGaUQXUaTS^0{8n|&RH4mmZ~1-M^hrqn3t?`{+0{J7ndZ)8ACd;S z3nh!1ry$kFgSQxSUj}_@w`*94Q*bb_`E-^nPu*AC^c|OLqrRJ-H<1)WR2T2!5_Do~ zpI_Ig_3$l)=hqH9JWNq4zve`3sr-#DjXt|AooXo98~No>nq#Hyp78Zm|g0bp6QHK9eftFGj^zDDzb@Z_2PkQR`xl$H?7 zh7erpn2xNqiA#&2G`PKi2Z-*fI)|6-5D6nHddT}JEf%!ZwL~RDesKZJgFjC-+)HR; zY8CkxS0J#Sa6tp1zv3uA+G)N0g#bv_%tPfZZY%Q+s6DgYMHo55okNK@zmr{$dwT?M z|H<1cY>z5M4|vuQAi{3B2|#i|2#H3BS*`9qqXyDZ#cSXRBBf3D9+FEY)w~IO`&Amh zCS~ZX!AyAKjCBagHkwqE@D-u-O6A^bx-H;rVzv%fL8O0ePZZb9ytYr!K^*qi);ir<4&g79x+ufg2`D6n;Jzj$M3i7}7<~q|l_PQzf@@CX+nW=Q^<&Dg! zo$7e=(Fs72@8+`JUkCGla`zF)a0~&y+wMwL_%wE~)>>m!cp++SnzZk&>u$=u=dEkL zdATO+Bn3@D2wlPK(0*DZ;?qeRpLnWwh$1my4*}uLIpSb^F4`v@O>g|3#c637$D{}q5z+V!ga}m>UF)?&8 zg&I4V!XE$*HV$qUHZTi2uL?T^%FYgD<6&ZBgR-&3bv$wUF92J66H7DC{|&I7(J}%D zc=^u^E|#{Y&Mt=VSAYLC2`>v9w+b5<6by!fdH*%Z)3VdwZzw4sc|3aqhU}$U#U`NL38;AG(wS&L< z{vy$vf02nDPA2#ccvU#qq3mEN7let82Tmqjb{$zb4EsO$e#6*1so2}w0EA8KjZA@x ze=i&;DhlLaV+R9Ys$1Hb*t&pz!hv7?4J>EsVhCrB;r~5ls5R%{pUZ@k0?wHK z8>V^Gz!eTd`1eeTmL8@y8gJknvU9eyw*zoMq diff --git a/toybox/VirtualKeyboard/1/assets/images/keyStates.png b/toybox/VirtualKeyboard/1/assets/images/keyStates.png new file mode 100644 index 0000000000000000000000000000000000000000..15d7672a57c55b1d570850857408f2c0c11a080a GIT binary patch literal 29322 zcmbUIbyyqE8$Jp{k>FN}YjG%U#jVAmIK`z9+#$FZYiXf)aEcdgLvRRE+=9Ei1SsyD ze7@&>f7kW?@&0j6_S&7z?CfMRJM+wQKli4bA>-;0^%4Sw-FUXQ^p&d+rN=yCs41^?wFE5H+Zz@*RNh`^Tqs}-{X z>b*!5szh(~{qikcvOkg6+4NXsKu3lrgy{7ATnM`%9sKG}g{>3*B;c)FXk-7|-`zt%H> zy`fT6>d6;gTH7oC>sx(q+eCmVaXNq1x*f)`*!h}oYuZfG{;9!Yued2b9VsxFGFmhU zQyI-H!ll(?GzyymyK9{>vhruQ{v2jLo%R!0`sa`R_b2RUPVMjLBMl>Gzemfd1KO-u za??Me(g45YeQ2%}ju7XgQ~xAC;-$p5&vc>X;T#qxn)t08h|fPcf_dOqRoPoV7eVsv zZXojpFZnZpJJ+5mpDaazWd*^{y^myHJ!xs&4QUWV-C@hjJ|Ffhg{gDS`SH#@@tM~# zV_)*0+h3e2I(H_!z&p=R*GMV{M3yGM0wLOAL4uq5{xV4 z=C`;Y&BK%eXV6J^WAf($ff0L3YHy(9B zw0d>7AdJ}MIDZls8Fp9r(%2AliM5_N3Bl47s^5*J@O0;2&|JzU^HYA7`|4Sa&!wQ^ zGE4zmHgOR_%bSgTq{d9RG@v$!36UQLzg2WiUe5HWfXwMzjYE@AX zM_Uv2H{zd|+|14AlOsKN6A6W4QfcRMM^(nKG6J)bPl%(9?fIjd=g}9D_O5QY$8V}c zGPJOFr%w4UaRX$RFWTRtoZ8c1;B;R&Com+U3miqd+0*1Eho_>p zsmS!gCQ1MSMt%5ee}&}!z@Yt1KLrh5qBEh8xDMi=5Q#bQ4U>#ly<-~-O0p%OLmgds z`}0F=UCkJ_C6>Fh?FIwMp?A7r-)<(>G0~8>iK?$0O?Bxj9A14!f^JFO@;55v11Uu8 ztLTeGuDYJP7>TKR;d2DlQAU0d`hJ|NA}cSI+_rOm9&F*&T|}#mGHf_}cw!S&`oPW~ z@j2Jdn(lhE6g_inNN8@?fZ4pJll3OnO0}Lrz@DZxFh}D$i30(gY^s;o`1sr~lqu(P zo^gLiXLFZJgC?3}v?b_i*)M>3_nkt}K0TIEg09Sx+3eLRG_wk`VK+hfg=T#)W)UTw zl2%l*0^f~SGS{T<-;V)T? zsH`!3?XbLMpS$wUEv7E-UxE&LtF@FW@hQ%861@8KBw;o&<}AVPn-02U4{SOh8+o}) zMqDkH>1lkxWu@Tl|4OpI@8EhUgkZaC&P*96k>S6rm~QLg%x^ErDUrgUXOidjrt!6D zg+g94{6;fsD9Ks=^eb@0;EG1aQtV^H_7kyDys0;)b|c8$hxF+$)hT9lC^WoHBE)CPY<6!m! z7_=0Y|GRgfU956ey;odrD0g9T6g_2Tbmy!C#fS06? zy7zg;tIrZ_lx`foaPxCf)=;{z(TJ=DA%kxZlOp>--3s5xNeFe4IjvYreY>Y-(3cS> z3qkVQcjj2eSNSgKOtKKv0~O|+6r<8Q)8ax5eZ$LipY->jacMfZk2@=c{S>t68Na!yi0ztCS?!2eH+GV9|OShjR5)Sg|gYR5fUAvB6J7K zqOWM5edHu0&tWg?lKWz=!$4cM4G~wAwkZnt(a#I{h=;`e5I8|X?=iZ|>X0iY;^kmj z3@GOrryw{qqX4ilAdZmA&w2PUiacKbp_Xk?rA&oCML&z@vkX=EMuBc7e@Vzm9NIR7 zfxZc-7Cd4+LuIpS?Z@}NhObcWq`@_xYR~B4p8UETb*95pINaodQ2C)A_2U1sxR6J|3#_P|LXeRjsL%O{h#Xp_ly5eOa1?8Dox-*8c8U3*0W#4gV7pAjEuL&Gn(qI z`(-fMItFqrM|Jbm)I%Nn>6?lCLiEW>6NF6V{y#f1K|#6u>(`5d0-RQPC#UDPfOzx> z((^ai{RSJmHsEB`C>0c@w4tE0na_xerI?q5LY*;9a?M2)$&Avh=AA{p^240J`lrTH zK)C-Hz55#*%Re+peQv{_HS&7#HK8@D_4C{c1%FyB@&b^EGP+Y;!zYt~QSTsR?7Z*V zAPHDPYqx|MP*@;aKW=>0D~{Tx$26SYKcNW@(T9=y~`rTUyote7to$?^#T0 z&8@$(=GU3v{*2PS^zS9I`#_+YoP^M?m$LFL&{t*C70tMHk!7slmvPC7Bn+Gyb!ga# zpHaLoX*+Y+0!`x;#m=@DS3fZGV8;brp4*Mx-&_tq=1Ps-tAL)5f)yP$_98Z|g;eh& z9zPxXe&4)EeU#$SDdez-SPK>S_O#I*e`g)Ag%5i(cKc&#hgF!-H*posYELkdel)w{&QhNnieMqK% zO#AT0cQxc`cgapa^?ulOY#>IXRVPjm6N9Y_t)OIV)rcgZ+;{wTeSKni)w{0$VK3qF z;pq0H_3AcTrvDagmv9S`cW)u%e#a(bl-p3ec8r_O`&gZ}01RXz8OY8ux?;fDKO`h< zYoT!7J{nOXbDoHP9671E;SD@x@^<*D8($Y0pQWiw5N|)*MV*?p^O*y1SKO6t>b?{U zn%r>xc+Gv2*T`%$@Zc{-$Ii$4tu8cb^QX|jI0mLpsUf1e5AT;vXGo_<^WbrPVZKj* z26g*GNPA|1pxfKy=7`NYy{t#=#4~it<9O=AYSnkyRlNH6@zeOf2xVrS`iD&k^DiAF_Y(sV=ZVjg{(cU8%OpRf6Pcz+ zFkFiB&@*^A(#+wrb$RGFCc;2AxrOp%cA}%_>36zQ_Ph7=c71gULu54YUyr7_tH;k# zAfAWipwFldmC^7_IrfVn0E>?3d^kEV0@doBi@c8^CGwu5nklRvI*{*9`n6sA9a-XK` z4(ogZ4TJ2(hFc{TcP*lnFd&ytnB;tZ-^!;ZEO?|X8qam}eytehJ$FBg>=NeshBAcy zTYP?|e{!RLrho2Uy8a&1+u)Xm@Ne8$jyyfxSS*0G<0UMVsn zlg(?Pktb6JuUc?phio~pNkfGE+;Q>C6u5*b-?a{4-1`tgUml06+(#S^(9vfbx~i5M z;R{K0|2T>qp;9h)sTU^Fp7V~<(N!RR52Qfbnk>jq_=$>Q;_#MuU1&tLnhfX~#w~x$sp1S~~ z@}BZ~KKZmi{=MwCw;H=b{Fb>(vsF{I+|Ht>4)MStv*4f@lE5#psu`id!t6DN7a2@q z*Yi`@LkQ~MAw3zPeaz}Umiv}>5vXDmmJaFSKP{-S|oK!2IFIyq3 zR$q3<3OTCQxgJ+H!&OdY5}s>P_>^hv3lfOoWTtCyP13oQM#iD^J4J@8=;@f5p}YBF8OH%E!W`{!c>nOMWC605#*zsX%cgn8=m&~7HT{PyYOxkfDXwDlhCHree7YH=HtmllSAlL^Y>Id`VPvH7(V2ip`FW4%4wD*jUE;o+ZZzrDRxW$o`6bY; zz_0oJ6Q1HQ1!3>j;E+}N^uJ4ZS5kt)2!LT+D6K@V%R*$f7?(^0edfe8L13Ndc0Ds2~m91K8s&E89 zQRSy_Q~g^xRU8%z%}L?w^*6IV2FHrwc*ctVe0RZ+2F4)t%rQ-pPsEvLee7^dN)XFn zGgDt2@reIKl}%apssqT?FDX zc-ewZoWa)Gnv6q!fpzg5v0qY7Z&XFFtFj*1bB~l;scFMa5;Mi669$YP9|0q_S3dSB==oTrS z{^){Ny&F44@~Tk7xVk1Xdoy3CTf(knXrlPEC$D)`ND%9~zCGCbZf>K8keUrTQSuIin(RQB*5tNEUQ+> zXpE1xQkVf^%Kv7ZjF<7CKh7P8*xgf1#O}}NAi&lVHVXdd_ za~w*RZPXjoCWbzBZ1*od@y)VPAWPc5J&cW{>^yCp4%T0Cy*9*Q=9_gAmN6dmeBW%IKRiGJ~(n!iQA9WrkvYijlT*KLw~$lC1rwB~;eU=)wA zj>D^}3v1m5!)>9ri)T-U_6=Q)-kGXUZ+L-b-86gU4E|5^BNrmD!X;n`uVyvYa z3D20P;!brDh>kisVBE!En3TY^<58jrgihIAy$>qyVKoMJEdyYzL8)%2p7 zUq?>7F;tjUhb*1fHz`kMr%FR+yt6{2Y`-F)TB-GpFi>EiO(~u-s*gPuMAHG8wjuGZO+Gr@?@>qkev_lX-migzuS{MQI$X;wOkK}tuSzIIrOZLw^c21QKct)J@% zim{put|~Vo7%?H9s3zwasxHosmZXDl#P&E7C@2R z-qY`K3{Ye34ySW0M{nZMRtax?11X^%Hs&Q2$1=6_aW(C~jZ9>9nRCUcsv8lhO4|%D zVk2D(4Tp9felm09O!nY_srOcS3|Hs(9WjVer%D^S1OSa4lC7F9_9D_2kWhJ@GfpKc zJE##*l$=bcaDjAQz+ya`@yfT|KE_=7$XJHP0#M>0LEEm{Q(AZJm!)6}qK+Fn?VRd3 z&r~d;{A2&>2zSREefcp$pqIpWl4w3AeA7{ECw$Zs;K|^5qoKfpoVTN zrNc%PFQxZj1M_mD1Pnr3jA0`7N#eCJkEqXhwW-pYs0f-X-bg&<8lgx7r*Fq~X^wX) za}25FjFw^T^To%3z^F;PlJAQ7&Yw538AhSsbDe4{hAhvxQa+oG2LJg)L|9Q=l|yT@8SGXWRh(+ z9MqeE!XcW&ipgB36#26`N>ID4T3aJLUYWc{Hr{|!cH@tBH2^Ib?VRJW5?zfo6Ye^K ziNoz_;p8&K8ZPYo`p1ZAh6gyw#8ch%PqYS&pr~-MaI=8Wiz)bzYXd%pl&16OV}}^U zS10YbQ}M;`3#TY65IPz$6zgqJVrNSYSys7Ol8MNC7O~L8a{$qxL7rnV{ypUq zl}UIrfWE=NdV;yaZt|T&63f02Hs@QgW(zb8+#|q~-gdTc!0|&<_sNo*B`Gbb+`Lz; zbH{gf=xvV9)3My0M%2YpLKEji`J~QkhkbuIi}2A|H1Mg(yWbIVr~P$FC}(Hl$Rs=Q zbL-A?L^-stuJc*r0F|_A+A1(El4|xd_c)rs@fW^UijkT3i4OayO%CSQqk;`&>@DgG zErjBh3HROkksNeOEYFlB0{7{oJJ(IsoVd{l;T0>tI z<+h!DlBR>jl%~w9?4SaYGMm*6X*-nn(i=KFGgaxQLuCaYfjUFPEor) z=>fuEF+az+bX({k;6TW+#YOeLE{^|>Bae3nEgw&4ain&-mlEye59VaK@vUHZ3f&Y7 z!?MnBF2?tEvjb)vRu^h`Tg#3(h1 zjL{T>6)D8~Nx>MRIqkTEES%pa4D4ga-+hqFGHEd<{e-VXfo%c!#po?VK@b^krY?dq zNp@F9um=yVRLNKIb;6x1a=93|joe+P|2!-wNJFKq=5Id;;wWJ`vG!|%0@*Lx))&;H z1wh)Vnv(@iP&**zUeQBm00NG5T%jfsLXOEN&J#MWH?0skO!XSWC(yT<1+IGUvm^p| zRkz!7_Fw2=ih4+1+CtCMxBI+FHRGQzb@Q$g76Y3%*Hdz8v+A5NjTk96Ad3otZ7o1p z-b`&SoL~{*2IX(7u@aDt=DKQ4I{=Ohx9uy%x)U*X50PN#I%gs%<{oFDVzeFz<$#H* zwi^?~`!;l7vT3q(B9GC*bwuSBz7bA9l=(M>y`<~rdEAxQ*=4)hhORmMdgAyLK&jAT7}|P=083+ZJP<1RkJgb0;3EZ1!~e+tc#i5YSiLhJrE~% z_^tao<(Wg5zs~-EyS6~z?y-Gex?FtdhNR1YM#T2OsXEp_cgjNFJGOf7Rg$T|)WNKR zqeV1@W?$<5lFgla=3xoyyjH}#taj;$R%}HB!ZKQ^SM(v$>?8|>JNdWm@Ix^PzZFCW z(78NKZYK#bv6;y6hq4W=L`l481v=sYF;>x52p3++*Jia#x98%n4E}hC`QXb9uWX(C z)ZMIZ$7<;G>RTNQ63?H1w9KT_g_3hZ=fUlgz>^uvFambcc^xSjM142QXJzl=s?;>|O?FCnOlEK=<7wlBWga0FOZ zm&l4Pd<)^4Qw$vM!dNWhu1uWC$~CaI6UPb>yNJmdVcV>mB)C;k8Z(c$d29j z5Qq|UgGtdQC0%1#gar2=of(o~cMX%e1bo2;n@F1c+wnSb!Qww0UPGWo0>ov-13zL4 zH=z6bd}G>vS}S!y8Q8MiH%**lW80beO_psUF!4};+YO~LRSKd=;Qs334HI+6(T_%> z#Ixj=z}{>Cr7)n?(=v`{nhYMl0*UIdMT{H3jRU7e!eGKLFV!Nz116O}tSA%B4Q>jr zGOCijKrsk-3kedVs7%pZkE(X@ifB`&y*#=+UU-=~C0_F(%Ny;|pLY>c!`|#k!|+JY zJzcj|e)sE0$<*Isv56CXRrxP6;)DK{=aF0!+}ent)`L_B(+?`j$zVsAV9hWzJ<4~W z);V3+|5Lr3t}*$as|e_q-_2mMnK-cdZJFT9DDW~QOVErD5(@e%gYs0fd&FP|IMr!% ze-lK2Lm-wN>8@}7niQ2FDKV1aai>+RoM>B!+8Oom;@5ZSgp6>O|;!_2!SF_dm0=jS~o} zrpli9Jj&CuoY(c&N{+V@j|uJ;vl?+LZ7A)+RThe9DP~o<0UJWPFH%$?D=59iD;MbR z9i;Nh86w4hnmrMn3!|!Z;c?ruIjxhmb;N)65!6x@EVPtx){6;ubk)LJD7##cT#e2^ zsJe#}!iNP)GX(P=o9s{_fmF8V3rkYU9R_pXOP&GdFbn6Mc2yIh+E$|w{;)any{@Q{*<IQWpL0`0 zoU*u2(+`FMOeMUtBY_k5<#3UN+Rs zu+fsLNcddnSn;a73)qfuPM14|%u@@vc*>H+R0pO4VJQNCiKvT=kq(EZU;0CXv)*+e zbt~B&3-JO$(cEwQDklKv61DgVG)uxQZ!M)reQauw!KVdPnV(&0zKq&M!TwvDJ%BqZ zrzFd9-?UCam08}clTyPG|GGP^N>lHkF6+8cDmWqG?TVw-Mf57 zH@R${R598OPJ1zmuU%E#|FESjr-WXd!?+9^F0hIo~!`%Wmd zQGC}Oc-Ie}G>03HKw=_TqTedVAcyqML+AN1q5R5rtNtzHdqvxhO6{y!& z4D%sUrz+{%ni4he&b33ZR+U=aP<7y!f$-vKR48gCaItE8g4ao7?62+J z2(j2zk59*YRyK?Dx7bZW!BeP0!$%B2Yu*MNvR;+3;E8EF5Q>a0PdqR)A(wn~gX)xSi7 z=R4bmoA5(aclKTfN00nH4MovoXk` zqD+5T@?aH{cEGz3toya8vO?(Mw34=g?cr=*o3@ti(MqPQm|&HWQ~4gZ#&5IWOKUQ( zu(B~I>+Gj+(Y%IT0L1rw7Q$XG{CMI}gmN(BH@v5nUl=U+%;1#iI$1{V>~4;$kf$-{%RWB&2xy zY!idag(q0B9iAUT>Tk4%ES}2E$Ik;!4T{^!-D)3P!SlT2`+wEcTn7o0MIijN_4{_0~My2u*i?m zV1N&3VcfPKq_gos_q9W;4KZ1erQ4R3*o=m|jA%~j#tj-o1bA2Qn9BY%onzs&hD-^nqSvaEO*8E_ldS#%y- znqM=|Zz+uD{|kS|<^D2@%I7`Nq;^}m?_82}KX5cGbf%=vRN_*SJDZ^ZAXF^u&8J*C zHK9>ca*+%2ltJlIK4h&bNPh7p4wZPh#-5@s3aeD^wa)7i%kM)Bt_XHUM1BSBMCMV6 z`Oo}aj_J<|427RiPnBEZiSUsEiPsgW=#OLeIlUeAxBB2ql#o|zUnJAQol`X?iWE7w z%1n2^b6-ga+Fm#+7K#i|Mqd^-tEo`{nPTQuO|O(WQvaw3u271-R=FkK|0if{6FLP1 z+Ij1dI~f9BVRtP!0_vtj7F}x=DCMnw-@mz&xkQzU<~Jg!J-;%|k=Fcu?=>vuU@EP;dx#-nU9G4gVyOmuM>n z(!51?MiC*hinKrner^V%6@Vrgxk^PQH33{OZ%q-ks_f^k69-kNx=pX;dhM4w4^5A( zWS=qv!uw%k7YEL z9mPQNS2o;xZ<%gNZuq(>f>qR=diFczB&o7`KoM5P78!Z?Eb}d0{3B@}1%_CEZTuP^ z;yT=W&ru`Xd=}RjgCyOm37t6t)cJ;0F3gY*5NDzai6osV>sp$K@>VXa3uEp z2Sz$u#!FFtAwGmn(OT{wOaWS06$1dkng+>N%bU%1rI zDQXOqyw}g1(m@36b5C*uY@U^__?dm)ksI~{d*jHmea-0Rf4=`T$Btc&PNk8+fxdV` zxceYCO5cAHymR^Nd-D0@{t(zlZz})>AD-AG0qCOX+AojkHLqG<`Z$qUNHVeD5j{Wn zko<Gzr&_hwdj%O9%ed-(4|2{*|L!RjuL3xdB9hh|*q^;^)g=rZP-~3d#c2FtN12 zDE=cMA4b;mo`&tn-Zjz-Q10Dd!%QBHhBp&`bv|b1XEyQ81xKCmP54S+85y0VszBnS zdPj_s6F0hMm(;flSqQ|v-lGWI4QO*iM?l_)1pC60y6Ok+hO7FG5(R56qoXg4WXnGH zyFZQse8E#QUqHDTye?AK#)a_2GCdIaB`bl-O3 zwq^eejwI`yU)JKL)?nv}*VwhkyT5$}*^p&C0#U}TX3~!|OS^+>Pfihz=a6Fy^}Ks2 z??q_EtGed<$NtZYq5febhSKX__+Xo&W~daW?w0_PA*&mX!RUd8Uy9b zRmA|;ahs!^!DkuEj)mez|9(n1Un-K{R#SQ2^K$!sUyI@*m=Na8c@ua&s1CM!x{z6I zIs-Y&g`L7e)}C+4M`m`8)&>S4K!8zo6n@_AOhm#-^meIC^JNqlI+|nsNpN!)zTV5_ zW3dE)!U6rOmfhg(m#&u7Qk&mf4l(PKd?OisuDGvTs_>2l=`Lzkv2^Z|<+5y9?ICsZ zB12;Dx}CFDK54a_NbuQh^J8s_RmU8m?THZY2S4v#&UHN5X5)IDN#6hxo}VaEs2k;2Dq8-DPd|iy)@Vs$8 z$`fzoearNGkOk`g*_&?uIA;ylLGs>6qC{l~vO~Ca{Tz7h`*>5#Aj_^P=!M8pKkK;o zqJ$un7V{ULW_28R?e?bX^Y$gZ*>%_D8}|=n47+lv z_pK(Im`CG=%VCc(GRQNT)^+9Zryy_^u_kHLBYmz9#o5zmuI7m^Ytl6s%<_VrcoVMD zep`ls$K5gMKZ3(y3Z3Zh!Sj^c2Mn?SlOt1Yf|_m^ye)ZZRM}5YPa@aB%fZ)y7i{WC z-u~8v+AMnOrIjeWb#5K=j3~;NY4z%#lNR6k#6S||?6l?R?nCEfS}B4M@4uPUL05bD zNO98!e)9fJwzx5zJFgR3$h6rXzgPu^Ks5F_8y>a6RaZ)OW0G$V>yfyX3ET$R-uDFj zaHw&JlyXUTXTKSy>@fV{{P!qf_*mJ{eKi4SFzULM`0WNQ1j7BB9PvKT1LU>SY5{Ce zcROvEzZAc99j29f$!un4AolsUdQIp8;i3zu*usJOh#24Q$C|pK><+O<`4mIcdt}ez z`yYT#Gi-hUGzKgFiFe{gE!#>n&6OgRUncz@@B4C|{c z8p>z2@q1}nTwY~>ntH$Ge+jtD<9?cNUA+U?7Odbg5u&G z1YJtLm9OoNGkE=342EWz^)h*J2yH>ZkAhX2tCJ=<=JcQC9Zsq_GR#jHg6-Ufd>m47 zTHd@pksQ!ZGZ0(c0VBPk&yp^o3{0W$OPP>L36sb~f-!HH*Tz>}-EpPG)yJHMisWlr z(%oHFXQVlIefVS2a?_pAh;^Odd%NhY&h~tk=Gz;u{BMi0k7~!aQJ6?PQn>J9(%>RS zBSKHtdYYw!?0-X7@Q4 zo!wfecIJ-G*j9Mj=yMn7Ha@g_OJp14hhEJr<0rGg&!XsWqCQv*Rf{35jL{D^7r+W7 zkNn|#^N!U0C-wHyK~XNQ)k4o)6_LcR{gWSa$v zP|U3m06X?GOq^z)7@)aj$M^dm2!kZ;$dR+}(?Orz^FBQZj=-Dnb{511?ZUAD>igN{ zl~tM`19W)HZwpY~>ini7&(n&DE8@P`AblMBUspv>9+wyv#w71$ih3mZpwpO48nSaU zB^^SECiDr!zM_BKf1y~H#bNT(CE#899nuLP7!(K;elL>))}C~6EOs+qqL*$x!4qyH zj)j++K!?Z{%>_WiiqLrVQS^3c>@TGyt&OZl>^Tc=Ty@i$e^fIy#*WN6QlatEm4BGL z55a)+J-Eg6Moc)p z@V~an@OibaRX9UrkdX_gS*(6tgXW)fi<>$IPnhYR7rc$&j>4Q#Z5vRdH2{hVyUjV# zpucVJTpcR@-foWlrJ!=vS;_gc6Zjmw7&2#`MH%dUH2zEd-^E}|DLs#duZBg4pKFPj zlo;f2yiG~sF`XWvk*7|0U$w6;o;+&L?8SiGP>zUUGd1X;6-+R2O+6@h6B#uY*;ZMU z>s*&S9Fg^VogyI7p%yc!kkv^|fFIbsMmgq(sa`-S8j0#E+mP%1-o9RlUE1{c%jA(y zLt0gp+`(M{+?J1anAz*BA?0Wya#9p^Uo(wY=ejvQJ{Qjsa2L_R(7$#w!jtOU(8BWb zb~i&5+`w|;b9%MoW-^6~MCcO_L`d;a{9y7GfbkDPw$B;ThFUqzRnNF4b~y3s_)p!G z{;S0lO(#{QiJ^yDvl784?%uuprC$4jya1`=1^qJrg)a}aY9q^ew=lr7@RgvGIrUBM zTa`rOFY_B#r=Xi%`#zE!4OFO}Hl1^kCf)bY?35vis2Zmr7st&81`sV(t5*62W8koB|Mf1=TSjP z=-RA}YZp}>mfU+DYndW**5nlS+HjtL7ail;`@$~h7;h|b1q1EeBDqH^^SCj@FC1M> z{jRoD?jwmdOD0nyNn^VFH`k!iv!s<_TEMHhLT$5sWkqi=XwN86J#edR|MJ)6^X2F` zE?7cL`lDgj&)MR)~oHLqL>kF9_REbHloehtJ! zU65SUzVUMsEfPJv=?~+ZwfpMZo1*rciSdUy9~(0d*$wykem1RS%h}(f{3v6baL_0b z2Ix-$qot{MF%1RdZz8uag66qmljV4Q*qFZVdCAqhEm)JsGi$n%8L2qp%^5~_iB2=0 zfnyx4n}z&yaJp7tMy`XtBN$i?-0(^9j?X+0VK70pxFqIsNkv`sc7Q3T?Ps7%qA<<+ zL3*AmGAdC8`$=gjZr?{E8i&*&fwonURJ?Cbamp66ge^(YVxW{appog-gp{vTDl6+{ z0cV@6sexKk$d?SEiR$aPMO7C#7pUV37~Ry9aXcMoinAY@^;bn1n^HX1G3oPt-SR@g ztdQ;3aQ3Pvvh;vqQz6?GBe&wK3<1JS+1J4G2K22Gut-ILyKnQ91>kGDOwdAB#A$A; zXq(Py^;WRm(>k zn@v(|-~_fn*e# z7nELiX(W_Hb&JLEh?C@^%(7)-bo<@ik9=r}=B}#pis23RKk-|+{L3FcA?Mcc;L9^W z!EhV+agw}~SM|fNddE@4B>C}4SPYc#d2yKC?@*&$FLK`PHHod0<;PXlK-;EwK7+@z zm&Sc5p~J9pr;i2P7mgbz0oP&!U#fp+J$(8Y-PlXCNuZ@DstvFdZTZO@s*Y5}^gJ_B z?R`=y8JDYhadQ(^uhoCZr~k~C+YTTM0qrOQu}W&B9s7r6f#bso$OUgcmCY9LbqEJZ zN1_Txnx=l8AVf_hF6o*g>Og2=+Z4&;$bnQBve6Lmz*?c*_PtQ7PPuwDD?l=OhPGzB_zddC|lPCBaa9vDMc{#K1CookfxeB@qCG z6Yz32d8{M*f1B{nb7>#i%%4hhC*3C(*!*{el9`0LCyYF+P;gH8yt$04n}=gPFQjh; zcsKrgUM}=xa%M&J`*1a=h!WBUmbxMD{vk`{5tO9csvc4E zwXk~4^Po1&{FUqtLc!-+-pIFsEaN7qAWf@=zYb^(KTAZ6@54POvEXJ72DW4BA6fDF zu<$pFRw#`EeE}4;G@~kH#&bVrY-Y8*&Y-FWWAkH`tqJWA6vK&6po}8TT+PN&%6c&Bnj!Rlt^-fS_;{AKas&v7Y_-;32xe+YMp%H1Um!o|4{6P z50C%qRWF}M_n&Q5_2RWpSh%g5zW&BkaEWSheW_Ipwq$MPnzAzRX{_WUN2k#>f1`pDfn>@Y}Ae?{QmmTDC*4VD5zL1?aE4sPyWfXFk z%f*VH=B)~y{~kem8YtME4|uo)Bf0CR02$28I;$2jq|n;OU+0NoE9z_k_j|ZAe_*Q) zRt}eaU@f)t@8{7Y+BJ=t+868-`pmLJVl@?E1XZR)M&>hhX@@|8lD|c5;`s!gm%l=N z`F-1@3QS}pdc@_7vkTib<9qN9RVYPw+dQ3UFvacWnF%;Azet!{KuQs>7&@iCFr1jP zgI7>*VY#z*(FJQ-=8?WMtrlbRosc&l1_=a+VZ*NB7qBz6hY>!L6tVq0RK@*#JoN@r zUS`2063VS9N&h65DPd#6N;HBo31$t)Z`At*SKZf(9)m7+wr zPvtrvUk~Tc4%UN+y9kHTQ5nGOG|D^=33x`l3e)9W)CSnmzTGg~OPgdpN{?KViXP;Y zuK~n1Zom^~{<)3eW_6A^?_NrIzE8ChkM|+;d+5;wiHnu2;;XEmd>4tog&n1Z$8#y( zPM22wbwdX)OGyY&qv`_9({D-5CWQJOjO5v7n_dUT36hX<6pXw<$sahIR8wz8hb!hx z1)PS?zmElfy$Yj4Up{%Z+)+l3YJKFWPBf%H;A>+b#w;RHZz$RxjllJfXYt^9Px%8C z#i-`dIU>xtzbe)Bg@yx1*3Jhe;XkIp+} z8&(aP*ngZ#;4yae{CUrjVLyC^U>z$QxjwJWuKzln{`14iaJe`CjRd{EOe+B-#_nlo z$|1x2dT;B#BFbQ6Ldp4%|a0Gj- zqW}8@Y=k!6(OaPfET+ITv}M0L*#ci2S!GQ)UgwvieOKnzhHC_SgaNcQl@bZeST=shZnJEb~9pZoRH^qS1ek#_OoqsTPw|FmgW2=;3dchEgNY zL&dsE&@DhYEVSr?W3U@4K=eJC@Pp zF|Aa*Km++3!}I20zRz~@Ev?2#Z{X2CgbC?m_ijhlqvcFE0^PqqV!CHY1bB2}Fe@u# z@5O@lrSnH~FJfeC4y4UqR%-i3TnuvTqj5IHVSpxktZF|6m^sN-Yjq+@t40X(_QFC1 zAX=;Q>T-~we&yneA!0beavAPraQ6M*aI$Dh)ciD6BpR2k2!WZS&rH0h_S+KPZb(Mj z!p0!@Q-`u1)iyhk&-4IEU}Xk7mz=(H{YRCJBtDk&>EA%O3GD*r#14K8kCY2E7)`Yv zM>2mk=l%#Jl=FS()!A_jPFq!?VJ6^D0B^0)>?!AllnzGd{Vzzz*IIqXg2- zXQ+$etmQMKVWcHAM;Wkbm@w=LA!V$oV^d z&%;hOtXjV;w75cn?l@nZ?`*P|K7c7w+X*dLFzd8a7^M%g-$J_r^djY!t9X)%xg_x4 zulXI43Og6{#v&Z&k!pOBL=UenF5l_8m8P}xhsl`*(cIxSKch!X7mXwrx52Z#T`XkfDkf`{~dUyG~<6t*XrT2Y;rYzRg3< z1;1uLoTMHNkNoeIqaE6d{08wlpC^Ct0Q`3=erPU9@Ak$OcVm07#*HII=cv4%rl)m+Rlr}^B&T=er`m`F% z>CcCuyTAWyAD_?1{qFCDw4bki!2B?t`sD|De{VE8Q@=*WM;ytF?CMaztrNdlv0H(G z?CT+KK^u`9mn(SCpeRJVjX#)_ZaIse(1}4x5)ur=mo$F1e=NM@^IHF*fkr4=Y9erG z8L(S8I&7RVFV!M8i6MAz_pdt<5=j^n1P86%l^|CsI&-S7-7}H`tj`3|$W7i}B^RGC zjC@FfwjHOy$G`7Pk^CX|F+{+WT2uaM@L@ax$ZpS*!D;_FVkKhXJxBklQEh?corBYP zH`__+N$c6^?I|aF*|3ZOALeZXt~u{V!5TX^c3v7D6v&(j+COh`I`O9d6J69J z`RJJU((a)Q$XD*&(1$I{gzLzM>#ooJHxqql?-?OzNHCka#w`NXK_}H#!XRM36xZ4( zF(Uav%z?rnpEkT{A?b1%ys3Pn_cnERP)!H#1+`D13rgTPfpLeoE_V(C3rWH3rIHu@ z98M99dz^Db>!kT`{3z3Q)XLd{I(5I?Nixz1UHxZ-s-}o>WOa=@@beV}o6%z93hn5| zdCXoISEP#L{9k>2cU%MLM1Cy9-gX$;-0R%1N@<_89RUnhO|X#dip#!Vq!`F!wN~z1OEh1yUP@ z%;MS{l-+8_5mPRC(%8@qkGM$R#~12=%gMIOyP%yqJnIzb_gi76!G;VGjhVO*qdIq)pNjjg?UOm@amyyiRSs@dsefY9 ztngX){%|OA1AFJCg66nI(Dystub%O(#=%y#LQ|y)VsQ`np5J6UcP;$5N(BJy3b$Bl znEr6woODzTaC4<`7g%hbbN*?<#BLOvD|CW^_~n^g>uol47mch7 zM*Ew!->fs)r?>;%+v%}~80CvRG{UPtJX2KCok}n2tY8ZoPwo|D*BfnFkPys)2&lTS zR}^_#7*}%2M5M9f^813uFzHhrXB;im)}`YvU$imxkMr(2ewLd5h~5$gbyfH0U?RU< z)oC*NUWaZmU!M33L(~Yxv4XmcPO@QOnI--4N$NhG0L6#y5?nwcnBq6En#$aTp0)ot@Pz-CCBG7me(cmrUqoh7lzko!Q4hEwBx^@D<8Yv)bJ~8S2VTiS0IOks z!-d(h3Cb-P-f9%@A)IQ_jAr?;8*-A7M@=aceGb;jMW$H#oGU{3B`j}omSKY5Y;BV; zMeF3pbgSZ*SbVUK z&5`z+S7TLpS@mYLk#`yf@~?OQEM%NRZ@f_`%EGjuPw+OcgNNduZ}}&-L)`rA8TD`7 ztMC_b(vp8kYYqpH@{Be5>XmBm`n;>N+h&z+I-3apaQ6wsRBd>!+e%(r*;)N-@~Qrq zS6&#JO}^?Zqt01n+10j2z67}iTo(Nfqlo8RV9)5l1wa9 z0(-vgnVN^TYejsDB>M86z;W)a?E~?fP5=fhmvdY%p?*K?N!(XO4Y{iv>^J9(oxAl7 zr))`(<;PXIw8ps5tb#Okl!LRQ4<1uyM9%AS zlKMj_bBE%HM%tb%UdoY5LWq32dB}1x;Lxo$F}5dchxu(xUFwa(*!%BtQXRUKMc)(C zr7O4G56GQcpC*hO?Iw1ZxDVb!R@6q*?Cs@tbi33k$8Be^QR-X-2@Dj)63%8Q3`Sd| z539KomhS4yRL!PO95{t+o(+r;&vpr3wS>?g7cT2^K>B__`d#txXqvDKC5B0=8LZ3x zLFIyQ8CrHIoms5AJ+1eIa~{U|6z%qNSg} z-$mVMYcDJXppjci+A^BdJtB{J9pl_0M(f2q{3Fg+z@F5=K4P17d?kHWMm6-7Fq%Jn zcJeat47U%L)Z00aI)VIi3#3FZ-b+N4S0_RXp1qP?n$MPC`a)IK*(7}p%@dRah zv8zTCZHuw^Ad?yM=wK@NFn1ynquzt}D}=2&zJ;nzl-m^VJ?`28fm z{&8t_mdY1HN2}vT3t-bBFUKNz1Jl4U%OS<9rsZ2jqL#Pj&2^h5?LK$);@K2cS}ej{ z4Y>>(yeDtYSc&R3U$<=fT&1H8+aa0Ar)A$UPgyP&hM{Z!TArLFw#o35j&aQ0#bBQ^eK;Iv&AszHlyW9!ngZlVKjkSV5BY==%N z$8eo?`HpC4Ey&&0+uNo9ftXGiDd*!4+psIGV8-*SED!;Cn3uvt^=pKMfEqU6c^rQp z=NA8KjwwwwnOHT}&L_o)rY?y*6F=d18lGFc2Bv6l1Ym7Ji`mWxDlXbvvz?$_mnT1^ zA+PRCJi{1(?G_)(Ei$1o`Q&WL*um$%fYhaP2oPjB5M=jRjf6T1FojU~mc-VJ`Z*~+ z#W&(pVKE;D`nm=Bx*ygsfL&p61n1fR$aj+caV2mqd}xBw?gh9${k?kCuovav4~QND zkN)EwIGNSS0Q7`3j~c9?cYoJP^grz7;|yksj&viKVMJny_SORqZuRoL)$nnPx5@{- zyJPKh4_}-w4c9IHXZ(DEH3i|jv;iiwbkO%azl*`ZxcCO1 z!^rpsjVj40zD(Z=i=<1`3?s0uuln&k@T^#}iykiFa(jMDlvEQq90~axnfarCQHC(D z@s?}_MU6yOfwW+@#?1P>3LJD>{|2^8YL!tpqn}&+>S22Bu4m6`23VcF1!i(FWQLIg zZGvp$iEkK?S~o%N$4gCBwIQq4**d3d{R6W>R}XPA=4104S37Jz(aVWjqftwTuF8zA z#?FJkyH|@XkPqi6dQsGyes~(2JD*oGfJP|iB{13l^z2=Ap7&iCA~ZHwWFnB+Y1tB- zRqj3O)(p_lH8*Ai-K^Fk7^!}8=QqkKWF+4fGpxu<;K1tV4CBuNFzV@%YZN{sLsHxE zq72_k&A;UBY}xq?0>1`Ymwo(dNUy>UEo2an!@Wb&i%RVrgE_2Dw>0#;0b}R-l^L80 zSOBWhXzi#c+IK0ta6T@9xBFYYQ9aeP{T(u|FUqiHO1jz$X@@3E zm6+o5gsS3{bc%}h0{g|Yj{vKZ-c?Tcnu&cM#=FNW@fHFTYcq@o+~%SF>wbXAVR2EF z4bmCL0iZ7t3mK<-v!SQa5@&y0D8#)fAv20O9S*VMbyO0fDs^9LC4xTJN<3F)$}S2R z_>CNxtbRQt2n;uM;POfYP{^Cj^(*nIDkV>Fpxpt&iEsg$ zABR(6;Z8;>%@}-m=b4UKA&XJq1dSc zr8o!IxPn~DW;-T=j(7nu&z~X*#_R)V`U6B6 zmY{Zq`>}6X@G3dC1#L`X)Q0F?E?;5qe!s3Gly|&jNH&)CLLpo4YR1Cy3?tE2ZU-&X zqab2gZ@2hD@9Fq;k|)>vhVG{Pgr7wAg86QJJ#QL#c7VPSb^=RC7|rBr{H5Lw&3)oN zElOF2X;g>K-QxS1GON`oajVyH0h3RtEVa{OzHu`@y7^6f2zMRaAUpKP%xALVVPM_n zV&BnK#20>xL)vF&oQvWY2>B`9KYTNOltJeC{=G&EQzw-)I?8XjZGN%ZOO?%#z?k-uh{ttv-+^<4XjnoP68@>%jTeth@ zwm1lrQq&~Z)C$_}iNWGhoJ$o_k_iNUQ#f(-k(Z?q^qCQtV9xyLi$aW4h~R|{NWM)a zwKYv1OeQrP>tkDFO(h}waYy0vhD`6!vfR}S%PIJ9Rx%LRfcC;kIdFtTEAc~nJs+l~CVNmEdK6I&A```+;*US=gVrLe zpq>!rEHF(~o{j?J)B`-h9|vHPzWW0=1%CDo|9Yx65Z~`^iLz0qvzHnPDvQVAY8ZbKDKcs-q}1q1Dh-cc46LR>4sb zv9V3*$Nhz~nNLlyLnZ zWd0{ByNt>+!0;VQ$n1sL7n$fG=7;c7_p-{TY>1|Y%sr)JM#Q@oehyU0qk`869HF%f zp={GGl!&8LJ!Abukc3p)kn)G-^(PMA#PjxRu|MryHQ8@{ zrJ7!UPegk`V*&V4mUdvDX;sR`ndfwCERnz8iAF`dim43`xhZ*A{hMx7mF!vk0oxjx z_a$peNUgnC>Fryph1*FKY8pl7G(&|SMuWKa8&MwiOh8LlI zNdy|BBZ-CIFhs zU@K}OQ@$WYtI#Hhb4kvIpZAbFq|8{VUb|@Fftn_=!S94EfJNh~sNChUPZA<&#VF=* zhtFL(RR{;d!Bl}BVr&wINUe*^VUmOVoRbP&FmbZ2>_-(fB;h1}Lh zvD@nGDr3P73e_%=3vVY;CAp2yL>O%piRvx3pr1(^g?h?PMBCla7#JjpId=x}b-7$4 z!drHvbI8!|9ZA1d6T1~@!KFv>Cn7h}BsW4rRq`D=gSP|OZ|s^Ax?!ecU@W$t@o#*9 za}CpZ<(9CH*XmUn$fqcqnlLQVnd8$@MVP)CEpW9P+zB{+x>P|F^)k2~?AVy1xX>fU@NNVQ?Y1tiCbeBv*8syE%`*WUIk}*|zn#i=V3gG0g zGf26}w0^b|rfj9q5Upkf+}{bSE#wHyC$t|CZxhr#^!!?=hc29Kr#roa<@Emo;`+TmPe1@V+ilX|VgABE*Pa|zF zrnPD-(UOF3r5}z}Q=|4@+)+Z-+nn$WL((puyE5?5L!~JaJ9@;5t@;t7RE7D3sCIhp zw-AD0YD?1_`%};3vndlg#e8R^aF>vUq&p%4vb1Au;b1MWmG3W;t+{(_Ekn@*#7uS%&UQCZJ@-I9?cach&lT!4-Y(o zCpLJ30%uQZ^r|<*v<9QT5qlp|yMKMFjrv#uOB#cZXv7|de_mtOOq=eAf}PTMk>?br z*s^~1%aW7SDniT~Xxu3D?CyFxb(`}kSvo9dJ)I0PyU+l%m6Xv2vg&%{eaKHM8eMFE z{N|rA)}iXE2HG4U(0=k+tz6Mr{}(Rm=^OR+EmsHH;_YkbQ5U6BT0_Z@&KWm`-y~_F z2>Fk#hLjo$)>!V0r=QkxFE3wHYcFsZR5>5?gxtA>HI@ydqa=%+1)NaH1DBv^YbPO4lCT|$4F4DiZyZy(btQ`4 zW;?~y=Pequ`&t=fG@U`hr<4||rOv@7cB=tZaOV4^S7zDco?jYU7Jbm%CVAy#xu;Gu z-riBji!a`pneQFHZ5d^Kl6B>)r4?cQv!(rNP_EuM>84CA~!%p0N>I=%bc3EDB(DGaf( z7V2=l=)5U`xmSjaf({1%da1X6LLHVis2jL~CN!GzCcI7g+{S{QvMb_vfMPdwe?%noSgqzoQh!DGbQkK3xC+%&>h+Glj4cuuk^bqMQ+!oqer%a03QtaJ^8--7*~SP%?FIfujEmi| zE4x-L#UR;JUeb|8yMbaWef_iAt?&-9#MLUQHo9eIYTsy;?_F*}*REFh`J)Y}A!~Ie zivZl3A^0uM8x+{ckoYg`wb`VVnA9a_k$=fqE~QnDKC^r1alm}lk?`=MlQ8L{t*@^@ zg}Nt>pOu=&$1g=0#=mU9cV5vaF5|ggmmWLKB;oTrI8sl)nZ21D18M=5j=1DnHjsGk z_`1F}xSCR$*m*#HmyKiRXdLz+nN0Z_Up$|ga<&jXoxAK5`6IuwF;24VcdM`oQZn0} zfpEEvXjUXoLdEz4>)uIbm$|Ih3$<(?xs(z&TRgc_rp#CIqgq6L%qJlaF%{v1qUn(C zn{0E3ZC5;(32p(2`GYsoqZARn7tE5y4M#7(|NO1@ZpgpBtC?1upOc$FN21r*cZ&W; zEbNm>y^@P&yD{}i(yMHcUsG;FLYpA#c zKLUV)+n1i*RxV=^3j%}B-|3|2@H2%|$7%@0dV+N=#Rk?! zn(gY#xrtVK*OIQv!V(QESK0C>HCXY^qAwct~R$ z`Gf`131Wt**_A~^I+0j0m<+6ALvT(a8-Q^@=XmB%q(s*H+csiPH46W^?=;*OLlxYh z-C3xdPJsXY(SjAH->Ief(&5DiT(LSnNvfHF)5R0K9#?6P4N%M?XW@nRd*2`|vD)Oy^!FV(>HSx6FN>cEb*eQPe1fl>#SBc`$Jtva=(Jq$910Uk)=LdO zmds5I8+T?EG~ePUR$Mpp9hdG>QMg8*S2V}_myNS;s2NmGH*74)C^vVnZfr?b7MmUm zXhwu48*}_qh+B_W==3xQt63=|}3$ zKX;uADUE#%HK^J^F-`K(qXJdLP~(X^#?c>o*235Z*Lj=1I}$d-m}&$Aj*Is@Z>hkJ z$19z@MiE@rDYPKwB3aNbsgfHYE&IFnq%@8sGj+rAD~fERwQ}IK6WdFVub9^ z#Qtw@lK1IIvD+BPHLI7g+e5 z^umyr@BT_W(X%ki=W&k=CnK0y-w6tCGzp2J;i^|{oddrJU+TS+>r}YPmCmgLWn4)2 z)en{(@#PoLB3V8O#y^VVih`Ma%7OuQUTXJc;?@}7w7f7yI+`N!#x-=F$^8n0N_=Q}yDo~gfK5KnA z;F}*GZf1Z3e8L1|8eWX4X)fBYTp;o|Uc~z&RCwB5?pCZ=81nN|iX308POV`)%rU2_ zEdPT1JKDHaED3L=WEkX{MJyBQOclrv$~J$e=wF8)XR$?#dr){|^5zr|ktur7;ryet^OSqDwsDg9ftAH$kc?PiT2O+(4NwMVN9bbRrQEEAKp6@Yv7V-S} zs|Q;J+6-9w(qii^9M419*I-$isC&3^>mI>;e+!)6=--cUEpFfN#0Ulv+i#fY@n~Q5 zgqH%39VpV5+iDI$U5QW4v>%CDT)wr{XrWxR{$MQjxFtmFE!ak7o9{B5laCTT&0$Ys zJr%G^pUvo4VsRuO_ZYHA828WYJ9F{-B=Ej2ftH2%BBuMv!ym5(Irq5=pZO&c3l3in zS`FEJB>NHubxn1jCXI^_MP*Oy5uxPuDOuw*)uCM(zlcr|16>O#Wd~BQCJ4KAFk0k0 z$%L>xhc2mRSjQWlhMR}1cW3Y%OS9E7^=EGv3EkVe{m~}jK#0|bxt~1fMSo}-Gnb9& z)1d{BG6#QGY~g|!KeK=pOm98vVVie%YCZ|bjl?guc*h-D=hL-R_cRt=_acHJ$dWyZ zmUmAD-7`ysqt&YKYH~AcVj!oeWl^h6!$^RJP)!lHc$V%FLPD9e4?_Y{V&<}evw#wu z!#%w){TO5|n9ySjV1z!NcsS7q6=GORol=$r%rMFd+~u*`9CFcbcwwoR zB&%UBR$cIw*KLLIBM!q>=uB?N*+0B@R-B#88uH%g2tyhwh(S@T7aw+*?N>rF!)&gi z>B2$qttioqwG^IWIoiHG&QSnI&UrXK`So1#?esvDMc3`S&P8!FKCe@5BNRX9bSM~G z=)D!A^t#=qY(C~K1Kl{}?GEPb4!LP3pjE;Y^O77(h|6qnYq348(_N4MvFFdqk=K!+ zs&n2Dc*&aw*|euu8&1pB<)@u**W>ycMPdoev~X=+%CMDL^VKdtZ4t)g4*rUwfj*wsJ`-8>G7w^M>mCOYhQ!P z&>pXl%z_?6YF_RtLh0%`H9Hfs72#X50Zy}X#*1~i$4B!ca$B#({+Jm6IMCW0>B|xt z!)P)j$8*n{;hr*kwM_hc+0{xJJnskkSISOUM-e(xS=9GYpDV!_;gopu}=rLIn(IK=Wb< zn9*8(PuD>6_lA`Z^%)2(z1CQ7=lbD$XgwG!v8>WaMqnv;yP2tgBr|nXVy->O=zUxm zce99u;IEGN!dQZ{3w5{~R_|AWV6Re|w~m`oZ*!x9&F<>luZSpL@Z0*y&l}nKO53FZL8;E0;MC{s3*FI z=i&_Zjhn%ww5;V~(|9s>8`-RB2j}4hDxu{eBUxAJYBZjXCVDC!5I&wTOvL*3lO7c+S+2JB)9>>9H7K3VsdO5MZ;THrD+zgl0w=@hf2!kh)?F=~;CZ$Q zZ|n@aJ*odyY%43gRZ13C;#S^ChW&a}Pmn?ozH_e`@3kM9GVvp`GLpr20+RwI!G4&C zzB9dCR+Cr^_k&RBI_AT7zs5zfrZ|QRE`-WWna%$c+?3=*5Dg;eX^C1NYBaDo7e`>P z%fyjmEzD5lVoI>}ez7gix7f99yQMc>JqV!}aJa*Pq_Hl-``X@ki4557dNv9v8Yd0%JCcWV?qtA4g~ zmO1K~h2?mq(Ss^Xhmy2TmTYtF_PXhB$?6B*uM;)oEDvc_0z&`1x< z^CmU3O-d`0OChQUTFQ%NZL-*-k3Jmx7M2M4rSU)=-x1LjL!*ahS{{uiUv)|mF|9Ua zjr4vv9I4!(clR3vF&n<{i1)ThLiiU_RUfpEa~bBnm1Yk7ig#TLj%L0ThtT#c$T(Ub zl{JY*lh|JA-+gI7&Ehx;^qn(14p5iS=g= z&3w9>Jro#msGn5fR_*~N?0mOcNmjF<1BVf`Hgb3UxSWa1=pj25AlWp5NmGqcGgIPT z(E)k2np#~{IE}V$1LW;3j?y1gCgr)NSYz_M8Z;0j0*c6$lcHUHmmLhNn9vX&jbLkp zz}$I49^llM7RCEM%eGd5DoKxi;Jq1fG0<%}^`(V=xHj&>KNRuQf!ZfF%e23&GRmHR4SgJNY)^7NS;_B_iNgA3CvtxDOlmUVa%~L- zs)m75{1t!7l9FS?-=xM4|71E{o7-=sI!6)lM^+wPxgCiJNIz98A;X6^Sz-%$>-@jc zjb=!0B#;hGxA|gc##9uA7!Ipr+dperYRB~5zZteM`c?}|I;G~d^=E7fO1!vlEL60( zYYj}9tFv`oO9gT64--B_Px@jGRouqiF3S7R<G)85hRN@xDU*i%RP z@deUS`|=bS%`&NUi_Ej+u5g@n=oiazw{};NT~`mSkxQrc&qkwGj+Oq{{DTAuBO2k* z$P+FJ4VIaE%d`14?(x(^VaEm(=>yiuLD6R*8$s6!*L@u_AmhJZfCxq;HLO1D`wvGF>}h#Z@3|9)dpneI;{jV%l5>F zR$G}8kuOJ73Q81*^_dWP{V>_(!$I1}vs2r}8dFYt?Ct!mhv}Hv$UH!5bwM4(z4|?8Y|aui1iagcmc!9(64@zF9a?y$EP;<7DD8 z3lgW;tNJndsF=7!BBP!|Ka-1C=Fa{Yg*-?N12NZ`?(&$GpO6W_*X zeQYSh^=l%3$d(CH;7cg>qWNg$jnpeX4Y`5eM5i>nH}i{eIM;}LE)IC*VnVUII2_@4 z@mnLCM%FLdc+p)Fm;6(h?B{nsdu^v5ho3#6jfU;;nw)jqq=HdwPh)NuW*c?wl3~Q` z4IH=^%b8ymDq>#HkET5B*N#A_s^Zfv>cFvwhVNm8Xvjb9yN6T9Q!V<0a=(f1a%q_* z;)9nxy0==vGU)3i?Q4X7=H$i&+1ByZ58CSatNFj*3DGk&e8w^w7ixa#o^SFL$rb3b zIlO1}$M(xbF&X~@!&zl5{U%>7i4)v}V3_RbZq3=Z1NQ05ORI~C^vmUgfcL@RO(8I$ z+5@jMxWmWqp!4L@{s-#B_}5okE1jV1g1(Mg>pN~=o=~NwuCZbYLE-rp9M{!wdxKRU z*S%RZ%>6?;I{)##$b<`NDfVIYH0+IXHO=|)rb`hG|ANc8ya4+ujivU^&V@i`St}@~ zq$aiq?(7P)6$9BJv^3vsZ)xT9>62jpD{0uv*3T2chKm-%JVRs)$Tf>$vsBOL(e9rX zq|6%ERE)MSq60)dueWA3<`-DbLwnnJS|8_OfDht9IDC~%eC=&~9VG3%9l$S$kbsaV zuK<)+Q2c?QgruOLq<|QYfPkccKvFlh_kR$$d)mVt1OMLyD>-e$fPnMAp5P02ckuDG z0iVbF=ON;}0-_HDL?oe5NvQZg4ye`)L_9X-9>ZG0hccN-@MelHKFs}=?s zaGCo0vX`@`ucwc*ryQ&7xhAAJ>hRy8FB zQ9%h&QC=ZFK}LQzB0zEDngV>e@1MpDylm_oAc8ogeP6+~za0F%_IHRB{~0DhFie00 z@drYJl7b?Vf?$}$z%W6d%TEFt!T)OSn#R-nfv2Y%1ZMAP>%gk}?~b!7D6k3%2trvo zjo}{lo&i2wLK+-gfSCK=#99u%Hb6Kw|H~tVpppX6e;z5;A?fiyy9tH@2yC&2^A5Wjers@t#=qLo(vnp4wDSWk z_^K&FY_55gwD+`w+e`kpsKf;Y?CnIwp}ZpYVnV!vf)1j*wgN)7yf&h?!glsHf`Vca zqRN^&*X&6Dce839Ky5ti96+-`4193Vq#zXBEGTHp>mV#3!E0j+72*{KTE%N8WGm_* zY;PxF>nQX_aqL=rO8?!EjyK#1?qTEhKl*vypgm|1Y9lHkAuMjgYYVj%<`of!I`WE( kI@t3HI@;KXIM@gX2|Eh diff --git a/toybox/VirtualKeyboard/1/assets/images/spaceBar.png b/toybox/VirtualKeyboard/1/assets/images/spaceBar.png deleted file mode 100644 index f4312f3e319c382909257befedb4f82be34670e1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35754 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 \ No newline at end of file diff --git a/toybox/VirtualKeyboard/1/assets/images/spaceBarD.png b/toybox/VirtualKeyboard/1/assets/images/spaceBarD.png deleted file mode 100644 index 88747872719a0a357373e39aaeed0ba3b34fe7ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32504 zcmeFaby$>L_vk;gv;u;Fgn*zlL&?zHAYIa(Lkuk~Aky8^Aks*8iF9`(-AMQR@M(G8 zujjpf=bV4es@&bXLIN_AkPmu3RynT{3`B1L;fv_+Jh8hbhXI_qb8*p-H6b+sV4Q|g?*W>m6Pp+x z`$I7|1>#@9c)o#Yf`q@2yyuAn;ByHP$OiC0``g%)%mvjIfDv#A+ zfM@bqyekYqBkIXRnAC6pDYoHTA%F=dAirPvoiIR^5rAVP*~S5wpaU?9s~Cv^iYoz4 zefLpI0r$`VjB;;%p9A1s0XiMz!CW}(0RU3NAKpIg;k8pAMqytcTSx_lHr-|; zB9u%|Z|P>GKhK&E09b^$58lu+d?N7VMD(=0Nq@QvZ=!{oYJVDNREo)04oKaYQaQB# z&PF&bta@^CWqEl*qD??Uw@bzC#<)ehN#)w&+@0&{^mw7+D}@(O(@PZoY@wy+NGj`b ze+2Ryy_wZe(W^>S=oQg0S(~_CwJJ5vk{p&zh)_b%Aux+9Kp^7z=chwZgRk?nmvB7u ztbk%O9vIhfx(}NAa3_*f_f;WHUrzymgIaL&$g_KJo+dtv{f^K*-fPj+rvOhQ@dz6L zKud^%Ua{*F@0WW3fKaLzbumBTQS&3(Cd9|h2s6#7S2}EO1Sr~C1nvu<7`l(R-&# z9&!&a-uLWDvlu*9AO&gXTO2tu$#7ONCi!=Yq)K8$2i&%>9R9CFYQi}?06_06zdBK- zD6djE?h&+d!Rim}1aCyrQ3nhlObMa5sfh!&Wdz(oY-wu)c_x@sp*(_L7@O7~Au+dE zFw(cT3Y5H{MUK?(E+NZD%WEenR?dg34LH{CFL8v&@)B-BW`lPWAk`)27gLo|m6;Ix zK>C=1<-rs(J2KoGxfUw2$Pbcblr;~|+aBownfz77XvxR0La`a{yS^1{W2e9h5Cl?H zhrx=MWGW6}4T%g{$&+u;<|eX;5s*iAP8pb&B5_2~lG1eeE+;RyE;BC^E#^%_M~-Odeh+=VyfuD?pZEyrkukpG zqoO#eZ2aLs{4a`(>`Ys6Yw?)zU?o-R3hLJn!@E$TC88;#38@(rb3bN(ocoBXbg0y= z7@Di4;8rN2#H_HCi#!sOr}Q!T<(2}Wf^jbA2mg=8AHa&dneJ-W0f=g?QmwLrnR2Nm z+Dmoqq^SD%pPXo)n4{-l@DrYzDeS?YATK3X%(+j@1^_ z<~+!$mWh-PC5W`n_4wf}zq1oKh_cK7o((`1FjVNqi|<7zvf z!?%F}lHT3)u{`vNgzd8(^~(~FV;jvym!ae2#7~JWJ0GTtcG-4Kcl~EldRS7PzgJ_n z8))qiT##ECFRsjT{CKLnhsSfmW1F(Xv&6+d z2b@#W?qP4L?5OOPZ3VyA_MI2>Acq=2U7&-29M7jP#&7{%{9YQcmuD`opEQQ>f~E~m zN+);tgHo&cAM%HK7kIBU7d$cP*bXara=I4R7v5)z;rr5keQW?pcU3pA)g6t>*WXvg zk2l2ntt!DFQ!K0F;EbT%ywHVEjL=-7F_SDu5uF4pHP1(SGp;H28kQQ~c6uYVo>A3g zRdEZ+#~k$3g=|(94f9t!hyGuazoIU=;>9s^Xj(g`^|c)N=HTYiW6^?Q_@v&-yolQ8 zzfb)EH|XgBw&T+f6z3MA7GpAoLAJrS6N0a(iDeGNsHI&Zw!`aKohd#uXVI=yzoE(v ztCK>PnoR_~$4zh`wFGj5>Y02Z!ozF?K0c9-tT5g*UNv&)&NuWez4>evSjj*fjnrxW zDJ&mBjSi(kAbVX={n4Ko>_$-g}zfot{r!(_!pMdnwQztQM@rh>9#f zNfkOG)z$iZGmcU&8|P`1rH2SKe#(gXTvhKXs4Yij^LA8X3*GA}U8K#uw3@rTo6BL$ zw@{T(R(##IX2y3kWmOr}=F4?2{E`WI9tONC#F@s6edsaqY{qGY&>!5@7(WwLmYdgB zZP0HtWVEq5NB3R>Bj#i5YAl8o`s7p@`n+=4&hd%xG(&9fv*}u&3c>1(My{$l!<^2( zUQS-cOvNL`3q`iwq1kfX@iVKzT|(Eo6PE4KX^pzmYo5-AX(bpX=~1Im!%OkYPp7eY>ATlOmFcU~PNDxU>NVwF{)2NZ{;Tas;oO8Jn zohHFdV&PJAmN-=GG@aV;9*#6V=nqPcPIhyrKcCT=POorX)9;%%!vycu+=na<2}1+d zIO{n3UL90H4oat2uvMy55{f|DmYa}GuA9hV*)O$8wF!mQugt4=UFyqSjZQZR-?R74 z9@RU4!-bBGzUSbctv_@DpME|)p2wYax23x7>#m)2q}na0ldaP};@jb$6rA-R<<+>E znb@20Fl%y5Jsg}7Jw^2O>Awj-4#gBi2#pWz_CxpEdjEolgX1f&?e)f0j$pq*KjnM8 zoAjIRLOfgs_p{Nh@xH;h!L-2t)YU8 zjH14axju&h87~hCmlNpLfTbZsm(r z|8x`9f9@pyv$?;-|Kn2_=>Mf@ZEtJw8(;%{215%&%iB8bZuOY{<1;`^js9+&A0FYm zQMIaN1i4DZUPQ%E4>y`g7@_WfUHl%NT z-#{0l``=jkq3m5dU;|Sl=l{ye-z9&vAuSCOx3YuiTIm~#3-R1OJH4r?0Z5mFS)WnY z5J-2s^3|hzq0hoW$6?69PRGc~%xK8U!KTY_9puCPOwlJw~7&oi3Xmv%Y~Y6B9cJ+n*Kv!`QznlC?Fx z9hY=1{!!-pvl!e~#>{5Gtjm0R^FR&*W;zx_7JWKBeO*?%7cYQ%`izDw>@QgK|E%Ia znE6*#Vy1St-sJobFT3@%-$ot)P5Ge(#vvWPkR|zlNN@MwIV5 zzjbhsuKxG2he!Xr`x+W>G5nDG_ig>9`G=b6znk;FL-4!(f2jY@bvT+BTK&xZe>e48 zssGuC9oPussB3G;Z+z<^|4q;Q-Rh5Ocg=G#d>^YVO#d%5gq^O#|6VivcfIQWiDvlQ zi}g)(t&9x~co_b4;6F3|yO#UCJ%8+3fA-dY_e8G0N0vWF0{+{P?R$r{`_UtQ>#zd4 z#y{qMzwJN#=g;8-$iybV#>C9T$jHtw%)-RPFT@5E7J9+LA;QMb#wNu7zjCtUepW1(C|J@q@(7KH-zWhD7$YUojDnKeOEXc;h!Nx`hq-Xl4nSYe~haCfbkP+C{ zQWwHwYN=~%$Y5<{%*F6`=08e)pYlPsWAE)ap!?gHcWeD$d3VdJejh_E?0+lI4rILj zFx)oM_sRD+)qjfbs;d25^`GLqs())LS(!q3fPd6>C;J~--`6Fe-#9DWE};#1ZXfM# z*4@%CO`QxaRE11$XHL8CGXOKoZKM6BbXW2pHP!ysWV^k;zZL%>_(Sn`@A}6|_qXow zd+zNz`gRG=@YfptUp?ioiRb_1m%pFr|D@GBwtiJ|2guKpzi|DW&mHM6Tz7!{JoyXP z&-vVu{=#(!$j_6%aQ&Rm9qBJzcYypn`3u+2`P`BI!gUA8&y&A!{hZGo=`UP&fc!l9 z3)j#2+>!pmbqC1LlfQ8NoX;KUFI;zk{5<&!*U$Ofk^aJU2guKpzi|DW&mHM6Tz7!{ zJoyXP&-vVu{=#(!$j_6%aQ&Rm9qBJzcYypn`3u+2`P`BI!gUA8&y&A!{hZGo=`UP& zfc!l93)j#2+>!pmbqC1LlfQ8NoX;KUFI;zk{5<&!*U$Ofk^aJU2guKpzi|DW&mHM6 zTz7!{JoyXP&-vVu{=#(!$j_6%aQ&Rm9qBJzcYypn`BPjdfB!1h(CYT5u#UICdCkqz zUA_IiEUCVjyfgsd@)Q8@^Z@|Qp|{r!0KlFR0NB(406=j703J9(`?ClD@M1z-h+olZ zYP(+7LdiH`?c8&gdb-pMMbgI2Gf?%n)zY0|VkDL)F?kBNlr&1F??=nbt755jG(H3)82lHL1O5-X2nG0ilx!y(Gzc)b6-X>XM7-C12PeIt9YSq9={Lx8Fq-!$BXXnZ;}H~|YGh$wIcScc3?TH{ zAZ4n@RG})uL8b5Wb$V9Qa0davpV0aI42puh-P5-0?_Zf|rnAL$fDon%1IxV6TVBPe zF@n3>&?S%L>2Jj@<22Ei z?l*~!A1`%VRaVx3?wBo8Z7e)>eRA(0@&#H>AN(%;SZyak1*4bI_dJA za1t>r!Vk$pQ%K6;jCoMD9^}E|FelDkl*3nJpw4!yN6d@{;!|Gpf+-k2ACeL>zmnOb zKO4W^x;bvSKL2!+I@bPG&KzXO<}knLR)`C}oZ`(!5TIxq;YBQ_X|`zq0zCUf3xysgX^q^%Q?5| z;aOHMA&7)m2E09CU0Amn-8HXzsovU+v=E(ZuN}kcS)DLfzIs0_Eg4@YrEi^kVAw@V z*5_3u1_vsnvrN&)!dM>UmCuz;(lPMlx(NsU0zPg;$NM!Ysyw>E3fHBlI8i!h5&%C{ zGGUo#pV*q@MaCC<;tmNUZMf;l7DJtItqr8rG8so$O^7`Dh*-`X!fIwOnm@MHBShA zf>!ef6)TlG=gab2!&*)*mK#&U=KITuIV5zmemO{Xv*&KbGjZmfhPvm2w)(sJPA>Y+ z-4ev_-*@(|cp!IEsWfyI`M;X&iepFbCh<#ncM#211QWy4iN5UEB5z2a=<(|8wIPK^ ziwEF5@g_+N4m^}{SmbpY(x{E6zejSwT=U3)4W#;PhdCAB4d*5qiD&@i1~0`VBwN*l z!A&*CdSkOU$NY76eYRH{7HqpRJEPS>(mAhg3b&V}66*m!C%vbzM{!o~UNlcAGqOAM zjq!8X$&cV0U)<1;SA%W$yvO5t!!GYZW@;Ct`9Ruxrx(Vg(6u?v2Fy9*^9Oqwd{W!z z3*7skYVO_S^QOk~_jM}CcqPR?-$0r>0rI3)9=&^y2yJlhQ)VRTuov+n;=^5jv9-+D z5fsEsU?y{d1C0JKQl}w1fHES9n#1pC8cFZnQ>qOlJuL0e;r|pNaKDK~vK+&ECOU`< zc9>Gr0%;=>d zF-!xX;G1h54=Fk85KbLZYQfF8yE8xS4#^=>8Sa&X+t*4M<{&%R9u9?-P=J%4QODj9 z+oddD&M|WTyHiR`k^vFR!HVK#k4KpjX6x`5aR%6N>S|#XEg3#>9fW{Anxf$pI1iD; zWAoHRs%<0%hG=<`gZ_W9+&+cIdmumhikreWH6oES;{&nVl(fU_;jokP^F;h;0$_|R z6R4R->u^X-fvNC$9EDqrWrPE_P`MSQ=p8?Gj!mQ?!rMm1YekZQIm z(XH)GqBsorFl>GJA7dHSJ4eTeZAgcn)%~IGi=2iS?f| z|8ENbk8fAKkV%q)UQaa%ZPj6zgUh|~`NGM(i(!w-tqVfOMRqn7uo{$9d8&eJnm(R7 z0TB5)eRaJ!eVyLBvEpbZo!N!u%ABK&UY zA(8Uy_i36Jp33z*B&b5GF8N{nW}zV)iY39mkanSMWct6Sgzqy0Ljtr0uE-^EOJ^tc z;idK#yeapkk#=vG+4ZRd&mb*8*MmJ_&kr)_zEMfaRJFm=tC4p4MxWIg zq)&-rcW#ArYT?r-7w6Fp?7ee5f;#X|T_!O57_Hg${V`+xIdOTP7hP_T@=mB?thj11 zjpjTdV)q;6AE;YBe!|!%2(TQYyMSM(WNep~qM#e5bAdIGUtJ~`5Qz5kYX;r3m8mh5 z-4-!|%%o69g_UbC*W6o;>**#Hb4)KLlHpV6RT4pUc=RgUPlB|Y2*igdT}z8&s`T+u zuvd*m_J<(1#+1TLra}x{o^f}}QTo)-Z)fn&;~ycD-se#+2IO^?U_S~5reidcK%Trc zhtxBY4^ciP1~+mK1ewGW4?R!y(&}rI*k_wd+e^A#OqhlmMqQWrC6Hz^S1^jiN9gMC z0gvF&)pN;9dV2m|S>E~=6yY$I<2;rUvs7lth$>D15ewqYh?=`PLK>I1YW}IP%17vR zlabiRM_v#%C4GV?+%f8mbYWcy1g39}nwttp?OTqzcUmrUyNx8M^`Us~^B14hrTu2Y zi6;p|%yFcOHdyPKw1FD1^961oxEt)ocJRAvKqUYx3mQRw?#wm38DywF#?wlS&NA?_ zgC!l=63Rdut${9cP<%@6x{^N=Fsn^A*NMi&9lps*!bLt;Ju;sCxQE@S2<`J%i7|4Qsdy`7T@70>j+!%R?Ot;Dq^_v_Uak`4}T1LQVrivq}XtS?Iz!z+x z;CFnGxt)(3_}#vQvk+9^hta<&7;UlHcur>~XY7}yF7n4(|Bs6{7c>@A`T9^=|7F8k z^ijK37`X!Pb7-@}D7#15;m|7R%w5>t>blkVZD1s}-opZsfQi@+;;$9+TGS`fW;7CI zc+4&mj$eRjLY+Lh&Ba6v+q;bh#z7`rx;eoeU$#|}CfJ5cQH@!p{j>+Hf_$eUaDl#k z59A(4kl9S5R;yKX4cQ6Jh)au;ksl>q(cNGVI45`q5XdZ8>k47pm-{aKh}rwRk>z@` zUyB!GMOF*HP|N!$p*A<)8Q#z%iI{Qvi9LBe=P^EGz#>ku%-Rt(BekmmK|Z|@($)0M zCkHRXa&pFzQH-Y*wY@Q8J0(JNy`-7~&n{-W89S1;S!)3XVW>?+_1QK|2k9Kf^q#K_ z@%%kMHa&~w;L*h!cuFLXj`l3g%PPmw@!>ua|9yiOY>Qvwz6zv`66yon5W?gp$#Xn) zK~C^kyI&)jKJ_2L`nTE`WC^c&flbz6I)tYT;g2YCAlN2-yt^bpfk~#r8x!=kS}q%i zxB~lZiO4%_df)mwqgCs$Yp>C=TaV!Ine_N@S1b>o@B1oth3nvvx#58HCN6`~-ri|J zS`WJ>6KXi4&SeEzK6i&RsvX`J^2Mze?vw3AgeOrqdi0$_7TG|8qupqW0ob0C3+Y`@ zpv3++w8d?^ZF*(SZRc)#5MWS^p8jY^G1PF14*1*N@Xw5U_ds>3N2Qr^VN&SR1!CATd{M@zR1*b29rjn|)Is#>H64J!@A3{ZJ# ziiCzR9R~%(JoAk`t1QS~EY7m4H8MUSje&;~0U-SZSeo}(Yhte`rG z{4^D*WWh;BH(+*Z$ojlO@n6HWO493NOl?=R6{4cY^QthWF5UqP{btp~NYJp@Qyw_* zSjxEaKw?b?%s}AltY-KZ49U8Pb`K2RsCRlkj-d6LS9{U)wR?-Jmljt#$fib*uJa=I z397B2OIAhd=i=G{veuc;_noq6#W6dQ&MC^VtJ2r(vDb}t5yPXCk`REeGDteQY&MY2 zB~CgRtwq2hQ?bt%G&HFztFB$i8W@fUWLy@KgAspUaw!i_$l zichamRmeE>MH3w)K$vLZnBSesL)s&1)WG9s0H!zZ)|=5N8-chc@xvxb=j7>@uu>i! zvV6YYNe>{H%R_r;uAYu&&FhpwcM3I(fO?CD7j=%2`f`Kj;5P7Ud!;WQI}@LQg$0Q)1#_ZU5Ya9$YSY@w7qN4Y z9M0Szsb@y{oByZU-TaewvzM8Y*>H3LkT!HLm0(ACjfg7u13A8MlH@1hOhLgsw%b2u9;*7fd;Ei0H2z5 zg)!LRGHgVEf-nDLXYtcrgD*(a@A64Q+>MX|u`ESxPZz1D5+hzr*i4C{Hm2h~HC>a# z+B2YxWqf&4C_^INkY`_YXTc?RHC}(b*|q9cT+#T|YE&>RWgL1m0Cp=>hZ=KYf=_g} z*hiTO=bm3@y|T?nTNMEHq?=}B+E08eXxa7@z1@qui>wZOoj(Z01?G2*rNi@`ao2KM zy?f%KSCm4vivhA2*~l<{MaA@f;&c=8ey?ou8ApqhP)%Hv*#Jx_NV}?EZ2Ca+8wQn0 zCcpT*lvXr4brXm2jQYm57bt7j<{@8xoWQ~HP)Q`MW33{e0|uJXQBBeUj6o@_*eHfI1-tM-NJGLX*yF8Y@{pgrbx;;Ja?Rftb+SP~a40@3+vrOp?g+v7|G*a`1M1`rC;nAjG)WhX(-_kv3J*!Sr< z=wU8bxF_Lr7Zq7^Ji)En3tKA<_=qRe4Va)*)C>9!Q#+77fkB@|1shfn9`OUQ7zYki zMI7Si1DID4vw+u4-ZPk5Te_sJ`AL8|)SUUs#d)`+@mBa+OuDzq12C)QC~;`${^qR_ zPG=tk%&(Iwxs}aQ2C0b!*4R1FJEHCoZMHo)-o5AyUz@rcm<$5)?UwTIVUR+cRH(Ju@oN5i+;bP9_|}= z?*-ax+gRP9am(@;(?N2CMAJ9%0tQRrERQhU=F?}Ly0?ewP>^eu$jPD{-(bZlF{(wUDWcI5p*I14436AW>5#sEezkSdkgU0=a#!(j1$_ z`&DL;xM%9iLb{+5R<^;$(ZeYeTV~q9{w_dyQ{9^rynp~b5`;H5CPvTpi2}FjDB~6! z&Kd#ON46*?GTU>K^rVF20*IqeW`(~s><|Tho;FfWUKIEc$+Q7SGyq2sddks&PU1Fm zZ|Ob+7TFbDbe!<}o)w7D$PsL=`eU3^n-R)NvPREs3FjfplU$40o-ztEi+o;Lrv*hj z^RN3dXS`s)%BvoaNOUS%N4UU#u5m?G(nGQ68iOMpYk-10&ured^;;~)K}t2qRA3uQ zH0-R7iXWOX5H=BGrhMC)gSnQu{0eZn`MKfkE%U5M3ZGU{GBLA_dhx#{GIqx%EBJn$32G4z22z5C;Ctu(_Azqf= zz71j8Lob+LZ)`hbvpou*PdZMKBpzF%?_tTl!+`PtC*AFrVA(!7meflPu3=^5%@|(G6Q51rjt4H>v<4 z!QVb!F{z%jcywG6D2J?#pZn|2S1z>nw1#|{-|jbYT`cGJu!R+E+Fq;pYKb}RLiZ|c zu;q5+l+C`1WbAnUv@zv{p5EryEw%6yYLTA1qZDtNbjv_wy+XtN=|{Htcuq0MXjcXQhdZY=A}j^7YHb;3!D8s-a5CF=il0@(mGQ zI^ic6bWR?jrIH(6-w3C0lEjMJSTf`|Bi>3P1reWFr3ZnuRhYoj4U}=pC%fg$jc!Y)Yf+or zxqw&0CG*yb@JO6=GmG$y57%1;)%Lv`Q^~I9m?`3V1Z6d59nuU^-}<*ya$m?kYjHB& zD@5FlXm=f%EeO?sFx4nuFl19xM`o(W`o*FjKOXFZN|{Y(Fj+Q7d`?h+z($f4F(WLk zcswz@+4?f^8JxQa7$7~9g;l0GUB-;NFBHfQnry)ii_sy={L%yCd@o7rv^zTvb1Lbz zo(!yIDaUq{O?doTn`@3#H%1tU(vU4klGFN)tTQYq4WAB4-M;oBRjDR3*2~P+1wmdj zVKjp;rj-kx*!Qc&WycbW!kx9bc@yvPO@em5wBY9KQ8Md}&h{$pj;>VBbBN46$57K? zkC{fKn=1B*#s|Y(6XS>OQ=F<3vuhsfqid%=(6mb46a=W2`*oM|zR}^s>L>f62l~|# z`v2OdD%BwMl3&G30-Adk$Hd&76l*^9GX0d(zyH2}32RQk!GSCN{#) zEqB#c*N5>&G?ot`&T$mcL+{lk&gIOB@4H@WHL_Q10hx&Ji~@K!=Z>+7@8|rkdS8tW zk&6{=A7o$k76T*p4oEyt)!ggHADlTkV@{Oaot10h9;@g_g=SZ^?7&7lcSdaR2cc$JMb3aCAZk>F&;;A zrZrBmf!?X78rs89(6q|K&>nbhHC+NpVitV#y+W&zY!e89$e_-!_ptZ_nl%6y(IQiB zvDlfg#3n@(8ad7FN?bz;R>%;#l$pqLF7ILoY)}?92qwFkMGP(uISiBUr0WBVkIV+q z&hxho!)nZ60{)MNo_%ZWuKWc2Yvm$H&1jwe2QZ8e9OhnhJdXG7;BWJb>z#!9kG$88wS|o~c<%ak$1%s4 zdRaT(Pj(as;p1p=Sw;OH3Oge3=*VqU3mYGqY#+XnvmQ}k*Bz-%=oK1iod{tzmDF{d zeJa!`NA>p2ix*f*RNId!CWq(-fpl2Ui^k;XuiWKniDr$z-bj8;UZ2#!a-`v@L~JQY z`&PWOlQhF^(hn9MdQhbI|1&^)|MviGn_HI*cPsG|WD1sLy5lD_75Z;MsuUJD>4*7j zy6F*BIMP)g@Cs*V*$h5@t#?v6BnqT_r3V{J8@Ajc@mdQgx04aDY+o=dtk+Ul!PiJL zHwc|ZFx@lngC}B8=KSkr;vYN1)yCaVDo`DLj;KB7&x$F25C}{jQ-`@03G5OV`~c-_ zx~%7JDx9D#nQ6S$AU0_vkZRl$_hXa*DhmhIY$=rkIWuTi_Op7Hy}1Zp^W#WoeE1C+ zoQhUtlaJM1Ci(2UhGrk&OJ~yH3DWBa z0md!p3zb-Bn;5`rzr-6sRXQZO$t3E5{n>RYD*vlUy0tR#fG^WvOcrwWY<~!fNop)} z307UO)^`w=JGSD^o|>8=7tv#74Im9_hp+&k!OC9N6pR}NF&>nviTm3RYeLc zh7&&WSUj~vt;AZ=oW>_-SqOgJX@UH@@?-<;O!kVP@%afHmsLJzxr>p~!5>lBfEddP zY+-SKv`Y`&+`3GvlHf{K91Kotq0`v6jzB>v*-V#Sr2e;#^vC7Tkq(0=X$1Qj>8;o- zb1z7TC3aleTQnlnO&xyNE*xymN}cktMit-hv2U zPb!-2E`Q^*^AUVwvX<3rzkmI1bLPUo|2`fSRU5T>FjfgeICq<6W4+>b;)rsT2}8@6 znH*)*vq>t}M-@xHv)8fPK@Wndw#y!z$6dd}=G;6tcfD7X-noxD6auU0(Zg`%~#q9?#ISOB>o3fMQADv z{4Tw-iep^frMxnjaXVo(;d99sXmM@UD5^PjE)O3?a|6+p@zUXLETL+(O{G&09&MWW zV@=N*Np78}rpFkC(Lcet-Zgw=AajzS2>^B=EQDwcS3PnbOk&34D=ZBZVBM3 zjJh6yk&B2JHNaOnTw9BB=0kc9_&9N2U&X)q_-_Veq{uOb2_qzqZsXK5x=5#n}^6KCvkqZgL& zTPD{gH1i!M4r8|RL;TyT%Cl-QEKRxQ7fv@f(y`oA)izKi=Z#x$Pg9T}kQs2$eBKjf z!+I6nS8OncU?fJJQqDPOBp)8C-XlW(J~!GxInU5Nz)4ip#G@pkpxyB^JN2M{EmgtT z=O3dt!nZ+@ro6c={AYX9)@$WOeC}+gkz2ek@-D{qcBd{x5G*bs!{_hxqt3E~8#I+K zAKXO9OyLWqe|}%IBTdsmR$LU^4@2zDZFu0r3 z=t>SE%h^NbGEG2F94hd==mPL24cmgjs9sijo*fetJnzJe*uo!i&KN!$U4%tNkH`>3 z9P4-yUtRDO)7WuVZ6|m%RV-EtNgZqBEg@_X!Cqzsh0XPHh9?;>=*}Xg!(;YRsd3^p z(U{d_aiJOI*uilx$@TtWuYGYr$;HG4c7K(Zfn;&9V$#fLj0J0jSre95>4HW7T*t|i zTwgIyRi#K>$(ha&%!-F<{Ex4DFmRtvg)Hy3DwVN6wJBaI3&nL8rjr=z%b=L+l%m&7 zz0yqPiFmEDWxaUk9mH9Y>(*#=@1>5V$LHCP{-nG`(2cbDi&y10{SL%)+pS~ZkH&0u z3Kx>q%OZlkzK}TKVG;4IVkHgVNtnJear(|0-kIP}L|>^u+}i&3{(uZ6Utt!$82%ctJoFsZ_vIkgXVSG{Ss zISa02cduGJvjc{_Thk{)9u2*t?7jsi&`nbj+k)Y2ntYpfZdctk)wT-EkQIAeGZ{HyL zH}#R(PfEpiwKzv$Q*n5qU;}4XVyX%sGKkiG#Y%qBV8^bNPC>F$DrsUU0a8`%m9834 zp59rn+=8|2e{23I<(XFAXO0GQ28EghGQqEH&Ue;6EYRb5<(sk#f;vd8?R>UnSj?w1 zu`_MS>c>b5PdJ=aQ9uXhA8-GMxyd4Z7D|?f!KtX~g8CLs2q~rZ2lpT8ds!`L+P;E4 z>2@temT%29lYyD{Yit_tc4J1{R(eeW;ln%93>#jBY_fDDO}f5`nI}RuN!?g0pPw2BOdXy~Be=Ek@cdYn!fGg9B2QfuHb z1RJ1`*>&^ZzBkx_Ev-|@?B%#mpQ+ACj97-9iBj8LT&fy)Cj_8(o->ve8b#-cFVmmT z0e67OUVmP^8e-PkIDfoC=%~9@v03wOa$>hUGrJ8Im6ce}t)pFvl&4}U5VyYI3tzjy zB{I#|Zwq^~n8wOVg=$`6@KrU&_f)nI2d_ zI$Nf1%`BQWqZ>{aMPVDh$L!nDPHxKh_@Y6$_FKBVivVZjL>8WAfBSU%Cne)6{Sqe) zI%!EAseD$Kna0Z`toq|LR92=EQLH;5t0<52mn+&<7w}_Nv&9W&h(i?yz0Vt#iLKw^ zqqp^WN&Ae-mtEl11qkd-nBYyneNCCcM|AqlYbj}seBbyugw1D0k3rjOZUrO7=XDp3 zDBotLs0=K7MgUWqVF{n6eiSz!ujFe~kSrdjC)hs9IZQX#2xkH-XdXOH$pc?4h93#w zPkW{DNZ;Hd-6QYz4Dhm%YI;gEjG31+@(u$Q_GccHd)I?KFBdp*CpDxPn|Jm-U?|sD za0Zel>SJH=u@(BFeH5$-{OUH%i5pnin-6OBeMzN1gak@OVj|^;>u>P)F`Yx} z`_9pN_<(aHrym-qg?ehfsP8_TTpq$x< zWw=0M-n1trbfftuQ}5ChrsaLI5#`ks$b)?w%`t8xt`EgXqf_$Oh#lH^e$98WNxzcET=lV!CW0s&| z6Gf<44s-TRsPag4OH9Mk)BUP?0YzI(SbS_eBsk>Y*E^RE?5P!7WCaM}g%6=q{g#1@ z9?CwNZx(~wt+?{$Te^0y${LZCjSiOPkUQAnc{^^8=gJg)R?nWqz-4vM@hAdN&VN-p z*DpRWy$L&I&41iqbcp$-0o?2#?@tukIGHkd(qisCS7@);98a{1e%ZQc&ET=m(33KqBpBtM(kxb#fX}urDI~H1E7>NgBi0 z6S5g;yz7H)Z`<{+TTz~VBjaVW9K?sNZxYcMNS(wjdPCLZ?XZ(`+V_rjae-8>9yj?Up2goj zuqFd9^S*GxlF%<_Ui(|7DIY!4PcQaN_cdgm5bD=qeoX)9&P3k>U8z?x*W!h{J>Mh_ z=0e{nry7=C)mOdDI9eLv5rYC}qj>&4fC{*Ti0pa2A3Jm*SXOVDaNvw=W`H;+-JfM! zkK`MXWZ(Cqg-^y^=DO6=o4q|*j5hF8+Lpu#vkEh5aEV_OWn;lyhf|{pjdqCUx+!UF zLnbv)=A#$E#z<{bQd#_>bql%=Jv^CJ^2p(wkdz%z79ZPph+~#Z2VOM-$bD~Qpp$za z=_+UrkWeN0)TFk;)R=Z502d4?^`#=EYZejVjN`m534Y4JZsPe8 z8FPQYsZ4wf5vjdYm&K!4hlP772k;O9W_66RO^0LtJcu|0-01QVN@fj9o~ReCzwL04We~#m#69+Fr_Hwdg->IBS46vmt3wwzf>-EhoSj@K*<+zkX;WUEQ#TUZ+SBOI zvCw){>DAe)(rcR@66kD1&hWWY(JHN$o`3P}R}nd&xN6*e(kA9w5{V_{4tS{8H{ARlNfEnC% z+FZLndn^OX*#uXInq z6UPONus1zeKph}x{IUfHlqZ*Nb_GmrdhEo)o!o>V5b^x)ouaj20?essvlK) z;LNW+2_Z5|gg_6bP{-RUjG+ZNX3Wxs=moW-fJccXgyQN8E?+5-_F)qlW)(&q!=Wzn z+)e+N890t!^mzDW12I`Iv~_w-Kz+t|8F8khRfn_=by@TW`V)=e0p4KTK0BPaksx^| zc-3Y-qvcAZTa$L(ut$L)KReJf2969yX*?QyuPXafM2m%=`Gmgw9bhYl{634+C| z+mgH*^T-?0=CX}fxE;KpnWVXTt8ctFC;gT4&IQouu5rwUt_$G-gvJ?89PCMTULcK2 z1-`^Vamrt@SOLq=hpwBHK`tuX`IN0OGm+fR1!7aXtEg0dv)p+9-#VuY>)xKU`)!!1 z24DHfsU@=_jr;F&zrt_Z-;UV$ zaB=;Ib8NvM`{OM6|9SkYKOHKzMYxrZYaws*T#K@XlYQI|^K5r}>o0q$u`e-t*8g=3 zYJVguwD?~{qD2Tg5XZhO`2yq+L9h`UX)%P zzglL}Qs;Aa>i0IbzO|34mHF73XVxmbUaPGBIg@q&q>nd#bHD#?jlQvE{>l6E)Tipb zj;OZ*=*Mm!QK9_=axcI)lA02U`e$MVKSH3-){c^kg>-#_Q|IgoD{{PWk zd&{dI?ce<@{5f4TJ^r`+xxXj9Cr>>)```3mzrHkYy&d;!zj^+&%PXZ{dOi?i+!4Ly z#rKBA2j(+K%6Z#w=vMqI{9r{JKNRedGE5S34J- zSrWHyw)}CX`fjPY?5`U4$4~jFbK<`KuAqXuM!%R94nDsmdc*jIYpe#-^wh@vENUws z{_p7(-Ul>p*5@O~1O4s)=W;wden8ji$K@L>f2Ym<)qX&)to?iGJbTZSh}nmJzhSd` zxN(o*k(u_)b{CI3#ypLYc;9fi!S0%Q*STO}yQH1x`jg+^{^;Pn;lbgG_e|I9l5HY0 zA66gG<@mqNqrkrZCEB>K{(=8urhhbNJiD?r#CD0*^)G9D>p%BqdY4bl-BozldS8*k r_1Z`B{}aAP&zIf#FP#7Xe@2Gm23<~y--Ee97l?Yg`njxgN@xNA1T#Jx diff --git a/toybox/VirtualKeyboard/1/assets/images/spaceBarD.taml b/toybox/VirtualKeyboard/1/assets/images/spaceBarD.taml deleted file mode 100644 index ef8cba9c6..000000000 --- a/toybox/VirtualKeyboard/1/assets/images/spaceBarD.taml +++ /dev/null @@ -1,3 +0,0 @@ - \ No newline at end of file diff --git a/toybox/VirtualKeyboard/1/assets/images/spaceBarStates.png b/toybox/VirtualKeyboard/1/assets/images/spaceBarStates.png new file mode 100644 index 0000000000000000000000000000000000000000..cc3a9eaf4418dfcc8a4a954c34bb2eb3abd08da7 GIT binary patch literal 41069 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

    D}C70H~=gG(XgQ?G7#RYGrbj2G|M}=fjE8@de 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 aq#YgQ?GWl9KXaf#kdBt2W|{h}i2nzU;72R~ literal 0 HcmV?d00001 diff --git a/toybox/VirtualKeyboard/1/assets/images/spaceBarStates.taml b/toybox/VirtualKeyboard/1/assets/images/spaceBarStates.taml new file mode 100644 index 000000000..fa1f77555 --- /dev/null +++ b/toybox/VirtualKeyboard/1/assets/images/spaceBarStates.taml @@ -0,0 +1,7 @@ + diff --git a/toybox/VirtualKeyboard/1/gui/keyboardGui.taml b/toybox/VirtualKeyboard/1/gui/keyboardGui.taml index 82fc41f59..dcc678eb9 100644 --- a/toybox/VirtualKeyboard/1/gui/keyboardGui.taml +++ b/toybox/VirtualKeyboard/1/gui/keyboardGui.taml @@ -24,12 +24,13 @@ Active = "1" Image = "VirtualKeyboard:keyboardAlpha"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + command = "VirtualKeyboard.toUpper();"/> - + command = "VirtualKeyboard.toUpperLock();"/> - - + + command = "VirtualKeyboard.toLower();"/> - - - - - diff --git a/toybox/VirtualKeyboard/1/main.cs b/toybox/VirtualKeyboard/1/main.cs index 4761c3a0e..1fd120cce 100644 --- a/toybox/VirtualKeyboard/1/main.cs +++ b/toybox/VirtualKeyboard/1/main.cs @@ -182,42 +182,58 @@ %this.textBox.setText(getSubStr(%this.textBox.getText(), 0, %len - 1)); } -if(!isObject(GuiKeyboardProfile)) new GuiControlProfile (GuiKeyboardProfile) +//----------------------------------------------------------------------------- +// The key caps. +// +// A profile's imageAsset is indexed by control state, so one four frame strip +// carries a whole button: 0 normal, 1 hover, 2 pressed, 3 disabled. That is the +// whole reason these are profiles rather than fields on the buttons - the keys +// share four looks between thirty seven of them. +// +// No colors here. renderUniversalRect only reaches fillColor when there is no +// image to draw, and these buttons carry no text, so the font fields would go +// unread as well. The click comes from soundButtonDown, which GuiButtonCtrl +// plays for us. +//----------------------------------------------------------------------------- + +if(!isObject(GuiKeyboardKeyProfile)) new GuiControlProfile (GuiKeyboardKeyProfile) { tab = false; canKeyFocus = false; - hasBitmapArray = false; mouseOverSelected = false; - // fill color - fillColor = "211 211 211 255"; - fillColorHL = "244 244 244 255"; - fillColorSL = "244 244 244 255"; - fillColorNA = "244 244 244 255"; - - // border color - border = 0; - borderColor = "100 100 100 255"; - borderColorHL = "128 128 128 255"; - borderColorSL = "128 128 128 255"; - borderColorNA = "64 64 64 255"; - - // font - fontType = $platformFontType; - fontSize = $platformFontSize; - - fontColor = "0 0 0"; - fontColorHL = "32 100 100"; - fontColorSL= "10 10 10"; - fontColorNA = "0 0 0"; - - // used by guiTextControl - align = "left"; - returnTab = false; - numbersOnly = false; - cursorColor = "0 0 0 255"; - - // sounds + imageAsset = "VirtualKeyboard:keyStates"; + soundButtonDown = "VirtualKeyboard:keypress"; +}; + +if(!isObject(GuiKeyboardSpaceProfile)) new GuiControlProfile (GuiKeyboardSpaceProfile) +{ + tab = false; + canKeyFocus = false; + mouseOverSelected = false; + + imageAsset = "VirtualKeyboard:spaceBarStates"; + soundButtonDown = "VirtualKeyboard:keypress"; +}; + +if(!isObject(GuiKeyboardCloseProfile)) new GuiControlProfile (GuiKeyboardCloseProfile) +{ + tab = false; + canKeyFocus = false; + mouseOverSelected = false; + + imageAsset = "VirtualKeyboard:closeStates"; + soundButtonDown = "VirtualKeyboard:keypress"; +}; + +// Caps lock is shown by a button that wears the pressed cap in every state, so +// it reads as held down for as long as the lock is on. +if(!isObject(GuiKeyboardLatchedProfile)) new GuiControlProfile (GuiKeyboardLatchedProfile) +{ + tab = false; + canKeyFocus = false; + mouseOverSelected = false; + + imageAsset = "VirtualKeyboard:keyLatched"; soundButtonDown = "VirtualKeyboard:keypress"; - //soundButtonOver = "Sandbox:mouseOver"; }; From 8be9a900be583446e7263399ff8f25ceb9e65672 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Thu, 6 Aug 2026 09:02:10 -0400 Subject: [PATCH 43/55] KeyboardToy: the toy nobody had opened since the controls changed It opened to an empty screen. create() finished by calling reset() on a bare "KeyboardToy", which is not the name of anything - a module's create is handed its definition as %this, the way TruckToy and the rest call it - so reset never ran, the dialog was never pushed, and nothing was drawn. That is the whole of the black screen. Behind it sat three years of quiet rot that only shows once something opens the toy. UserNameTxt was a GuiTextCtrl, which Control Armageddon deleted in 2021; a plain GuiControl carries text now. Two of the profiles the dialogs named went with AppCore's: the image-backed roots asked for GuiToolboxProfile and now wear GuiSpriteProfile, and the entry asked for GuiConsoleTextEditProfile and now wears GuiTextEditProfile. BlueButtonProfile, GuiTextProfile and GuiToolTipProfile are all still there, and are left alone. The dialogs are held on %this and deleted through those handles rather than by global name, guarded, because the Sandbox owns them once they are added and may have taken them down first - which is exactly what the two "Unable to find object" errors at shutdown were. The smoke suite loads the toy and raises the keyboard the way its button does. It is also the first time the state strip keycaps have been rendered by anything, the toy being the only front end VirtualKeyboard has. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01JDKWjuEF5Nuw791mpU7aPF --- tests/smoke/keyboardToy.cs | 90 +++++++++++++++++++ .../KeyboardToy/1/ChangeUsernameDlg.gui.taml | 4 +- toybox/KeyboardToy/1/MainGameDlg.gui.taml | 8 +- toybox/KeyboardToy/1/main.cs | 29 ++++-- 4 files changed, 115 insertions(+), 16 deletions(-) create mode 100644 tests/smoke/keyboardToy.cs diff --git a/tests/smoke/keyboardToy.cs b/tests/smoke/keyboardToy.cs new file mode 100644 index 000000000..0d930fab0 --- /dev/null +++ b/tests/smoke/keyboardToy.cs @@ -0,0 +1,90 @@ +//----------------------------------------------------------------------------- +// The KeyboardToy comes up, and the VirtualKeyboard it exists to show comes up +// with it. +// +// The toy had rotted quietly: it called reset() on a bare "KeyboardToy" name +// that resolves to nothing, so the dialog was never pushed and the toy opened +// to an empty screen. Its label was a GuiTextCtrl, deleted from the engine in +// 2021, and two of the profiles it named went with AppCore's. None of that is +// visible until something loads the toy, which is what this does. +//----------------------------------------------------------------------------- + +setRandomSeed(); +setLogMode(2); +setScriptExecEcho(false); +trace(false); +$Scripts::ignoreDSOs = true; +setCompanyAndProduct("Torque Game Engines", "Torque2D"); +ModuleDatabase.EchoInfo = false; +AssetDatabase.EchoInfo = false; + +ModuleDatabase.scanModules(testRoot("toybox")); +ModuleDatabase.LoadExplicit("AppCore"); + +function smokeCheck(%label, %condition) +{ + echo(%condition ? ("SMOKE PASS: " @ %label) : ("SMOKE FAIL: " @ %label)); +} + +createPath(testRoot("shots/")); +schedule(5000, 0, "kbStepLoad"); + +function kbStepLoad() +{ + %toy = ModuleDatabase.findModule("KeyboardToy", 1); + smokeCheck("the KeyboardToy module is there", isObject(%toy)); + + loadToy(%toy); + schedule(1000, 0, "kbStepDialog"); +} + +function kbStepDialog() +{ + // create() makes both dialogs and reset() pushes the first. Before the fix + // reset() was never reached, so this is the check that matters. + smokeCheck("the toy made its dialog", isObject(MainGameDlg)); + smokeCheck("the toy made its second dialog", isObject(ChangeUsernameDlg)); + smokeCheck("the dialog was pushed to the canvas", isObject(MainGameDlg) && MainGameDlg.isAwake()); + + // The label was a GuiTextCtrl; a plain GuiControl carries text now. + smokeCheck("the label survived losing GuiTextCtrl", isObject(UserNameTxt)); + smokeCheck("the label kept its text", UserNameTxt.getText() $= "NONAME"); + + // Every profile the two dialogs name must actually exist, or the controls + // silently fall back and the toy looks wrong rather than failing. + smokeCheck("the dialog profile exists", isObject(GuiSpriteProfile)); + smokeCheck("the label profile exists", isObject(GuiTextProfile)); + smokeCheck("the entry profile exists", isObject(GuiTextEditProfile)); + smokeCheck("the button profile exists", isObject(BlueButtonProfile)); + + schedule(500, 0, "kbStepKeyboard"); +} + +function kbStepKeyboard() +{ + // What the toy is for: the keyboard, raised the way its button raises it. + VirtualKeyboard.push(ChangeUsernameDlg, ChangeUsernameEntry); + schedule(1000, 0, "kbStepKeys"); +} + +function kbStepKeys() +{ + smokeCheck("the keyboard came up", isObject(KeyboardGui)); + smokeCheck("its keys are in the tree", isObject(KeyboardSet)); + + // The four state strips that replaced GuiImageButtonCtrl. A profile's + // imageAsset is indexed by control state, so one strip is a whole button. + smokeCheck("the key profile exists", isObject(GuiKeyboardKeyProfile)); + smokeCheck("the space bar profile exists", isObject(GuiKeyboardSpaceProfile)); + smokeCheck("the close profile exists", isObject(GuiKeyboardCloseProfile)); + smokeCheck("the caps lock profile exists", isObject(GuiKeyboardLatchedProfile)); + + screenShot(testRoot("shots/keyboardToy.png"), "PNG"); + schedule(1000, 0, "kbSmokeDone"); +} + +function kbSmokeDone() +{ + echo("SMOKE DONE"); + quit(); +} diff --git a/toybox/KeyboardToy/1/ChangeUsernameDlg.gui.taml b/toybox/KeyboardToy/1/ChangeUsernameDlg.gui.taml index c4e911521..323d79e85 100644 --- a/toybox/KeyboardToy/1/ChangeUsernameDlg.gui.taml +++ b/toybox/KeyboardToy/1/ChangeUsernameDlg.gui.taml @@ -1,6 +1,6 @@ - + toolTipProfile="GuiToolTipProfile" /> Date: Thu, 6 Aug 2026 10:05:56 -0400 Subject: [PATCH 44/55] KeyboardToy: the dialogs as the Gui Editor laid them out Peter's pass through the Gui Editor: ChangeUsernameDlg rebuilt with the entry and a container holding Cancel and OK, both dialogs filling the canvas rather than anchoring two edges, and the sizing named for the edge it holds. MainGameDlg had only been resized, so it still carried the two dead references that keep the toy from drawing: the root asked for GuiToolboxProfile, which went with AppCore's profiles, and the label was still a GuiTextCtrl. The root wears GuiDefaultProfile now, to match the other dialog, and the label is a GuiControl carrying its own text. Everything else in the file is as it was laid out. Note for later: the entry renders invisible while it has focus, and push() always gives it focus. GuiTextEditProfile names fillColor, fillColorHL and fillColorNA but no fillColorSL, so the selected state falls back to GuiDefaultProfile's "0 0 0 0". That is the Sandbox's shared profile, not this toy's, so it is left alone here. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01JDKWjuEF5Nuw791mpU7aPF --- .../KeyboardToy/1/ChangeUsernameDlg.gui.taml | 138 ++++++++++-------- toybox/KeyboardToy/1/MainGameDlg.gui.taml | 6 +- 2 files changed, 80 insertions(+), 64 deletions(-) diff --git a/toybox/KeyboardToy/1/ChangeUsernameDlg.gui.taml b/toybox/KeyboardToy/1/ChangeUsernameDlg.gui.taml index 323d79e85..d8e8539b2 100644 --- a/toybox/KeyboardToy/1/ChangeUsernameDlg.gui.taml +++ b/toybox/KeyboardToy/1/ChangeUsernameDlg.gui.taml @@ -1,66 +1,82 @@ - + Active="true" + align="default" + vAlign="default" + Image="@asset=Sandbox:blueGradient" + constrainProportions="false" + HelpTag="0"> - - - - - + Name="ChangeUsernameEntry" + canSaveDynamicFields="false" + isContainer="false" + Profile="GuiTextEditProfile" + HorizSizing="center" + VertSizing="anchorTop" + Position="389 320" + Extent="430 50" + MinExtent="8 8" + AltCommand="VirtualKeyboard.pop();UserNameTxt.setText(ChangeUsernameEntry.getText());" + Active="true" + Text="" + align="default" + vAlign="default" + fontSizeAdjust="3" + sinkAllKeyEvents="true" + MaxLength="255" /> + + + + diff --git a/toybox/KeyboardToy/1/MainGameDlg.gui.taml b/toybox/KeyboardToy/1/MainGameDlg.gui.taml index 17c9ed717..fdedec05b 100644 --- a/toybox/KeyboardToy/1/MainGameDlg.gui.taml +++ b/toybox/KeyboardToy/1/MainGameDlg.gui.taml @@ -1,8 +1,8 @@ Date: Thu, 6 Aug 2026 11:08:58 -0400 Subject: [PATCH 45/55] GuiButtonCtrl: a caption the author cleared stays cleared The Gui Editor saved the keyboard's keys without a Text attribute, which was right - their captions were blank, and SimObject::writeField drops every empty value. Reading them back put "Button" on all thirty seven of them, because that is what the constructor seeded and an absent attribute is exactly what a constructor default fills. So a default caption cannot survive the round trip it is part of: blank is written as absent, absent reads as the default, and the author's blank is gone. GuiButtonCtrl carries no caption of its own now. GuiCheckBoxCtrl and GuiRadioCtrl take their text from it, so a check box that read "Button" is fixed along with it. The placeholder is still worth having, so it moved to the moment a control is actually placed. GuiEditorControlTile::makePayload captions what it makes from the palette's own label, and it is the one point both the drag and the click go through, so the two gestures cannot drift apart. A dropped check box reads "Check Box" now instead of the "Button" it used to inherit. GuiDropDownCtrl keeps its "none". It draws mText only while nothing is selected, so that is an empty state rather than a caption, and nothing about it is a caption to be replaced. The checks are a smoke suite rather than a C++ unit test because building a button assigns it a profile, a profile loads its font, and loading a font registers a texture - which asserts with no GL context. Two traps are worth knowing about and are written down in the suite: getText() on a control that was never made answers the empty string, so every check tests for the object first, and GuiDropDownCtrl binds its own getText that answers the selected item and never reads mText. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01JDKWjuEF5Nuw791mpU7aPF --- .../GuiEditor/scripts/GuiEditorControlTile.cs | 28 +++++ engine/source/gui/buttons/guiButtonCtrl.cc | 7 +- tests/smoke/buttonText.cs | 108 ++++++++++++++++++ 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 tests/smoke/buttonText.cs diff --git a/editor/GuiEditor/scripts/GuiEditorControlTile.cs b/editor/GuiEditor/scripts/GuiEditorControlTile.cs index 581ad51a0..7f9fffe29 100644 --- a/editor/GuiEditor/scripts/GuiEditorControlTile.cs +++ b/editor/GuiEditor/scripts/GuiEditorControlTile.cs @@ -226,9 +226,37 @@ %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. //----------------------------------------------------------------------------- diff --git a/engine/source/gui/buttons/guiButtonCtrl.cc b/engine/source/gui/buttons/guiButtonCtrl.cc index 2fd9501fe..d4c051508 100755 --- a/engine/source/gui/buttons/guiButtonCtrl.cc +++ b/engine/source/gui/buttons/guiButtonCtrl.cc @@ -38,7 +38,12 @@ GuiButtonCtrl::GuiButtonCtrl() mMouseOver = false; mActive = true; mBounds.extent.set(140, 30); - mText = StringTable->insert("Button"); + // No caption. A button whose text is deliberately blank - an image button + // wearing its whole face in the profile's imageAsset, say - must be able to + // say so: SimObject::writeField drops every empty value, so a blank caption + // is written as an absent one, and a default here would stand back up on + // read. The Gui Editor captions the buttons it places instead. + mText = StringTable->EmptyString; mTextID = StringTable->EmptyString; mProfile = NULL; mIsContainer = false; diff --git a/tests/smoke/buttonText.cs b/tests/smoke/buttonText.cs new file mode 100644 index 000000000..59e4aea64 --- /dev/null +++ b/tests/smoke/buttonText.cs @@ -0,0 +1,108 @@ +//----------------------------------------------------------------------------- +// A caption the author cleared has to come back cleared. +// +// SimObject::writeField drops every empty value, so a blank caption is written +// as an absent one. Anything the constructor seeds therefore stands back up on +// read and silently replaces the author's blank - which is what put "Button" on +// all thirty seven keys of the VirtualKeyboard the first time it went through +// the Gui Editor. A button carries no caption of its own now; the Gui Editor +// captions the ones it places. +// +// This is a smoke suite rather than a C++ unit test because building a button +// assigns it a profile, and a profile loads its font, which registers a texture +// - and a unit test has no GL context, so that asserts. +//----------------------------------------------------------------------------- + +setRandomSeed(); +setLogMode(2); +setScriptExecEcho(false); +trace(false); +$Scripts::ignoreDSOs = true; +setCompanyAndProduct("Torque Game Engines", "Torque2D"); +ModuleDatabase.EchoInfo = false; +AssetDatabase.EchoInfo = false; + +ModuleDatabase.scanModules(testRoot("toybox")); +ModuleDatabase.LoadExplicit("AppCore"); + +function smokeCheck(%label, %condition) +{ + echo(%condition ? ("SMOKE PASS: " @ %label) : ("SMOKE FAIL: " @ %label)); +} + +createPath(testRoot("shots/")); +schedule(4000, 0, "btDefaults"); + +function btDefaults() +{ + // What a freshly built control arrives with. Every one of these is checked + // for existence first: getText() on something that was never made answers + // the empty string too, so "has no caption" would pass on a control that + // does not exist. + %button = new GuiButtonCtrl(); + smokeCheck("a button was made", isObject(%button)); + smokeCheck("a new button has no caption (" @ %button.getText() @ ")", + %button.getText() $= ""); + %button.delete(); + + // A check box and a radio take their text from GuiButtonCtrl, so the seeded + // caption used to reach them too -- a check box read "Button". + %check = new GuiCheckBoxCtrl(); + smokeCheck("a check box was made", isObject(%check)); + smokeCheck("a new check box has no caption (" @ %check.getText() @ ")", + %check.getText() $= ""); + %check.delete(); + + %radio = new GuiRadioCtrl(); + smokeCheck("a radio was made", isObject(%radio)); + smokeCheck("a new radio has no caption (" @ %radio.getText() @ ")", + %radio.getText() $= ""); + %radio.delete(); + + // The drop down is deliberately left alone: it draws its text only while + // nothing is selected, so "none" is an empty state, not a caption. + // + // Read through the text FIELD, not getText(). GuiDropDownCtrl binds its own + // getText, which answers the selected item's text and an empty string when + // nothing is selected -- it never reads mText at all. Asserting on getText + // here would report a placeholder that is perfectly intact as missing. + %drop = new GuiDropDownCtrl(); + smokeCheck("a drop down was made", isObject(%drop)); + smokeCheck("a new drop down still reads none (" @ %drop.text @ ")", + %drop.text $= "none"); + %drop.delete(); + + schedule(200, 0, "btRoundTrip"); +} + +function btRoundTrip() +{ + %path = testRoot("shots/buttonTextRoundTrip.taml"); + + // The bug itself: blank is written as absent, and absent used to read back + // as "Button". + %blank = new GuiButtonCtrl(); + %blank.setText(""); + TamlWrite(%blank, %path); + %blank.delete(); + + %loaded = TamlRead(%path); + smokeCheck("a blank caption survives the round trip (" @ %loaded.getText() @ ")", + isObject(%loaded) && %loaded.getText() $= ""); + %loaded.delete(); + + // And the ordinary case still works. + %captioned = new GuiButtonCtrl(); + %captioned.setText("Change username"); + TamlWrite(%captioned, %path); + %captioned.delete(); + + %reloaded = TamlRead(%path); + smokeCheck("a set caption survives the round trip (" @ %reloaded.getText() @ ")", + isObject(%reloaded) && %reloaded.getText() $= "Change username"); + %reloaded.delete(); + + fileDelete(%path); + echo("SMOKE DONE"); + quit(); +} From 3447653693cba330397557ef3206dd15a966c15e Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Thu, 6 Aug 2026 12:18:39 -0400 Subject: [PATCH 46/55] GuiTextEditProfile: a field with focus is still a field A text edit takes focus the moment it is shown - VirtualKeyboard::push makes its target the first responder - and a focused control draws in the selected state. The profile named fillColor, fillColorHL and fillColorNA but no fillColorSL, so the selected state fell through to GuiDefaultProfile's "0 0 0 0" and a focused field rendered as nothing at all: a caret hanging in mid air over whatever was behind it. It takes the same color as the highlight it sits beside, so the field looks the same whether the pointer is over it or the caret is in it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01JDKWjuEF5Nuw791mpU7aPF --- toybox/Sandbox/1/gui/guiProfiles.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/toybox/Sandbox/1/gui/guiProfiles.cs b/toybox/Sandbox/1/gui/guiProfiles.cs index bb1c6a7bc..bdebd675d 100644 --- a/toybox/Sandbox/1/gui/guiProfiles.cs +++ b/toybox/Sandbox/1/gui/guiProfiles.cs @@ -565,6 +565,7 @@ function SafeCreateNamedObject(%name, %object) { fillColor = "232 240 248 255"; fillColorHL = "242 250 255 255"; + fillColorSL = "242 250 255 255"; fillColorNA = "127 127 127 52"; fillColorTextSL = "251 170 0 255"; fontColor = "27 59 95 255"; From d021c7487d62f43d8cb16137df8e1646334ca6c1 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Thu, 6 Aug 2026 12:19:01 -0400 Subject: [PATCH 47/55] KeyboardToy: the dialogs and the keyboard, as the Gui Editor laid them out MainGameDlg rebuilt the way ChangeUsernameDlg already was: the name and both buttons gathered into one centred container, so the group holds the middle of the canvas rather than each control anchoring separately, and sized to be read - "Change User Name" and Exit at 160 by 50, the name above them at twice the profile's font size. keyboardGui.taml is now keyboardGui.gui.taml. The Gui Editor lists the files it can open by that suffix, so the keyboard could not be opened in the editor that made the rest of this; main.cs loads the new name and nothing else referred to the old one. The keys come back from the editor with no Text attribute at all, which is exactly what they mean - their faces are in the profile's imageAsset and the caption is deliberately blank. That is only safe to save now: a blank caption is written as an absent one, and until GuiButtonCtrl stopped seeding "Button" an absent caption read back as a captioned key. All thirty seven of them. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01JDKWjuEF5Nuw791mpU7aPF --- toybox/KeyboardToy/1/MainGameDlg.gui.taml | 132 ++-- .../1/gui/keyboardGui.gui.taml | 664 +++++++++++++++++ toybox/VirtualKeyboard/1/gui/keyboardGui.taml | 674 ------------------ toybox/VirtualKeyboard/1/main.cs | 2 +- 4 files changed, 737 insertions(+), 735 deletions(-) create mode 100644 toybox/VirtualKeyboard/1/gui/keyboardGui.gui.taml delete mode 100644 toybox/VirtualKeyboard/1/gui/keyboardGui.taml diff --git a/toybox/KeyboardToy/1/MainGameDlg.gui.taml b/toybox/KeyboardToy/1/MainGameDlg.gui.taml index fdedec05b..93e6fb8db 100644 --- a/toybox/KeyboardToy/1/MainGameDlg.gui.taml +++ b/toybox/KeyboardToy/1/MainGameDlg.gui.taml @@ -4,65 +4,77 @@ HorizSizing="fill" VertSizing="fill" Position="0 0" - Extent="1024 768" + Extent="1208 951" MinExtent="8 8" - Visible="1" - HelpTag="0" - Image="@asset=Sandbox:blueGradient"> - - - - - - - + Active="true" + align="default" + vAlign="default" + Image="@asset=Sandbox:blueGradient" + constrainProportions="false" + HelpTag="0"> + + + + + diff --git a/toybox/VirtualKeyboard/1/gui/keyboardGui.gui.taml b/toybox/VirtualKeyboard/1/gui/keyboardGui.gui.taml new file mode 100644 index 000000000..74a319211 --- /dev/null +++ b/toybox/VirtualKeyboard/1/gui/keyboardGui.gui.taml @@ -0,0 +1,664 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/toybox/VirtualKeyboard/1/gui/keyboardGui.taml b/toybox/VirtualKeyboard/1/gui/keyboardGui.taml deleted file mode 100644 index dcc678eb9..000000000 --- a/toybox/VirtualKeyboard/1/gui/keyboardGui.taml +++ /dev/null @@ -1,674 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/toybox/VirtualKeyboard/1/main.cs b/toybox/VirtualKeyboard/1/main.cs index 1fd120cce..847394da1 100644 --- a/toybox/VirtualKeyboard/1/main.cs +++ b/toybox/VirtualKeyboard/1/main.cs @@ -18,7 +18,7 @@ } function VirtualKeyboard::push(%this, %targetGui, %textBox, %showClose) { - Sandbox.add( TamlRead("./gui/keyboardGui.taml") ); + Sandbox.add( TamlRead("./gui/keyboardGui.gui.taml") ); %textBox.setText(""); %this.targetGui = %targetGui; From c6f66126b874e14049ee9e418033c601ff4199f3 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Thu, 6 Aug 2026 19:45:13 -0400 Subject: [PATCH 48/55] Remove the design docs They are a working artefact of how a change got planned, not something the repository needs to carry. What the code does and why is already written where it is done, which is the only copy that cannot go stale. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01JDKWjuEF5Nuw791mpU7aPF --- .../2026-07-19-gui-profile-theme-design.md | 96 ---------- .../2026-07-29-gui-control-icon-set-design.md | 169 ------------------ 2 files changed, 265 deletions(-) delete mode 100644 docs/superpowers/specs/2026-07-19-gui-profile-theme-design.md delete mode 100644 docs/superpowers/specs/2026-07-29-gui-control-icon-set-design.md diff --git a/docs/superpowers/specs/2026-07-19-gui-profile-theme-design.md b/docs/superpowers/specs/2026-07-19-gui-profile-theme-design.md deleted file mode 100644 index 16c758d79..000000000 --- a/docs/superpowers/specs/2026-07-19-gui-profile-theme-design.md +++ /dev/null @@ -1,96 +0,0 @@ -# GuiProfileTheme — Design - -Date: 2026-07-19 -Branch: `Gui-Profile-Editor` -Status: approved (Peter, 2026-07-19) - -## Context - -Torque2D's GUI skinning rests on `GuiControlProfile`, originally designed for instant reskinning by profile swap. In practice profiles must be fine-tuned per control, many controls take several profiles (scroll = track/thumb/arrow; window = content + 3 buttons; menu = 4+ parts), and the `mCategory` field meant to express "what control is this profile for" is never read anywhere in the engine — it's a write-only persist field (`guiTypes.cc:437`, declared `guiTypes.h:253`). Missing profiles fall back by name convention to `GuiDefaultProfile` and, in release builds, null-deref (`guiControl.cc:1137-1192`, `AssertFatal` compiled out). - -Meanwhile, real theming exists only in TorqueScript, three times over: editor `BaseTheme` (~67 profiles, 2,310 lines, `editor/EditorCore/Themes/BaseTheme/BaseTheme.cs`), `AppCore::createGuiProfiles` (`library/AppCore/gui/guiProfiles.cs`), and legacy Sandbox globals. All share one shape: a small palette + fonts + two helpers (`adjustValue` = HSV-brightness shift, `setAlpha`), deriving every profile's state colors (base = palette color, HL = adjustValue(+10), SL = accent, NA = setAlpha(~80–150)). - -`GuiProfileTheme` formalizes that pattern as a C++ object — theme-wide values, auto-created profiles per category, per-field overrides, TAML persistence. It is the foundation for a future full-screen Profile Editor dialog in the GUI Editor; the theme is designed to be driven by that editor, not hand-authored. - -## Decisions (Peter, 2026-07-19) - -1. **Category list**: engine-defined static table in C++ (not script/data-driven). -2. **Propagation**: stamp-on-change — profile fields stay plain members; theme writes derived defaults into member profiles when theme values change, skipping overridden fields. No live fall-through reads, no rendering changes. -3. **Scope**: pure container — no changes to `GuiControl` or the profile-lookup/fallback chain. Lookup integration (active theme in `onWake` fallback) is a future branch. -4. **Theme value shape**: BaseTheme shape + destructive color — 3 semantic fonts + fontDirectory + fontSize, 6 semantic colors, borderSize. -5. **Architecture**: code-table theme — per-category C++ stamp functions porting the AppCore recipes. (Expression-driven derivation deliberately rejected for scope; can be layered later.) -6. **Category set**: full ~36-category table in v1, not a starter subset. - -## Design - -### Object model & ownership - -- **`GuiProfileTheme : SimObject`** — new files `engine/source/gui/guiProfileTheme.h`, `.cc`, `guiProfileTheme_ScriptBinding.h`. `DECLARE_CONOBJECT`/`IMPLEMENT_CONOBJECT`, `initPersistFields()` calling Parent first, registers into `Sim::getGuiDataGroup()` in `onAdd` (mirror `guiTypes.cc:445`). -- **Theme owns members**: lists of member `GuiControlProfile*` and `GuiBorderProfile*`. `onAdd` creates one profile per category-table entry (skipping any already present from TAML read), `onRemove` deletes all members. Ownership flows one way. -- **Members point back**: `GuiControlProfile` and `GuiBorderProfile` gain non-owning `GuiProfileTheme* mTheme` (never serialized; reconstructed at load). `mTheme == NULL` ⇒ standalone profile, behavior identical to today. **No changes to `GuiControl`.** -- **Delete safety both ways**: theme `deleteNotify()`s each member and overrides `onDeleteNotify` to drop the entry. Deleted *default* ⇒ recreated at next stamp (a theme is always complete). Deleted *extra* ⇒ removed. Theme deleted ⇒ members are deleted by `onRemove`; a member that outlives has its `mTheme` nulled via the member's own `onDeleteNotify`. - -### Category table (the engine-defined canon) - -Static table in `guiProfileTheme.cc`: `{ categoryName, profileNameSuffix, stampFunc }`. Theme sets `mCategory` on every member — making the field load-bearing for the first time. Auto-created member names: **``** (e.g. `DarkThemeButtonProfile`); extras get user names but keep the category (default extra name ``). Renaming a theme does not rename existing members (documented v1 limitation). - -v1 categories (36) — union of profile slots engine controls consume (GUI Guide slot map + AppCore inventory): - -> Default, Empty (transparent), Tooltip, Panel, Button, CheckBox, Radio, Label, TextEdit, Scroll, ScrollTrack, ScrollThumb, ScrollArrow, TabBook, Tab, TabPage, ListBox, DropDown, DropDownItem, Window, WindowContent, WindowButton, WindowCloseButton (destructive color), MenuBar, Menu, MenuItem, MenuContent, Overlay (menu/popup click-catcher), Progress, TreeView, FrameSet, FrameSetDropButton, ColorPicker, ColorSelector, ColorPopup, DragAndDrop - -A parallel smaller **border table** works identically for theme-owned `GuiBorderProfile`s (Default, Bright, Dark, button-bevel sides, etc., from AppCore's border profiles). `GuiBorderProfile` gains a `category` persist field for symmetry (it has none today). Control-profile stamp functions wire borders: `borderDefault` by object pointer, the four side-border fields by **name** (they are lazily name-resolved strings already — `guiTypes.cc:455-613`). - -Recipes: port from `library/AppCore/gui/guiProfiles.cs` (game-facing baseline), mapping its `color1..6` onto the semantic roles. Fixed bevel colors stay literal (`255 255 255 80` / `0 0 0 80` — deliberately hue-independent). Stamp functions set **all** persist fields of a profile (colors, fonts, alignment, geometry via borders, behavior flags); `bitmap`/`imageAsset` are left empty by stamps (user-overridable only). - -### Theme-wide values (persist fields) - -`fontBody`, `fontTitle`, `fontCode` (font type names), `fontDirectory`, `fontSize`; -`colorBackground`, `colorPanel`, `colorText`, `colorAccent`, `colorHighlight`, `colorWarning` (ColorI); -`borderSize` (S32). - -(Amended 2026-07-19 with Peter: the originally-planned `colorTextSubtle` was replaced by `colorHighlight` — AppCore's palette has two accents (blue interaction + yellow flavor) and no subtle-text color, so the 6 roles now map 1:1 onto AppCore's six palette entries with no dead fields.) - -Helpers as C++ statics on the class, exposed to script: `adjustValue(color, percent)` — HSV-value shift preserving hue/alpha, **fixing** BaseTheme's clamp bug (`mClamp(newValue, 0, 100)` should clamp the value fraction to 0..1, `BaseTheme.cs:2280-2309`) — and `setAlpha(color, alpha)`. - -### Stamping & override tracking (core mechanism) - -- `GuiProfileTheme::onStaticModified` (virtual on `SimObject`, `simObject.h:486`, fired from `setDataField` at `simObject.cc:558` — so script, editor inspector, and all three TAML readers hit it) ⇒ re-stamp all members synchronously (36 profiles × ~30 fields is trivial). -- **Stamp functions write raw member variables directly** (not `setDataField`) — so stamping never fires `onStaticModified`, needs no guard flag, and never marks overrides. -- Members override `onStaticModified`: when `mTheme != NULL`, any external field write adds the field name (`StringTableEntry`) to the member's override set; stamping skips fields in the set. Writes to `category` are ignored/not overridable (theme-managed). -- Override set: small `Vector` (pointer-compare, linear scan) inside a shared helper struct (e.g. `GuiThemeMembership { GuiProfileTheme* theme; Vector overrides; }`) embedded in both member classes; thin per-class glue, no multiple inheritance. -- `resetField`/`resetProfile` clear override entries and re-stamp. -- Note: `GuiControlProfile`'s constructor copy-from-`GuiDefaultProfile` (`guiTypes.cc:348-382`) runs before stamping and is simply overwritten — harmless, leave as-is. - -### TAML persistence - -- One theme file = theme values (ordinary persist fields) + all members as **TAML custom nodes** via `TamlCallbacks::onTamlCustomWrite/Read` (pattern: `engine/source/2d/assets/ParticleAsset.*`, also `SpriteBatch`, `guiFrameSetCtrl`; API in `engine/source/persistence/taml/tamlCustom.h`). -- Each member node writes: object name, category, and **only overridden fields** — enforced by overriding virtual `SimObject::writeField` (`simObject.h:673`; TAML consults it at `taml.cc:697,777`) to filter non-overridden fields when `mTheme != NULL` (always allow `name`/`category`). Standalone profile serialization unchanged. -- On read: members created from nodes and attached to the theme; fields applied via `setDataField` automatically re-mark themselves overridden (readers use `setPrefixedDataField` — `tamlXmlReader.cc:294`, `tamlBinaryReader.cc:241`, `tamlJSONReader.cc:258`). `onAdd` then creates any missing defaults and stamps everything. - -### Script API (`guiProfileTheme_ScriptBinding.h`, ConsoleMethodWithDocs style) - -`getProfile(category)`, `getProfiles(category)`, `createProfile(category [,name])`, `removeProfile(profile)` (extras only), `resetField(profile, field)`, `resetProfile(profile)`, `isFieldOverridden(profile, field)`, `restamp()`, `adjustValue(color, percent)`, `setAlpha(color, alpha)`; plus `getCategoryNames()` (static/console function) so the future editor enumerates the table. - -### Adjacent fix (small, justified) - -`GuiControlProfile` registers `deleteNotify` on its side-border profiles (`guiTypes.cc:479-613`) but never overrides `onDeleteNotify`, so deleting a border leaves dangling pointers (base is a no-op, `simObject.cc:904-906`). Since the theme now deletes/recreates borders routinely, this latent bug becomes live. Add `GuiControlProfile::onDeleteNotify` nulling the matching `mBorderDefault`/`mBorderLeft/Right/Top/Bottom` pointers. - -## Out of scope (explicitly) - -- The Profile Editor dialog (later task on this branch). -- Profile-lookup/fallback integration (`GuiControl::onWake` consulting an active theme) and the release-build null-profile crash fix — future branch. -- Migrating editor `BaseTheme`/`ThemeManager` or AppCore script themes to GuiProfileTheme. -- Theme-owned image assets / bitmap recipes; expression-driven derivation. - -## Testing - -GoogleTest in `engine/source/testing/tests/guiProfileThemeTests.cc`, run via `runAllUnitTests()`: - -- theme creates one profile per category with `mCategory` set; -- theme-value change propagates to non-overridden member fields; -- an overridden field survives re-stamp; reset restores derivation; -- TAML round-trip preserves values + override sets and writes only overridden fields; -- delete-safety in both directions; -- `adjustValue` clamp correctness (including the fixed over-brighten rail). - -Manual verification: editor boots and skins normally (standalone profiles unaffected); interactive console smoke (create theme, inspect members, edit theme color, override, reset, TAML round-trip, deletion behavior). diff --git a/docs/superpowers/specs/2026-07-29-gui-control-icon-set-design.md b/docs/superpowers/specs/2026-07-29-gui-control-icon-set-design.md deleted file mode 100644 index acb9e3d09..000000000 --- a/docs/superpowers/specs/2026-07-29-gui-control-icon-set-design.md +++ /dev/null @@ -1,169 +0,0 @@ -# Gui Control Icon Set — Design - -Date: 2026-07-29 -Branch: `gui-editor-improvements` -Status: approved (Peter, 2026-07-29) - -## Context - -The Gui Editor's control palette (`editor/GuiEditor/scripts/GuiEditorControlListWindow.cs`) is a plain-text `GuiListBoxCtrl` fed by `enumerateConsoleClasses("GuiControl")` minus the plumbing — `GuiTextEditSliderCtrl` above `GuiTreeViewCtrl` and below `GuiTextEditCtrl`, telling a person nothing about what any of them looks like. The intent is a visual palette: a grid of 128px tiles, or rows of 64px tiles with names, user's choice. - -This spec covers **the art and its pipeline**. The palette rewrite that consumes it is a separate follow-on, so the art can be reviewed on its own before UI work depends on it. - -The set is modelled on the purchased **Mono Icon Set** (`C:\Users\Peter\Documents\Circuit Hive\T2D Rocket Edition\Mono Icon Set`), which is what every editor icon already uses. New art lives in a sibling folder so the bought art stays untouched — the separation `Mono Icon Set/derived/` already keeps. - -## Decisions (Peter, 2026-07-29) - -1. **Two palette modes** — a grid of 128px tiles (icon only), or rows of 64px tiles with names. In grid mode the icon carries the whole load. -2. **Drawing language: miniature of the control**, not metaphor. A slider looks like a slider. Metaphor only where a control has no appearance (`GuiInputCtrl`). -3. **Scope: palette only (128 + 64)**, but every drawing constrained so the Explorer tree could adopt it later without a redraw. -4. **`GuiControl` gets four tiles**, one per profile category — Empty, Panel, Label, Overlay — so the commonest control in the editor drops pre-categorised instead of guessed at. -5. **Output lands in `editor/GuiEditor/`**, the only consumer. -6. **A generated entry table**, not `$ControlIcon::` constants, because an entry key is not always a class name. -7. **A fallback icon, not a build break**, for a class with no icon defined. - -## Design - -### The drawing language - -Measured off the Mono art rather than guessed at (`app_window_icon&48.png` studied at 4×): - -- Solid white, negative space knocked **out** of the fill rather than stroked. A frame is a filled rect with a smaller rect removed. -- Ink ramp `255` at the top of the tile falling linearly to `218` at the bottom — the same ramp `derive_valign.py` fits by least squares (≈ −0.84/row at 48px). Held as endpoints so it is size-independent, and applied from one function so 31 icons cannot drift apart tonally. -- Corner radius **6 units**, one constant, for the same reason. -- Art **bleeds to the tile edge**; Mono's padding is near zero. Breathing room is the tile control's job. -- **White only.** `EditorIconButton` (`editor/EditorCore/EditorIconButton.cs`) tints its `GuiSpriteCtrl` with `ImageColor` from the theme font colour and fades that on hover, press and disable. The art is a tint mask; the ramp survives underneath as shading. - -### The unit grid - -Every icon is placed in a **64-unit square**. Primary structure — frames, bars, dividers, major splits — snaps to multiples of **4 units**, which is a whole pixel at 16, 32, 64 and 128 alike. Secondary detail may use multiples of 2. - -The honest limit: the rule keeps structure *aligned* at 16px, it does not promise *legibility* there. `GuiTreeViewCtrl`, `GuiMenuBarCtrl`, `GuiFrameSetCtrl` and `SceneWindow` carry detail that will merge at 16 and would want simplified variants if the tree ever adopts the set. - -### Rendering - -Each size is drawn at **8× supersample** and downsampled with Lanczos. Corners get real antialiasing; straight edges cost nothing, because primary geometry lands on exact pixel boundaries at every size. - -### The 31 icons - -Frame index is display order in an 8×4 sheet — 30 used, 2 spare. Grouped by what a person is looking for, not alphabetically. - -| # | key | icon | -|---|---|---| -| 0 | `unknown` | Rounded frame with a **?** — the fallback | -| 1 | `GuiControl:Empty` | Four corner brackets — bounds, nothing painted | -| 2 | `GuiControl:Panel` | Border band, inset cut, filled space inside | -| 3 | `GuiControl:Label` | Two unframed text bars, ragged right | -| 4 | `GuiControl:Overlay` | Diagonal-hatched sheet with a dialog floating on it | -| 5 | `GuiButtonCtrl` | Rounded rect, centred label bar knocked out | -| 6 | `GuiCheckBoxCtrl` | Box with a check, label bars right | -| 7 | `GuiRadioCtrl` | Ring with a dot, label bars right | -| 8 | `GuiDropDownCtrl` | Field, divider, ▼ chevron | -| 9 | `GuiColorPopupCtrl` | 3×2 swatch block + the same chevron | -| 10 | `GuiTextEditCtrl` | Field, text bar, I-beam caret | -| 11 | `GuiTextEditSliderCtrl` | The same field with ▲▼ spinner | -| 12 | `GuiSliderCtrl` | Thin track, thumb, tick marks | -| 13 | `GuiProgressCtrl` | Pill filled from the left | -| 14 | `GuiColorPickerCtrl` | Blend field with a selector ring, banded hue strip | -| 15 | `GuiSpriteCtrl` | Closed frame, peak, sun — a photo | -| 16 | `SceneWindow` | Heavier frame, ground line, ball and crate — a game view | -| 17 | `GuiListBoxCtrl` | Framed rows, one inverted | -| 18 | `GuiTreeViewCtrl` | Spine, elbows, disclosure triangle | -| 19 | `GuiMenuBarCtrl` | Title bar with a menu dropped open | -| 20 | `GuiChainCtrl` | Three equal unframed bars, equal gaps | -| 21 | `GuiGridCtrl` | 3×3 cells with gutters | -| 22 | `GuiScrollCtrl` | Content with a track and thumb | -| 23 | `GuiFrameSetCtrl` | Panes split by thick dividers | -| 24 | `GuiPanelCtrl` | Captioned header, chevron right, body | -| 25 | `GuiExpandCtrl` | Bare header, centred double chevron, body with content | -| 26 | `GuiTabBookCtrl` | Three tabs, first merged into the page | -| 27 | `GuiTabPageCtrl` | One tab and its page, content drawn | -| 28 | `GuiWindowCtrl` | Title bar with three buttons, body | -| 29 | `GuiInputCtrl` | Keycap with a pointer over it — the one metaphor | - -### The eight pairs that must stay apart - -Each is a review checkpoint, and `review.py pairs` renders them side by side: - -| pair | differentiator | -|---|---| -| Sprite / ImageButton | button margin around the picture | -| Sprite / SceneWindow | photo furniture vs. physics furniture | -| Empty / SceneWindow | open brackets vs. closed frame | -| ColorPicker / ColorPopup | chevron only on the popup | -| Panel / Expand | caption present or absent | -| TabBook / TabPage | three tabs or one | -| Slider / Progress | thin track vs. pill | -| Label / Chain | ragged lengths vs. equal blocks | - -### Files - -Authoring, in `T2D Rocket Edition/Control Icon Set/` (not a git repo — it is where the Mono set lives): - -| | | -|---|---| -| `icons.py` | primitives, the 64-unit space, the renderer, the ink ramp, all 31 drawings, and `ICONS` — the ordered palette table | -| `build_control_icon_sheets.py` | renders, asserts, packs, and generates | -| `review.py` | contact sheets: `all`, `probe`, `pairs` | - -**Run with `py`, not `python`** — 3.11 has PIL 12.3.0, the 3.10 first on PATH does not. - -Generated into the repo: - -``` -editor/GuiEditor/images/controlIcons128.png 8×4, 1024×512 -editor/GuiEditor/images/controlIcons128.asset.taml -editor/GuiEditor/images/controlIcons64.png 8×4, 512×256 -editor/GuiEditor/images/controlIcons64.asset.taml -editor/GuiEditor/scripts/GuiEditorControlIcons.cs -``` - -8×4 rather than 6×6: 31 icons need more than 30 cells, and 8×4 lands both sheets on power-of-two dimensions. Both are well inside the 2048px cap that silently renders oversized textures black. The two sizes are **source resolution**, not tile size — `sheetFor()` picks the 128 sheet above 96px and the 64 sheet below. - -### The lookup - -`GuiEditorControlIcons` is a generated `ScriptObject` holding every entry in display order — `key`, `ctrlClass`, `category`, `label`, `frame` — with `keys()`, `frameFor()`, `classFor()`, `categoryFor()`, `labelFor()`, `isKnown()` and `sheetFor()`. - -The array is `ctrlClass`, not `class`: `class` is a live SimObject field, and `GuiEditorControlSpec` already documents dodging exactly that collision. - -### Drift: a fallback, not a build break - -Trading a runtime enumeration for a build-time list means a control class added to the engine could silently never reach the palette. That is handled by degrading, not by failing: - -1. **Frame 0 is the fallback**, deliberately not last. `frameFor` returns 0 for any unknown key, and an unset `Frame` field reads as 0 too — so both failures land on a legible "unknown control" rather than an arbitrary wrong picture. -2. **The palette keeps its runtime enumeration as a tail.** It walks the curated table for order, then appends anything `enumerateConsoleClasses("GuiControl")` yields that the table does not name, filtered by the rule `populate` uses today. A new class still appears and still drags; it wears the `?` until someone draws it. *(This half lands in the follow-on palette spec; the contract — curated ordered head plus a zero fallback — is established here.)* -3. **The build script prints, without failing**, both directions: placeable classes with no icon, and icons naming a class the engine no longer registers. - -Determining "placeable" needs the real ancestry, not a name prefix. Two traps found writing it: - -- `GuiControl` registers with **`IMPLEMENT_CONOBJECT_CHILDREN`**, not `IMPLEMENT_CONOBJECT`. Matching only the plain macro loses the one class the palette needs most. -- `GuiCursor`, `GuiControlProfile`, `GuiBorderProfile` and `GuiProfileTheme` are all registered and all named `Gui…`, but derive from `SimObject` and never appear in the palette. The script walks first-base ancestry to `GuiControl` — which is what `enumerateConsoleClasses("GuiControl")` actually answers. - -### Module wiring — two silent failures - -- **`editor/GuiEditor/module.taml` had no `` block and no `images` folder** — the one editor module shipping no assets. Without the block the PNGs sit on disk and never register, with no error anywhere. Added, matching `EditorCore/module.taml`; note the extension is `asset.taml`, not `image.taml`. Asset ids are `GuiEditor:controlIcons64` / `…128`. -- **`GuiEditor::create` execs every script by name.** Added the exec, plus the `ScriptObject` beside `themeLibrary` and its `delete()` in `GuiEditor::destroy`, per the ownership chain in `TORQUE_SCRIPT.md`. - -## Verification - -**Build-script assertions**, run every build: - -- every icon renders at both sizes, exactly `size × size` -- opaque bbox reaches at least two tile edges — catches an icon that silently drew small (this caught `GuiGridCtrl` stopping 2 units short of the right edge) -- **every opaque pixel carries the exact ramp value for its row.** Sampling the tile's top and bottom rows does not work: most icons are inset and touch neither. The first attempt asserted on the sheet's row 0 and failed against correct art, because no icon in the first sheet row happens to reach y=0. -- frame 0 is the fallback; no duplicate keys; 31 entries in 32 cells - -**`review.py`** writes contact sheets on a dark ground — the whole set at both sizes, the style probe at 2×/4×, and the eight pairs side by side. - -**`tests/shots/controlIcons.cs`** renders both sheets in-engine at the sizes the palette will draw them, tinted the way `EditorIconButton` tints, labelled by key, asking `GuiEditor.controlIcons` for the list so the page cannot drift from the sheet. Its real job is proving the assets load: a missing image renders as nothing rather than throwing, so the `DeclaredAssets` failure mode is a page of labels over blank space — which nothing else would catch. - -Full smoke suite re-run after the wiring changes. - -## Out of scope - -- The palette rewrite — grid/rows toggle, tile control, `populate` walking the table then appending the enumerated tail. -- 16/24/32 sheets and the Explorer tree adopting them. The 4-unit grid rule keeps that a rerun. - -## Open item for the follow-on - -The Control List window is **250px wide** by default (`GuiEditor.cs`, `Extent = "250 380"`). A true 128px grid gets one column at that width. The palette spec will need to widen the default extent, draw the 128 art at ~96px for two columns, or both. From e1a18972912ecd2986407d12d3b84abeeeebadb1 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Thu, 6 Aug 2026 20:06:20 -0400 Subject: [PATCH 49/55] GuiControl: a control that changes parent keeps the size it had A scaled control caches the proportion of its parent it occupies, so that a run of layout passes cannot round its edges away a pixel at a time. The Position and Extent field setters clear that cache, because writing a position is the moment it stops describing the control -- and changing parent is that moment too, which nothing had noticed. So onChildAdded applied the OLD parent's proportion to the NEW parent's extent. A button 200 wide at x=100 in an 800-wide container arrived 50 wide at x=25 in a 200-wide one, and the drop then put the position back under the pointer, which hid half of it: the button landed where it was dropped, at a quarter of its size. Every other sizing mode already kept its extent across a move, and now scale does too. The cache is recharged against the new parent rather than switched off, so a scaled control still scales when that parent is resized. mStoredExtent goes the same way. It records extent given up to minExtent and owed back when there is room again, and a debt run up under one parent is not the next one's to pay. Two more things fall out of testing it. moveSelectionToCtrl now resizes rather than writing mBounds directly. Keeping the control under the pointer is right for the modes that own their position and wrong for the two that do not: addObject has just centred or filled the control against its new parent, and the direct write threw that away. It looked correct only because the next mouse move called resize for some other reason and put it back. And rescuedPosition/pullIntoView, which the Explorer tree needs next: a tree drag has no pointer in it, so nothing supplies a position and a small enough new parent can leave the control entirely outside itself. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01JDKWjuEF5Nuw791mpU7aPF --- cmake/EngineSources.cmake | 1 + engine/source/gui/editor/guiEditCtrl.cc | 20 +- engine/source/gui/guiControl.cc | 67 ++ engine/source/gui/guiControl.h | 25 + engine/source/gui/guiControl_ScriptBinding.h | 22 + .../testing/tests/guiControlReparentTests.cc | 576 ++++++++++++++++++ 6 files changed, 710 insertions(+), 1 deletion(-) create mode 100644 engine/source/testing/tests/guiControlReparentTests.cc diff --git a/cmake/EngineSources.cmake b/cmake/EngineSources.cmake index eed2addfe..bb7b255de 100644 --- a/cmake/EngineSources.cmake +++ b/cmake/EngineSources.cmake @@ -339,6 +339,7 @@ set(TORQUE_ENGINE_SOURCES # ---- testing ---- ${TORQUE_SRC}/testing/unitTesting.cc # ---- testing/tests ---- + ${TORQUE_SRC}/testing/tests/guiControlReparentTests.cc ${TORQUE_SRC}/testing/tests/guiCursorHotSpotTests.cc ${TORQUE_SRC}/testing/tests/guiProfileThemeTests.cc ${TORQUE_SRC}/testing/tests/guiScrollLayoutTests.cc diff --git a/engine/source/gui/editor/guiEditCtrl.cc b/engine/source/gui/editor/guiEditCtrl.cc index dfd3437d0..5e10da220 100755 --- a/engine/source/gui/editor/guiEditCtrl.cc +++ b/engine/source/gui/editor/guiEditCtrl.cc @@ -1229,7 +1229,25 @@ void GuiEditCtrl::moveSelectionToCtrl(GuiControl* newParent) Point2I globalpos = ctrl->localToGlobalCoord(Point2I(0, 0)); newParent->addObject(ctrl); Point2I newpos = ctrl->globalToLocalCoord(globalpos) + ctrl->mBounds.point; - ctrl->mBounds.set(newpos, ctrl->mBounds.extent); + + // resize rather than a direct write to mBounds, which is what this was. + // + // Keeping the control under the pointer is right for the modes that own + // their position, and wrong for the two that do not: addObject has just + // centred or filled the control against its new parent, and writing + // mBounds threw that away. resize puts it back -- it forces center and + // fill itself, for exactly this reason -- so the control is correct the + // moment it arrives rather than on the next mouse move that happens to + // call resize for some other reason. + ctrl->resize(newpos, ctrl->mBounds.extent); + + // addObject cleared the cached proportion of a scaled control and + // onChildAdded recaptured it -- but against the position the control + // arrived at, which the line above has just replaced with the one under + // the pointer. Clear it again so the recapture happens from where the + // control actually is; otherwise the next time this parent is resized the + // control jumps back to where the drag happened to enter it. + ctrl->resetStoredRelPos(); } Con::executef(this, 2, "onSelectionParentChange", Con::getIntArg(newParent->getId())); diff --git a/engine/source/gui/guiControl.cc b/engine/source/gui/guiControl.cc index 74c3633ef..c91155d38 100755 --- a/engine/source/gui/guiControl.cc +++ b/engine/source/gui/guiControl.cc @@ -367,6 +367,30 @@ void GuiControl::addObject(SimObject *object) if(mAwake) ctrl->awaken(); + // Two pieces of cached sizing state describe the parent the control has just + // left, and neither means anything here. Both are cleared before + // onChildAdded, which is the first thing to read them. + // + // mStoredRelativePos is the proportion of its parent a SCALED control + // occupies, cached so that a run of layout passes cannot round its edges away + // a pixel at a time. Writing a position is the moment it stops describing the + // control, which is why the Position and Extent field setters clear it -- and + // changing parent is that moment too. Left alone, onChildAdded applied the OLD + // parent's proportion to the NEW parent's extent: a button 200 wide at x=100 + // in an 800-wide container arrived 50 wide at x=25 in a 200-wide one. Reset, + // the proportion is recaptured against the parent it has now, old and new + // extents are the same value in that call, and the arithmetic is the identity + // -- so a move keeps the size it moved, which is what every other sizing mode + // already did. + // + // mStoredExtent is extent given up to minExtent and owed back when there is + // room again. A debt run up under one parent is not the next one's to pay. + // + // Note this cannot fire on a re-add: addObject returns above when the object + // is already a child of this control. + ctrl->resetStoredRelPos(); + ctrl->resetStoredExtent(); + onChildAdded( ctrl ); } @@ -667,6 +691,49 @@ void GuiControl::parentResized(const Point2I &oldParentExtent, const Point2I &ne resize(newPosition, newExtent); } +// One axis of the rescue. Nothing of the control is visible when its far edge is +// at or before the parent's near edge, or its near edge is at or past the +// parent's far edge -- and a parent with no room at all shows nothing wherever +// the control is put, which the second test catches on its own (pos >= 0). +static S32 rescuedAxis(S32 pos, S32 extent, S32 parentInnerExtent) +{ + const bool offTheNearSide = (pos + extent) <= 0; + const bool offTheFarSide = pos >= parentInnerExtent; + + return (offTheNearSide || offTheFarSide) ? 0 : pos; +} + +Point2I GuiControl::rescuedPosition(const Point2I &pos, const Point2I &extent, const Point2I &parentInnerExtent) +{ + return Point2I(rescuedAxis(pos.x, extent.x, parentInnerExtent.x), + rescuedAxis(pos.y, extent.y, parentInnerExtent.y)); +} + +bool GuiControl::pullIntoView() +{ + GuiControl* parent = getParent(); + if (parent == NULL) + { + return false; + } + + const Point2I rescued = rescuedPosition(mBounds.point, mBounds.extent, parent->getInnerRect().extent); + if (rescued == mBounds.point) + { + return false; + } + + // Through resize rather than a direct write to mBounds, so a container that + // places its own children hears about it -- and then reset the cached + // proportion, because a scaled control that has just been moved must measure + // from where it landed. resize() does not do that for its caller: it is what + // parentResized itself calls, and clearing the cache there would defeat it. + resize(rescued, mBounds.extent); + resetStoredRelPos(); + + return true; +} + void GuiControl::preventResizeModeFill() { preventHorizResizeModeFill(); diff --git a/engine/source/gui/guiControl.h b/engine/source/gui/guiControl.h index 3a1ba3752..14c86ab02 100755 --- a/engine/source/gui/guiControl.h +++ b/engine/source/gui/guiControl.h @@ -486,6 +486,31 @@ class GuiControl : public SimGroup, public virtual Tickable /// @param newParentExtent The new size of the parent object virtual void parentResized(const Point2I &oldParentExtent, const Point2I &newParentExtent); + /// Where a control goes when a move has stranded it outside its parent. + /// + /// Reparenting in the Explorer tree is a gesture with no pointer in it, so + /// nothing supplies a position and the control keeps the local one it held + /// in its old parent. Dropped into something smaller that can put it + /// entirely outside: not clipped, not partly visible, gone. + /// + /// Per axis, because a placement that is still valid should be kept -- a + /// control that was 20 pixels down and 400 across is still 20 pixels down. + /// Only when it is ENTIRELY outside on that axis, because a control the user + /// can see is a control the user can drag, and moving one that merely + /// overhangs the edge would be undoing a placement rather than rescuing it. + /// + /// Static and taking everything it uses, so the arithmetic can be tested + /// without a canvas -- as GuiScrollCtrl::subtractScrollBars is. + static Point2I rescuedPosition(const Point2I &pos, const Point2I &extent, const Point2I &parentInnerExtent); + + /// The same against the parent this control actually has, resizing it if it + /// had to move. Answers whether it did. + /// + /// A no-op for a control that is even partly visible, and for one with no + /// parent at all, so a caller can ask about a whole selection without + /// working out which members need it. + bool pullIntoView(); + /// Removes the resize mode of fill and changes it to right or bottom void preventResizeModeFill(); diff --git a/engine/source/gui/guiControl_ScriptBinding.h b/engine/source/gui/guiControl_ScriptBinding.h index e4f31690b..d630af911 100644 --- a/engine/source/gui/guiControl_ScriptBinding.h +++ b/engine/source/gui/guiControl_ScriptBinding.h @@ -450,6 +450,28 @@ ConsoleMethodWithDocs(GuiControl, applySizing, ConsoleVoid, 2, 2, ()) object->parentResized( extent, extent ); } +/*! Moves this control back inside its parent if a move has left it entirely + outside. + + Reparenting in the Explorer tree is a gesture with no pointer in it, so + nothing supplies a position and the control keeps the local one it held in + its old parent. Dropped into something smaller that can put it entirely + outside: not clipped, not partly visible, gone. + + An axis on which nothing of the control is visible is set to 0. The other axis + is left alone -- a control that was 20 pixels down and 400 across is still 20 + pixels down -- and so is a control that merely overhangs an edge, because one + the user can see is one the user can drag. + + Does nothing to a control with no parent, so a whole selection can be offered + without checking each member. + @return True if the control had to be moved. +*/ +ConsoleMethodWithDocs(GuiControl, pullIntoView, ConsoleBool, 2, 2, ()) +{ + return object->pullIntoView(); +} + /*! Returns true if this control draws its children. A control that does not render children can never be a container: the isContainer field is forced to false for it and editing that field is diff --git a/engine/source/testing/tests/guiControlReparentTests.cc b/engine/source/testing/tests/guiControlReparentTests.cc new file mode 100644 index 000000000..a0a795b70 --- /dev/null +++ b/engine/source/testing/tests/guiControlReparentTests.cc @@ -0,0 +1,576 @@ +//----------------------------------------------------------------------------- +// 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. +//----------------------------------------------------------------------------- + +// We don't want tests in a shipping version. +#ifndef TORQUE_SHIPPING + +#ifndef _UNIT_TESTING_H_ +#include "testing/unitTesting.h" +#endif + +#ifndef _GUICONTROL_H_ +#include "gui/guiControl.h" +#endif + +#ifndef _SIMBASE_H_ +#include "sim/simBase.h" +#endif + +//----------------------------------------------------------------------------- +// What happens to a control when it changes parent. +// +// In the Gui Editor a control can change parent two ways: dragged across the +// canvas until the pointer is over a different container, or dragged in the +// Explorer tree from one branch to another. Either way the control is handed to +// GuiControl::addObject, which ends in onChildAdded, which calls parentResized +// with the new parent's inner extent for BOTH the old and the new value -- a +// zero delta, so that a centred or filled child settles into its new home at +// once and everything else is left alone. +// +// "Left alone" is the promise these tests hold it to. A move is a move: the +// control keeps the size it had, in every mode but the two that compute a size +// from the parent every layout. +// +// None of this needs a canvas. Nothing here is woken, nothing measures text, and +// no font is ever asked for, so there is no texture assert waiting -- see the +// note about that in guiScrollLayoutTests.cc. The controls are real ones with +// real parents because the thing under test IS the reparent, not arithmetic that +// could be lifted out of it. The one piece that could be lifted out -- the rescue +// at the bottom of this file -- was. +//----------------------------------------------------------------------------- + +static StringTableEntry reparentField( const char* name ) +{ + return StringTable->insert( name ); +} + +// Through the fields rather than resize(), because a control with no parent yet +// is exactly the case resize() has nothing to lay out against. The field setters +// also clear the sizing batteries, which is the state a freshly built control is +// supposed to be in. +static GuiControl* makeControl( const char* position, const char* extent ) +{ + GuiControl* ctrl = new GuiControl(); + ctrl->registerObject(); + ctrl->setDataField( reparentField( "Position" ), NULL, position ); + ctrl->setDataField( reparentField( "Extent" ), NULL, extent ); + return ctrl; +} + +static void setSizing( GuiControl* ctrl, const char* horiz, const char* vert ) +{ + ctrl->setDataField( reparentField( "HorizSizing" ), NULL, horiz ); + ctrl->setDataField( reparentField( "VertSizing" ), NULL, vert ); +} + +// GuiDefaultProfile's border profile is all zeroes -- GuiBorderProfile's +// constructor sets margin, border and padding to 0 and GuiDefaultBorderProfile +// never changes them -- so a control wearing it has an inner rect the size of +// its bounds. Asserted rather than assumed: every expected number below is +// written as though inner and outer are the same, and if that ever stops being +// true this is the test that says so rather than six confusing failures. +TEST( GuiControlReparentTests, TheDefaultProfileCostsAChildNothing ) +{ + GuiControl* parent = makeControl( "0 0", "800 600" ); + + const RectI inner = parent->getInnerRect(); + ASSERT_EQ( inner.point.x, 0 ); + ASSERT_EQ( inner.point.y, 0 ); + ASSERT_EQ( inner.extent.x, 800 ); + ASSERT_EQ( inner.extent.y, 600 ); + + parent->deleteObject(); +} + +//----------------------------------------------------------------------------- +// scale -- the mode that was wrong. +// +// A scaled control caches the proportion of its parent it occupies, so that a +// run of layout passes cannot round its edges away a pixel at a time +// (relPosBatteryH, behind the mUseRelPosH flag). resetStoredRelPos clears the +// cache, and the Position and Extent field setters call it, because writing a +// position is the moment the cached proportion stops describing the control. +// +// Changing parent is that moment too. Before the fix nothing called it there, so +// onChildAdded applied the OLD parent's proportion to the NEW parent's extent: a +// button 200 wide at x=100 in an 800-wide container came out 50 wide at x=25 in +// a 200-wide one. The drop then put the position back under the pointer, which +// hid half the damage -- the button landed where it was dropped, at a quarter of +// its size. +//----------------------------------------------------------------------------- + +TEST( GuiControlReparentTests, ScaleKeepsItsExtentInASmallerParent ) +{ + GuiControl* big = makeControl( "0 0", "800 600" ); + GuiControl* small = makeControl( "0 0", "200 150" ); + + GuiControl* child = makeControl( "100 100", "200 40" ); + setSizing( child, "scale", "scale" ); + + big->addObject( child ); + ASSERT_EQ( child->getExtent().x, 200 ) << "Arriving anywhere must not resize it."; + ASSERT_EQ( child->getExtent().y, 40 ); + + small->addObject( child ); + + ASSERT_EQ( child->getExtent().x, 200 ) + << "A move is a move. Scale describes what a control does when its " + "parent is RESIZED, not what it does when it is handed to a " + "different one -- 0.125 to 0.375 of 200 is 50, which is the bug."; + ASSERT_EQ( child->getExtent().y, 40 ); + ASSERT_EQ( child->getPosition().x, 100 ) + << "And the position is the caller's to set afterwards, not the " + "layout's to guess at."; + ASSERT_EQ( child->getPosition().y, 100 ); + + small->deleteObject(); + big->deleteObject(); +} + +// The other direction, which fails differently: a stale proportion applied to a +// bigger parent inflates the control instead of shrinking it. Worth its own case +// because the fix could plausibly have been a clamp, and a clamp would pass the +// test above and fail this one. +TEST( GuiControlReparentTests, ScaleKeepsItsExtentInALargerParent ) +{ + GuiControl* small = makeControl( "0 0", "200 150" ); + GuiControl* big = makeControl( "0 0", "800 600" ); + + GuiControl* child = makeControl( "20 20", "100 30" ); + setSizing( child, "scale", "scale" ); + + small->addObject( child ); + big->addObject( child ); + + ASSERT_EQ( child->getExtent().x, 100 ) + << "0.1 to 0.6 of 800 is 400 -- four times the size it was dropped at."; + ASSERT_EQ( child->getExtent().y, 30 ); + + big->deleteObject(); + small->deleteObject(); +} + +// The feature the cache exists for, which the fix must not have switched off. +// Resetting the proportion on a move recharges it against the new parent; it +// does not stop it being used. +TEST( GuiControlReparentTests, ScaleStillScalesWhenItsOwnParentResizes ) +{ + GuiControl* parent = makeControl( "0 0", "800 600" ); + + GuiControl* child = makeControl( "100 100", "200 40" ); + setSizing( child, "scale", "scale" ); + parent->addObject( child ); + + parent->resize( Point2I( 0, 0 ), Point2I( 400, 300 ) ); + + ASSERT_EQ( child->getPosition().x, 50 ) << "Half the parent, half the offset."; + ASSERT_EQ( child->getExtent().x, 100 ) << "And half the width."; + ASSERT_EQ( child->getPosition().y, 50 ); + ASSERT_EQ( child->getExtent().y, 20 ); + + parent->deleteObject(); +} + +// The whole point of the cache, and the reason the fix had to be a reset rather +// than a removal. Recomputing the proportion from integer bounds every pass +// loses a little each time; the cached one is exact, so a round trip comes back +// where it started. +TEST( GuiControlReparentTests, ScaleSurvivesARoundTripWithoutDrift ) +{ + GuiControl* parent = makeControl( "0 0", "800 600" ); + + GuiControl* child = makeControl( "101 101", "199 41" ); + setSizing( child, "scale", "scale" ); + parent->addObject( child ); + + parent->resize( Point2I( 0, 0 ), Point2I( 333, 251 ) ); + parent->resize( Point2I( 0, 0 ), Point2I( 97, 63 ) ); + parent->resize( Point2I( 0, 0 ), Point2I( 800, 600 ) ); + + ASSERT_EQ( child->getPosition().x, 101 ) << "Three resizes, no drift."; + ASSERT_EQ( child->getExtent().x, 199 ); + ASSERT_EQ( child->getPosition().y, 101 ); + ASSERT_EQ( child->getExtent().y, 41 ); + + parent->deleteObject(); +} + +// A scaled control that has been moved scales against the parent it is in now. +// This is the pair to the test above: the cache must be recharged by the move, +// not merely ignored during it. +TEST( GuiControlReparentTests, ScaleScalesAgainstTheNewParentAfterAMove ) +{ + GuiControl* big = makeControl( "0 0", "800 600" ); + GuiControl* small = makeControl( "0 0", "200 150" ); + + GuiControl* child = makeControl( "20 20", "100 30" ); + setSizing( child, "scale", "scale" ); + + big->addObject( child ); + small->addObject( child ); + + small->resize( Point2I( 0, 0 ), Point2I( 100, 75 ) ); + + ASSERT_EQ( child->getPosition().x, 10 ) + << "Halving the parent it lives in now halves it. Against the 800-wide " + "parent it came from, 20 of 800 would round to 0."; + ASSERT_EQ( child->getExtent().x, 50 ); + ASSERT_EQ( child->getPosition().y, 10 ); + ASSERT_EQ( child->getExtent().y, 15 ); + + small->deleteObject(); + big->deleteObject(); +} + +//----------------------------------------------------------------------------- +// The modes that were already right, locked down so that the fix above cannot +// quietly change them. Each one is a different branch of parentResized. +//----------------------------------------------------------------------------- + +TEST( GuiControlReparentTests, AnchoredKeepsBothPositionAndExtent ) +{ + GuiControl* big = makeControl( "0 0", "800 600" ); + GuiControl* small = makeControl( "0 0", "200 150" ); + + GuiControl* child = makeControl( "100 100", "200 40" ); + setSizing( child, "anchorLeft", "anchorTop" ); + + big->addObject( child ); + small->addObject( child ); + + ASSERT_EQ( child->getPosition().x, 100 ) + << "A zero delta moves an anchored control not at all -- which is what " + "lets it end up outside a smaller parent. See the rescue below."; + ASSERT_EQ( child->getPosition().y, 100 ); + ASSERT_EQ( child->getExtent().x, 200 ); + ASSERT_EQ( child->getExtent().y, 40 ); + + small->deleteObject(); + big->deleteObject(); +} + +// width and height pin both edges, so they respond to a delta by changing size. +// A move has no delta, so they behave like the anchors here. +TEST( GuiControlReparentTests, WidthAndHeightKeepBothPositionAndExtent ) +{ + GuiControl* big = makeControl( "0 0", "800 600" ); + GuiControl* small = makeControl( "0 0", "200 150" ); + + GuiControl* child = makeControl( "100 100", "200 40" ); + setSizing( child, "width", "height" ); + + big->addObject( child ); + small->addObject( child ); + + ASSERT_EQ( child->getPosition().x, 100 ); + ASSERT_EQ( child->getPosition().y, 100 ); + ASSERT_EQ( child->getExtent().x, 200 ); + ASSERT_EQ( child->getExtent().y, 40 ); + + small->deleteObject(); + big->deleteObject(); +} + +TEST( GuiControlReparentTests, CenterRecentersInTheNewParent ) +{ + GuiControl* big = makeControl( "0 0", "800 600" ); + GuiControl* small = makeControl( "0 0", "200 150" ); + + GuiControl* child = makeControl( "0 0", "100 40" ); + setSizing( child, "center", "center" ); + + big->addObject( child ); + ASSERT_EQ( child->getPosition().x, 350 ) << "(800 - 100) / 2"; + ASSERT_EQ( child->getPosition().y, 280 ) << "(600 - 40) / 2"; + + small->addObject( child ); + ASSERT_EQ( child->getPosition().x, 50 ) << "(200 - 100) / 2"; + ASSERT_EQ( child->getPosition().y, 55 ) << "(150 - 40) / 2"; + ASSERT_EQ( child->getExtent().x, 100 ) << "Centering moves a control; it does not resize one."; + ASSERT_EQ( child->getExtent().y, 40 ); + + small->deleteObject(); + big->deleteObject(); +} + +TEST( GuiControlReparentTests, FillFillsTheNewParent ) +{ + GuiControl* big = makeControl( "0 0", "800 600" ); + GuiControl* small = makeControl( "0 0", "200 150" ); + + GuiControl* child = makeControl( "10 10", "100 40" ); + setSizing( child, "fill", "fill" ); + + big->addObject( child ); + ASSERT_EQ( child->getPosition().x, 0 ); + ASSERT_EQ( child->getPosition().y, 0 ); + ASSERT_EQ( child->getExtent().x, 800 ); + ASSERT_EQ( child->getExtent().y, 600 ); + + small->addObject( child ); + ASSERT_EQ( child->getPosition().x, 0 ); + ASSERT_EQ( child->getPosition().y, 0 ); + ASSERT_EQ( child->getExtent().x, 200 ); + ASSERT_EQ( child->getExtent().y, 150 ); + + small->deleteObject(); + big->deleteObject(); +} + +// The axes are independent, and mixing them is the case a fix that resets "the +// battery" as one thing would get wrong. +TEST( GuiControlReparentTests, TheTwoAxesAreIndependent ) +{ + GuiControl* big = makeControl( "0 0", "800 600" ); + GuiControl* small = makeControl( "0 0", "200 150" ); + + GuiControl* child = makeControl( "100 100", "200 40" ); + setSizing( child, "scale", "center" ); + + big->addObject( child ); + small->addObject( child ); + + ASSERT_EQ( child->getExtent().x, 200 ) << "Scale across: the size is kept."; + ASSERT_EQ( child->getPosition().x, 100 ); + ASSERT_EQ( child->getPosition().y, 55 ) << "Center down: (150 - 40) / 2."; + ASSERT_EQ( child->getExtent().y, 40 ); + + small->deleteObject(); + big->deleteObject(); +} + +//----------------------------------------------------------------------------- +// The minExtent battery, which is the same stale-state bug wearing different +// clothes. mStoredExtent records extent a control gave up to its minExtent and +// is owed back when there is room again. A debt run up under one parent means +// nothing under the next, so a move clears it. +//----------------------------------------------------------------------------- + +TEST( GuiControlReparentTests, AMinExtentDebtDoesNotFollowTheControl ) +{ + GuiControl* squeezer = makeControl( "0 0", "800 600" ); + + GuiControl* child = makeControl( "0 0", "400 300" ); + child->setDataField( reparentField( "MinExtent" ), NULL, "100 80" ); + setSizing( child, "width", "height" ); + squeezer->addObject( child ); + + // Squeeze it past its minimum: it stops at 100 wide and remembers it is owed + // the rest. + squeezer->resize( Point2I( 0, 0 ), Point2I( 400, 380 ) ); + ASSERT_EQ( child->getExtent().x, 100 ) << "Clamped at the minimum."; + + GuiControl* roomy = makeControl( "0 0", "800 600" ); + roomy->addObject( child ); + ASSERT_EQ( child->getExtent().x, 100 ) << "The move itself changes nothing."; + + // Growing the new parent by 50 should give the control 50, not pay back a + // debt it ran up somewhere else. + roomy->resize( Point2I( 0, 0 ), Point2I( 850, 600 ) ); + ASSERT_EQ( child->getExtent().x, 150 ) + << "A control that arrives at 100 wide is 100 wide. Carrying the debt " + "over would swallow the 50 and leave it at 100."; + + roomy->deleteObject(); + squeezer->deleteObject(); +} + +//----------------------------------------------------------------------------- +// rescuedPosition -- where a control goes when a move has stranded it. +// +// A tree drag has no pointer, so nothing supplies a position and the control +// keeps the local one it held in its old parent. Dropped into something smaller +// that can put it entirely outside: not clipped, not partly visible, gone. +// +// Per axis, because a placement that is still valid should be kept -- a button +// that was 20 pixels down and 400 across is still 20 pixels down. Only when the +// control is ENTIRELY outside, because a control the user can see is a control +// the user can drag, and moving one that merely overhangs the edge would be +// undoing a placement rather than rescuing it. +// +// Static, so the arithmetic can be read on its own: the same shape as +// GuiScrollCtrl::subtractScrollBars and GuiTreeViewCtrl::resolveIndent. +//----------------------------------------------------------------------------- + +static const Point2I RescueInner( 100, 300 ); +static const Point2I RescueSize( 80, 24 ); + +TEST( GuiControlReparentTests, RescueLeavesAControlThatFitsAlone ) +{ + const Point2I at = GuiControl::rescuedPosition( Point2I( 10, 40 ), RescueSize, RescueInner ); + + ASSERT_EQ( at.x, 10 ); + ASSERT_EQ( at.y, 40 ); +} + +TEST( GuiControlReparentTests, RescueZerosTheAxisThatIsPastTheRightEdge ) +{ + const Point2I at = GuiControl::rescuedPosition( Point2I( 400, 20 ), RescueSize, RescueInner ); + + ASSERT_EQ( at.x, 0 ); + ASSERT_EQ( at.y, 20 ) << "20 down is still 20 down."; +} + +TEST( GuiControlReparentTests, RescueZerosTheAxisThatIsPastTheBottomEdge ) +{ + const Point2I at = GuiControl::rescuedPosition( Point2I( 20, 500 ), RescueSize, RescueInner ); + + ASSERT_EQ( at.x, 20 ); + ASSERT_EQ( at.y, 0 ); +} + +TEST( GuiControlReparentTests, RescueZerosBothWhenBothAreOut ) +{ + const Point2I at = GuiControl::rescuedPosition( Point2I( 400, 500 ), RescueSize, RescueInner ); + + ASSERT_EQ( at.x, 0 ); + ASSERT_EQ( at.y, 0 ); +} + +// Off the left and off the top are just as invisible, and cost nothing to catch. +TEST( GuiControlReparentTests, RescueCatchesOffTheLeftAndOffTheTop ) +{ + const Point2I left = GuiControl::rescuedPosition( Point2I( -90, 20 ), RescueSize, RescueInner ); + ASSERT_EQ( left.x, 0 ) << "-90 + 80 is -10: the right edge is off the left side."; + ASSERT_EQ( left.y, 20 ); + + const Point2I above = GuiControl::rescuedPosition( Point2I( 20, -30 ), RescueSize, RescueInner ); + ASSERT_EQ( above.x, 20 ); + ASSERT_EQ( above.y, 0 ) << "-30 + 24 is -6."; +} + +TEST( GuiControlReparentTests, RescueLeavesAControlThatIsOnlyPartlyOutside ) +{ + const Point2I over = GuiControl::rescuedPosition( Point2I( 90, 20 ), RescueSize, RescueInner ); + ASSERT_EQ( over.x, 90 ) << "10 pixels of it are visible, so it can be dragged."; + ASSERT_EQ( over.y, 20 ); + + const Point2I under = GuiControl::rescuedPosition( Point2I( -10, 20 ), RescueSize, RescueInner ); + ASSERT_EQ( under.x, -10 ) << "And 70 pixels here."; + ASSERT_EQ( under.y, 20 ); +} + +// The boundaries, where "entirely outside" is decided. A control whose left edge +// sits exactly on the parent's right edge shows nothing; one pixel back shows one +// pixel. +TEST( GuiControlReparentTests, RescueIsExactAtTheEdges ) +{ + ASSERT_EQ( GuiControl::rescuedPosition( Point2I( 100, 0 ), RescueSize, RescueInner ).x, 0 ) + << "Left edge on the parent's right edge: nothing is visible."; + ASSERT_EQ( GuiControl::rescuedPosition( Point2I( 99, 0 ), RescueSize, RescueInner ).x, 99 ) + << "One pixel visible is visible."; + ASSERT_EQ( GuiControl::rescuedPosition( Point2I( -80, 0 ), RescueSize, RescueInner ).x, 0 ) + << "Right edge on the parent's left edge: nothing is visible."; + ASSERT_EQ( GuiControl::rescuedPosition( Point2I( -79, 0 ), RescueSize, RescueInner ).x, -79 ); +} + +// A container with no room at all cannot show anything wherever the control is +// put, and 0 is the least surprising answer. +TEST( GuiControlReparentTests, RescueHandlesAParentWithNoRoom ) +{ + const Point2I at = GuiControl::rescuedPosition( Point2I( 40, 40 ), RescueSize, Point2I( 0, 0 ) ); + + ASSERT_EQ( at.x, 0 ); + ASSERT_EQ( at.y, 0 ); +} + +//----------------------------------------------------------------------------- +// pullIntoView -- the same thing against the parent a control actually has. +//----------------------------------------------------------------------------- + +TEST( GuiControlReparentTests, PullIntoViewRescuesAStrandedControl ) +{ + GuiControl* big = makeControl( "0 0", "800 600" ); + GuiControl* small = makeControl( "0 0", "100 300" ); + + GuiControl* child = makeControl( "400 20", "80 24" ); + setSizing( child, "anchorLeft", "anchorTop" ); + + big->addObject( child ); + small->addObject( child ); + ASSERT_EQ( child->getPosition().x, 400 ) << "Stranded by the move itself."; + + ASSERT_TRUE( child->pullIntoView() ) << "It moved, so it says so."; + ASSERT_EQ( child->getPosition().x, 0 ); + ASSERT_EQ( child->getPosition().y, 20 ); + ASSERT_EQ( child->getExtent().x, 80 ) << "A rescue moves a control; it does not resize one."; + ASSERT_EQ( child->getExtent().y, 24 ); + + ASSERT_FALSE( child->pullIntoView() ) << "And a second call has nothing to do."; + + small->deleteObject(); + big->deleteObject(); +} + +TEST( GuiControlReparentTests, PullIntoViewLeavesAVisibleControlAlone ) +{ + GuiControl* parent = makeControl( "0 0", "800 600" ); + + GuiControl* child = makeControl( "100 100", "200 40" ); + parent->addObject( child ); + + ASSERT_FALSE( child->pullIntoView() ); + ASSERT_EQ( child->getPosition().x, 100 ); + ASSERT_EQ( child->getPosition().y, 100 ); + + parent->deleteObject(); +} + +// A control with no parent has no view to be pulled into, and asking must not +// crash -- the Explorer tree asks about a whole selection without checking each +// one, and the root of the document is in that selection. +TEST( GuiControlReparentTests, PullIntoViewIsSafeWithNoParent ) +{ + GuiControl* orphan = makeControl( "400 400", "80 24" ); + + ASSERT_FALSE( orphan->pullIntoView() ); + ASSERT_EQ( orphan->getPosition().x, 400 ) << "Left exactly as it was."; + + orphan->deleteObject(); +} + +// A rescue has to leave the sizing cache honest, or the next time the parent +// resizes the control jumps back to where it was rescued from. +TEST( GuiControlReparentTests, PullIntoViewLeavesScaleMeasuringFromWhereItLanded ) +{ + GuiControl* big = makeControl( "0 0", "800 600" ); + GuiControl* small = makeControl( "0 0", "100 300" ); + + GuiControl* child = makeControl( "400 20", "80 24" ); + setSizing( child, "scale", "scale" ); + + big->addObject( child ); + small->addObject( child ); + ASSERT_TRUE( child->pullIntoView() ); + ASSERT_EQ( child->getPosition().x, 0 ); + + small->resize( Point2I( 0, 0 ), Point2I( 200, 300 ) ); + + ASSERT_EQ( child->getPosition().x, 0 ) + << "Doubling the parent doubles an offset of 0, which is 0."; + ASSERT_EQ( child->getExtent().x, 160 ) << "And doubles the width."; + + small->deleteObject(); + big->deleteObject(); +} + +#endif // TORQUE_SHIPPING From 64485f7910a2fd75c4d742dcb731fce93eb9a63c Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Thu, 6 Aug 2026 20:06:35 -0400 Subject: [PATCH 50/55] Gui Editor: a control a tree drag stranded comes back into view Dragging a row in the Explorer tree from one branch to another is the one reparenting gesture with no pointer in it. Nothing supplies a position, so the control keeps the local one it held in its old parent -- and a button at x=400 moved into a container 100 wide is not clipped or half hidden, it is gone. Gone from the canvas is nearly gone for good, because the canvas is where you would reach for it. So onPostReorder now offers each control that moved the chance to come back. Per axis, because a placement that is still valid should be kept: a control that was 20 pixels down and 400 across is still 20 pixels down. Only for a control that is ENTIRELY outside, because one the user can see is one the user can drag, and moving one that merely overhangs an edge would be undoing a placement rather than rescuing it. Before commitHierarchy, which is what makes undo right at no cost. The undo step is built from what layoutOf reads at commit time, so rescuing first folds the correction into the same "Reparent Control" action: one Ctrl+Z puts the control back in its old parent AT ITS OLD POSITION, rather than leaving it rescued in a parent that no longer wants it there. The canvas drag is deliberately left out. A dragged control is under the pointer and so inside its new container by construction, and a sibling of a multiple selection snapping to the origin half way through a gesture the user is still making is worse than the thing it fixes. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01JDKWjuEF5Nuw791mpU7aPF --- .../scripts/GuiEditorExplorerTree.cs | 46 +++ tests/smoke/reparent.cs | 360 ++++++++++++++++++ 2 files changed, 406 insertions(+) create mode 100644 tests/smoke/reparent.cs diff --git a/editor/GuiEditor/scripts/GuiEditorExplorerTree.cs b/editor/GuiEditor/scripts/GuiEditorExplorerTree.cs index 5a3e17478..e7899d454 100644 --- a/editor/GuiEditor/scripts/GuiEditorExplorerTree.cs +++ b/editor/GuiEditor/scripts/GuiEditorExplorerTree.cs @@ -55,9 +55,55 @@ 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 diff --git a/tests/smoke/reparent.cs b/tests/smoke/reparent.cs new file mode 100644 index 000000000..2f99776b4 --- /dev/null +++ b/tests/smoke/reparent.cs @@ -0,0 +1,360 @@ +//----------------------------------------------------------------------------- +// Moving a control from a large container into a small one, by each of the two +// gestures that can do it, in each of the sizing modes. +// +// the canvas drag the control until the pointer is over another container. +// GuiEditCtrl::moveSelectionToCtrl reparents it and then puts it +// back under the pointer, so where it ends up is settled and the +// only question is what happened to its SIZE. +// +// the tree drag its row onto another branch. There is no pointer in that +// gesture, so nothing supplies a position: the control keeps the +// local one it had in its old parent, and a small enough new +// parent can leave it entirely outside. +// +// Both were wrong for "scale". A scaled control caches the proportion of its +// parent it occupies, and nothing cleared that cache when it changed parent, so +// the old parent's proportion was applied to the new parent's extent -- a button +// 200 wide arriving in a container a quarter the width came out 50 wide. That +// half is engine-side and is pinned down properly in +// engine/source/testing/tests/guiControlReparentTests.cc, which needs no canvas. +// It is checked again here because this is the real editor doing it, through the +// real gesture code, to a real themed control whose profile has borders. +// +// The rescue is the half that only exists here: GuiEditorExplorerTree's +// onPostReorder pulls a stranded control back into view before the undo step is +// committed, so one Ctrl+Z puts it back in its old parent at its old position. +// +// Neither drag is posted. A real one needs startDragging, which mouse-locks the +// canvas, and the tree's needs mDragIndex state that only a genuine touch +// sequence sets up -- so each gesture is driven at the seam its own code uses: +// moveSelectionToCtrl for the canvas, and onPreReorder / add / onPostReorder for +// the tree, which is exactly what GuiTreeViewCtrl::reorderFromDrag does around +// its own move. +//----------------------------------------------------------------------------- + +setLogMode(2); +setScriptExecEcho(false); +trace(false); +$Scripts::ignoreDSOs = true; +setCompanyAndProduct("Torque Game Engines", "Torque2D"); +ModuleDatabase.EchoInfo = false; +AssetDatabase.EchoInfo = false; +AssetDatabase.IgnoreAutoUnload = true; + +testExec("editor/main.cs"); + +$Pass = 0; +$Fail = 0; + +function rpCheck(%label, %condition) +{ + if(%condition) + { + $Pass++; + echo("RPAR PASS: " @ %label); + } + else + { + $Fail++; + echo("RPAR FAIL: " @ %label); + } +} + +function rpSame(%label, %got, %want) +{ + rpCheck(%label @ " (" @ %got @ ")", %got $= %want); +} + +schedule(2000, 0, "rpSetup"); + +// A project, so there is a theme. An unthemed control wears its constructor's +// profiles, and the whole point of running this in the editor rather than in a +// unit test is that the containers here have real borders and so an inner rect +// that is smaller than their extent. +function rpSetup() +{ + ProjectManager.setProjectFolder("PlanetX"); + ModuleDatabase.ScanModules("PlanetX"); + ModuleDatabase.LoadExplicit("AppCore", 1); + + GuiEditor.open(); + schedule(500, 0, "rpBuild"); +} + +//----------------------------------------------------------------------------- +// The document: one large container and one small one, side by side on the root. +//----------------------------------------------------------------------------- + +function rpBuild() +{ + $rpBig = rpContainer("20 20", "600 400"); + $rpSmall = rpContainer("650 20", "100 80"); + + rpCheck("the large container is on the root", $rpBig.getGroup() == GuiEditor.rootGui); + rpCheck("the small container is on the root", $rpSmall.getGroup() == GuiEditor.rootGui); + + rpCanvasDrag(); +} + +function rpContainer(%pos, %ext) +{ + %ctrl = new GuiControl() + { + Position = %pos; + Extent = %ext; + isContainer = true; + }; + + // Through the brain, so the control arrives the way a dropped one does: added + // to the current add set, themed, announced, and listed in the tree. + GuiEditor.brain.setCurrentAddSet(GuiEditor.rootGui); + GuiEditor.brain.acceptControl(%ctrl); + + return %ctrl; +} + +// A fresh button in the large container, in the sizing mode under test. Fresh +// each time: a control that has already been moved once has a recharged +// proportion cache, and the bug is about the FIRST move. +function rpButton(%pos, %horiz, %vert) +{ + %button = new GuiButtonCtrl() + { + Position = %pos; + Extent = "200 40"; + Text = "Button"; + }; + + GuiEditor.brain.setCurrentAddSet($rpBig); + GuiEditor.brain.acceptControl(%button); + + %button.HorizSizing = %horiz; + %button.VertSizing = %vert; + + return %button; +} + +// Done with a control, between cases. Through the editor rather than a bare +// delete(): a control that is still the selection when it goes leaves the brain +// and every pane holding an id that no longer answers. This is the route the +// Delete key takes, and it puts the control in the trash rather than destroying +// it, which is also what keeps undo coherent. +function rpDiscard(%button) +{ + GuiEditor.brain.onInspect(%button); + GuiEditor.brain.deleteSelection(); + GuiEditor.brain.onDelete(); +} + +//----------------------------------------------------------------------------- +// The canvas drag. What has to survive it is the SIZE: the position is the +// gesture's to decide and it has already decided, by putting the control back +// under the pointer it was dragged with. +//----------------------------------------------------------------------------- + +function rpCanvasDrag() +{ + rpCanvasCase("anchored", "anchorLeft", "anchorTop"); + rpCanvasCase("width/height", "width", "height"); + rpCanvasCase("scale", "scale", "scale"); + + rpCanvasOwned("center", "center", "center", true); + rpCanvasOwned("fill", "fill", "fill", false); + + schedule(100, 0, "rpTreeDrag"); +} + +// The modes that keep their own geometry. A move must change neither the extent +// nor where the control appears on screen. +function rpCanvasCase(%mode, %horiz, %vert) +{ + %button = rpButton("100 100", %horiz, %vert); + + %extent = %button.getExtent(); + %where = %button.getGlobalPosition(); + + GuiEditor.brain.onInspect(%button); + GuiEditor.brain.moveSelectionToCtrl($rpSmall); + + rpCheck("canvas " @ %mode @ ": the control changed parent", + %button.getGroup() == $rpSmall); + rpSame("canvas " @ %mode @ ": the extent is the one it was dragged at", + %button.getExtent(), %extent); + rpSame("canvas " @ %mode @ ": and it is still where it was dropped", + %button.getGlobalPosition(), %where); + + rpDiscard(%button); +} + +// The two modes that compute their geometry from the parent every layout, and +// so are expected to move rather than to stay. +// +// What they are held to is that they settled against the container they are in +// NOW: running the layout again must change nothing. A control still carrying +// the old parent's answer fails that immediately, and unlike a hard-coded +// coordinate it does not need this test to know what the container's borders +// cost -- which is the whole reason these two are checked in the real themed +// editor rather than only in the unit suite. +// +// center is in this group for its POSITION only. It centers a control; it does +// not resize one, so a 200-wide button centered in a container half that wide +// stays 200 wide and hangs out of both sides. That is correct, and it is why +// this cannot simply assert that the control fits. +function rpCanvasOwned(%mode, %horiz, %vert, %keepsExtent) +{ + %button = rpButton("100 100", %horiz, %vert); + %extent = %button.getExtent(); + + GuiEditor.brain.onInspect(%button); + GuiEditor.brain.moveSelectionToCtrl($rpSmall); + + rpCheck("canvas " @ %mode @ ": the control changed parent", + %button.getGroup() == $rpSmall); + rpSettled("canvas " @ %mode, %button); + + if(%keepsExtent) + { + rpSame("canvas " @ %mode @ ": centering moves a control, it does not resize one", + %button.getExtent(), %extent); + } + else + { + rpCheck("canvas " @ %mode @ ": it took the new container's width, not the old one's", + getWord(%button.getExtent(), 0) < getWord($rpBig.getExtent(), 0)); + rpCheck("canvas " @ %mode @ ": and its height", + getWord(%button.getExtent(), 1) < getWord($rpBig.getExtent(), 1)); + } + + rpDiscard(%button); +} + +// Re-running the layout against the parent the control has now must be a no-op. +// applySizing is parentResized with a zero delta, so the modes that respond to a +// change have nothing to respond to and the two that describe a position simply +// reassert it -- which is exactly the question being asked. +function rpSettled(%label, %ctrl) +{ + %was = %ctrl.getPosition() SPC %ctrl.getExtent(); + %ctrl.applySizing(); + + rpSame(%label @ ": the move left it where a fresh layout would put it", + %ctrl.getPosition() SPC %ctrl.getExtent(), %was); +} + +//----------------------------------------------------------------------------- +// The tree drag. Here the position is nobody's to decide, so it is the position +// that is at stake -- and the extent has to hold as well. +//----------------------------------------------------------------------------- + +function rpTreeDrag() +{ + // Both axes outside the small container: 300 across is past a 100-wide one, + // and 100 down is past an 80-tall one. + rpTreeCase("anchored", "anchorLeft", "anchorTop", "300 100", "0 0"); + rpTreeCase("width/height", "width", "height", "300 100", "0 0"); + rpTreeCase("scale", "scale", "scale", "300 100", "0 0"); + + // One axis outside. 20 down is inside 80 even once the container's borders + // have taken their share, so it is kept: a control that was 20 pixels down is + // still 20 pixels down. + rpTreeCase("one axis", "anchorLeft", "anchorTop", "300 20", "0 20"); + + // Inside already, and not to be touched. + rpTreeCase("already visible", "anchorLeft", "anchorTop", "10 10", "10 10"); + + // The two that place themselves need no rescue, and must not get one: a + // control the layout has already put somewhere is not stranded, whatever its + // coordinates read. fill in particular sits at 0,0 with the parent's whole + // inner extent, which no rescue would touch, and center can legitimately be + // at a NEGATIVE position when the control is wider than the container. + rpTreeOwned("center", "center", "center"); + rpTreeOwned("fill", "fill", "fill"); + + schedule(100, 0, "rpUndo"); +} + +function rpTreeOwned(%mode, %horiz, %vert) +{ + %button = rpButton("300 100", %horiz, %vert); + + rpTreeMove(%button, $rpSmall); + + rpCheck("tree " @ %mode @ ": the control changed parent", + %button.getGroup() == $rpSmall); + rpSettled("tree " @ %mode, %button); + + rpDiscard(%button); +} + +function rpTreeCase(%mode, %horiz, %vert, %at, %expect) +{ + %button = rpButton(%at, %horiz, %vert); + %extent = %button.getExtent(); + + rpTreeMove(%button, $rpSmall); + + rpCheck("tree " @ %mode @ ": the control changed parent", + %button.getGroup() == $rpSmall); + rpSame("tree " @ %mode @ ": the extent survived the move", + %button.getExtent(), %extent); + rpSame("tree " @ %mode @ ": it is somewhere the user can see", + %button.getPosition(), %expect); + + rpDiscard(%button); +} + +// What GuiTreeViewCtrl::reorderFromDrag does around its own move, with the +// selection set the way a drag would have left it. +function rpTreeMove(%ctrl, %target) +{ + %tree = GuiEditor.explorerWindow.tree; + + %index = %tree.findItemID(%ctrl); + rpCheck("the tree has a row for the control", %index != -1); + + %tree.clearSelection(); + %tree.setSelected(%index, true); + + %tree.onPreReorder(); + %target.add(%ctrl); + %tree.onPostReorder(); + + %tree.refresh(); +} + +//----------------------------------------------------------------------------- +// Undo. The rescue runs before commitHierarchy, so the corrected position is +// what the undo step records as the "after" -- which means one step puts the +// control back in its old parent AT ITS OLD POSITION, rather than leaving it +// rescued somewhere it was never placed. +//----------------------------------------------------------------------------- + +function rpUndo() +{ + %button = rpButton("300 100", "anchorLeft", "anchorTop"); + %extent = %button.getExtent(); + + rpTreeMove(%button, $rpSmall); + rpSame("undo: the move stranded it and the rescue caught it", + %button.getPosition(), "0 0"); + + GuiEditor.Undo(); + + rpCheck("undo put the control back in the container it came from", + %button.getGroup() == $rpBig); + rpSame("undo put it back where it was, not where it was rescued to", + %button.getPosition(), "300 100"); + rpSame("undo left the extent alone", %button.getExtent(), %extent); + + // And forward again, because a rescue that only survives one direction is a + // rescue the user loses by pressing Ctrl+Y. + GuiEditor.Redo(); + + rpCheck("redo moved it back into the small container", + %button.getGroup() == $rpSmall); + rpSame("redo restored the rescued position", %button.getPosition(), "0 0"); + + echo("RPAR DONE " @ $Pass @ " passed, " @ $Fail @ " failed"); + quit(); +} From cf7f9db2ecd6fde063b1af07d7d6c1d58449e77d Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Thu, 6 Aug 2026 23:58:54 -0400 Subject: [PATCH 51/55] Gui Editor: a hidden control is out of the way, not just out of sight Clicking the eye in the Explorer stopped a control being drawn and left it taking every click aimed at what was behind it -- so you could see through it but not reach through it, which is the one thing hiding is for. The intent was already written down in two places that promised it and nowhere that did it: SimObject::_writeHidden's comment ("hide a control to reach what was behind it") and the eye's own tooltip. The asymmetry was one line. renderChildControls skipped a hidden child; findHitControl did not. Both now ask GuiControl::isHiddenInEditor, so the paint and the hit test cannot drift apart again, and skipping the child before the recursion takes its whole subtree with it exactly as the render does. The test is scoped by isEditMode, which walks up to the edit root and gives up at once when the editor is shut, so a running game neither pays for it nor obeys it. Three paths reach a control without hit testing it, and all three would have defeated that: - The sizing knobs are tested at the top of onTouchDown, ahead of the hit test, and the eye click deliberately does not move the selection - so a control hidden while selected kept eight invisible grab zones straddling its edges, which is where someone reaching past it would click. editGeometryFrozen gained isHidden, which settles the resize cursor, the resize gesture, the drag snap-back and the arrow-key nudge at once, and makes the decoration honest: it already drew a hidden control as a dashed outline with no handles. - A rubber band walks the add set's children rather than hit testing them. - The add set is not chosen by hit testing at all, so hiding the container being worked in left the next control placed inside it, appearing nowhere. GuiEditCtrl::controlHidden walks back out to what still draws. A drag heals itself, because it picks its target with findHitControl. Locked is deliberately untouched: a padlock means "do not change this by accident", not "get out of my way", and you still want to click it to read its properties. Verified both ways round. guiHitTestTests.cc covers the rule and its scoping without a canvas; hiddenClickThrough.cs asks the same questions of the real editor; hiddenNotATarget.cs posts real input for the three paths script cannot reach - a click on a hidden control's knob, a click on the eye of the add set, and a band across a hidden control and a shown one. With the engine change reverted, each of those fails exactly the checks it exists for and no others. Send-EngineDrag is new in tests/lib/input.ps1: a posted press, run of moves and release does drive a rubber band, unlike the drag-and-drop gesture. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01JDKWjuEF5Nuw791mpU7aPF --- cmake/EngineSources.cmake | 1 + engine/source/gui/editor/guiEditCtrl.cc | 65 ++++- engine/source/gui/editor/guiEditCtrl.h | 1 + .../gui/editor/guiEditorExplorerTree.cc | 8 + engine/source/gui/guiControl.cc | 13 +- engine/source/gui/guiControl.h | 13 + .../source/testing/tests/guiHitTestTests.cc | 220 +++++++++++++++ tests/lib/input.ps1 | 29 ++ tests/smoke/hiddenClickThrough.cs | 186 +++++++++++++ tests/smoke/hiddenNotATarget.cs | 261 ++++++++++++++++++ tests/smoke/hiddenNotATarget.input.ps1 | 59 ++++ 11 files changed, 846 insertions(+), 10 deletions(-) create mode 100644 engine/source/testing/tests/guiHitTestTests.cc create mode 100644 tests/smoke/hiddenClickThrough.cs create mode 100644 tests/smoke/hiddenNotATarget.cs create mode 100644 tests/smoke/hiddenNotATarget.input.ps1 diff --git a/cmake/EngineSources.cmake b/cmake/EngineSources.cmake index bb7b255de..8b7181c48 100644 --- a/cmake/EngineSources.cmake +++ b/cmake/EngineSources.cmake @@ -341,6 +341,7 @@ set(TORQUE_ENGINE_SOURCES # ---- testing/tests ---- ${TORQUE_SRC}/testing/tests/guiControlReparentTests.cc ${TORQUE_SRC}/testing/tests/guiCursorHotSpotTests.cc + ${TORQUE_SRC}/testing/tests/guiHitTestTests.cc ${TORQUE_SRC}/testing/tests/guiProfileThemeTests.cc ${TORQUE_SRC}/testing/tests/guiScrollLayoutTests.cc ${TORQUE_SRC}/testing/tests/guiTextEditTests.cc diff --git a/engine/source/gui/editor/guiEditCtrl.cc b/engine/source/gui/editor/guiEditCtrl.cc index 5e10da220..7e45c1279 100755 --- a/engine/source/gui/editor/guiEditCtrl.cc +++ b/engine/source/gui/editor/guiEditCtrl.cc @@ -307,6 +307,46 @@ void GuiEditCtrl::setEditMode(bool value) mCurrentAddSet = mEditorRoot; } +// The eye was just turned off for a control. If the container being worked in is +// that control or something inside it, come back out to the nearest one that is +// still drawn. +// +// A drag heals itself, because onControlDragged picks its target with +// findHitControl and that no longer answers anything hidden. Click-to-place from +// the palette does not: it adds into the add set, so without this the next +// control placed would land inside a hidden parent and appear nowhere at all. +// +// The selection is deliberately left alone. Clicking the eye must not move it - +// that is the promise the Explorer's gutter is built around. +void GuiEditCtrl::controlHidden(GuiControl* ctrl) +{ + if (ctrl == NULL || !ctrl->isHidden() || mCurrentAddSet == NULL) + return; + + bool inside = false; + for (GuiControl* walk = mCurrentAddSet; walk != NULL; walk = walk->getParent()) + { + if (walk == ctrl) + { + inside = true; + break; + } + if (walk == mEditorRoot) + break; + } + + if (!inside) + return; + + // Up past anything else that is hidden. One hidden branch inside another is + // the case this loop is for. + GuiControl* newAddSet = ctrl->getParent(); + while (newAddSet != NULL && newAddSet != mEditorRoot && newAddSet->isHidden()) + newAddSet = newAddSet->getParent(); + + setCurrentAddSet(newAddSet != NULL ? newAddSet : mEditorRoot, false); +} + void GuiEditCtrl::setCurrentAddSet(GuiControl* ctrl, bool clearSelection) { if (ctrl != mCurrentAddSet) @@ -428,13 +468,17 @@ S32 GuiEditCtrl::getSizingHitKnobs(const Point2I& pt, const RectI& box) // Whether the editor must leave this control's position and extent alone. // -// Two different reasons that come to the same thing: isLocked is the padlock the -// user turned on, and isGeometryEditable is the control saying its parent +// Three different reasons that come to the same thing. isLocked is the padlock +// the user turned on. isGeometryEditable is the control saying its parent // dictates its geometry - a tab page, a menu item - so that dragging it would -// change a number something else overwrites on the next layout pass. +// change a number something else overwrites on the next layout pass. And +// isHidden is the eye: a control the editor does not draw offers nothing to take +// hold of, so the eight sizing knobs it would otherwise keep - hit tested before +// findHitControl ever runs, and drawn by nothing - would be invisible traps +// straddling the edges of whatever the user was trying to reach behind it. static inline bool editGeometryFrozen(GuiControl* ctrl) { - return ctrl == NULL || ctrl->isLocked() || !ctrl->isGeometryEditable(); + return ctrl == NULL || ctrl->isHidden() || ctrl->isLocked() || !ctrl->isGeometryEditable(); } void GuiEditCtrl::drawControlDecoration(GuiControl* ctrl, RectI& box, ColorI& outlineColor, ColorI& nutColor) @@ -982,6 +1026,13 @@ void GuiEditCtrl::onTouchUp(const GuiEvent& event) for (i = mCurrentAddSet->begin(); i != mCurrentAddSet->end(); i++) { GuiControl* ctrl = dynamic_cast(*i); + + // A band is drawn across the canvas, and a hidden control is not on + // the canvas. This walks the children rather than hit testing them, + // so it is the one selection path findHitControl does not answer for. + if (ctrl == NULL || ctrl->isHidden()) + continue; + Point2I upperL = globalToLocalCoord(ctrl->localToGlobalCoord(Point2I(0, 0))); Point2I lowerR = upperL + ctrl->mBounds.extent - Point2I(1, 1); @@ -1217,8 +1268,10 @@ void GuiEditCtrl::moveSelectionToCtrl(GuiControl* newParent) if (ctrl->getParent() == newParent) continue; - // skip locked controls - if (ctrl->isLocked()) + // skip locked controls, and hidden ones: this runs off a drag that + // moveSelection has already refused to move them with, so reparenting + // them here would change the one number the drag left alone + if (ctrl->isLocked() || ctrl->isHidden()) continue; // skip controls that will not live there - a tab page outside a tab book diff --git a/engine/source/gui/editor/guiEditCtrl.h b/engine/source/gui/editor/guiEditCtrl.h index 1b91a9c4f..3ab2c77b6 100755 --- a/engine/source/gui/editor/guiEditCtrl.h +++ b/engine/source/gui/editor/guiEditCtrl.h @@ -88,6 +88,7 @@ class GuiEditCtrl : public GuiControl void setCurrentAddSet(GuiControl *ctrl, bool clearSelection = true); const GuiControl* getCurrentAddSet() const; void setSelection(GuiControl *ctrl); + void controlHidden(GuiControl *ctrl); // Undo Access void undo(); diff --git a/engine/source/gui/editor/guiEditorExplorerTree.cc b/engine/source/gui/editor/guiEditorExplorerTree.cc index 1cc51b9f3..4b46282ed 100644 --- a/engine/source/gui/editor/guiEditorExplorerTree.cc +++ b/engine/source/gui/editor/guiEditorExplorerTree.cc @@ -23,6 +23,7 @@ #include "gui/editor/guiEditorExplorerTree.h" #include "graphics/dgl.h" #include "gui/guiCanvas.h" +#include "gui/editor/guiEditCtrl.h" #include "guiEditorExplorerTree_ScriptBinding.h" @@ -353,6 +354,13 @@ void GuiEditorExplorerTree::onTouchDown(const GuiEvent& event) if (column == GutterHidden) { obj->setHidden(!obj->isHidden()); + + // A hidden control is not a target on the canvas any more, and that + // includes being the container the next control is placed into. + if (GuiControl::smEditorHandle != NULL) + { + GuiControl::smEditorHandle->controlHidden(dynamic_cast(obj)); + } } else { diff --git a/engine/source/gui/guiControl.cc b/engine/source/gui/guiControl.cc index c91155d38..e521e13b0 100755 --- a/engine/source/gui/guiControl.cc +++ b/engine/source/gui/guiControl.cc @@ -1233,7 +1233,7 @@ void GuiControl::renderChildControls(const Point2I& offset, const RectI& content Con::errorf( "GuiControl::renderChildControls() object %i is NULL", count ); continue; } - if (ctrl->mVisible && (!isEditMode() || !ctrl->isHidden())) + if (ctrl->mVisible && !isHiddenInEditor(ctrl)) { renderChild(ctrl, offset, content, clipRect); } @@ -1672,6 +1672,10 @@ bool GuiControl::pointInControl(const Point2I& parentCoordPoint) } +// A hidden child is skipped here and not descended into, which is what takes its +// whole subtree with it - the same thing renderChildControls does, and it has to +// be the same thing, or the Gui Editor's eye would take a control out of sight +// while leaving it in the way of every click aimed at what is behind it. GuiControl* GuiControl::findHitControl(const Point2I &pt, S32 initialLayer, const bool ignoreUseInput, const bool ignoreEditSelected) { iterator i = end(); // find in z order (last to first) @@ -1683,9 +1687,10 @@ GuiControl* GuiControl::findHitControl(const Point2I &pt, S32 initialLayer, cons { continue; } - else if (ctrl->pointInControl(pt - ctrl->mRenderInsetLT) && - ctrl->mVisible && - (ignoreUseInput || ctrl->mUseInput) && + else if (ctrl->pointInControl(pt - ctrl->mRenderInsetLT) && + ctrl->mVisible && + !isHiddenInEditor(ctrl) && + (ignoreUseInput || ctrl->mUseInput) && (ignoreEditSelected || (isEditMode() && !ctrl->isEditSelected()))) { Point2I ptemp = pt - (ctrl->mBounds.point + ctrl->mRenderInsetLT); diff --git a/engine/source/gui/guiControl.h b/engine/source/gui/guiControl.h index 14c86ab02..c6f26e99d 100755 --- a/engine/source/gui/guiControl.h +++ b/engine/source/gui/guiControl.h @@ -175,6 +175,19 @@ class GuiControl : public SimGroup, public virtual Tickable virtual bool isEditMode(); virtual bool isEditSelected(); + /// Whether a child of this control must be treated as though it were not + /// there: the eye in the Gui Editor's Explorer is off for it. + /// + /// The paint and the hit test both ask this, and they have to agree. A + /// control the editor does not draw must not be a target either, or the eye + /// takes it out of sight while leaving it in the way - which is the one + /// thing hiding is for. + /// + /// Cheap test first: isHidden is a bit, isEditMode walks the parent chain, + /// and the answer for almost every control on almost every mouse move is no. + inline bool isHiddenInEditor(GuiControl* child) + { return child->isHidden() && isEditMode(); } + /// @name Keyboard Input /// @{ GuiControl* mFirstResponder; diff --git a/engine/source/testing/tests/guiHitTestTests.cc b/engine/source/testing/tests/guiHitTestTests.cc new file mode 100644 index 000000000..445d0b35d --- /dev/null +++ b/engine/source/testing/tests/guiHitTestTests.cc @@ -0,0 +1,220 @@ +//----------------------------------------------------------------------------- +// 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. +//----------------------------------------------------------------------------- + +// We don't want tests in a shipping version. +#ifndef TORQUE_SHIPPING + +#ifndef _UNIT_TESTING_H_ +#include "testing/unitTesting.h" +#endif + +#ifndef _GUICONTROL_H_ +#include "gui/guiControl.h" +#endif + +#ifndef _GUIEDITCTRL_H_ +#include "gui/editor/guiEditCtrl.h" +#endif + +#ifndef _SIMBASE_H_ +#include "sim/simBase.h" +#endif + +//----------------------------------------------------------------------------- +// What the mouse can hit, and what the Gui Editor's eye takes away. +// +// Clicking the eye in the Explorer sets SimObject's Hidden flag, and +// renderChildControls stops drawing that control and everything under it. The +// hit test has to agree, or the eye takes a control out of sight while leaving +// it in the way of every click aimed at what is behind it -- which is the one +// thing hiding is for. Both paths now ask isHiddenInEditor, and these are the +// tests that hold them to the same answer. +// +// The rule is scoped to the editor, twice over: isEditMode walks up the parent +// chain looking for the edit root, and gives up at once if smDesignTime is +// false. The last two tests are the ones that pin that down -- a shipped game +// must not pay for this and must not obey it. +// +// None of it needs a canvas. findHitControl reads mBounds and mRenderInsetLT and +// nothing else; nothing here is woken, nothing measures text, and no font is +// ever asked for, so there is no texture assert waiting (see the note in +// guiScrollLayoutTests.cc). The GuiEditCtrl is real because isEditMode reads the +// real static and asks it for its real root -- stubbing that would test the stub. +//----------------------------------------------------------------------------- + +static StringTableEntry hitTestField( const char* name ) +{ + return StringTable->insert( name ); +} + +// Through the fields rather than resize(), for the reason given in +// guiControlReparentTests.cc: a control with no parent yet is exactly the case +// resize() has nothing to lay out against. +static GuiControl* makeHitControl( const char* position, const char* extent ) +{ + GuiControl* ctrl = new GuiControl(); + ctrl->registerObject(); + ctrl->setDataField( hitTestField( "Position" ), NULL, position ); + ctrl->setDataField( hitTestField( "Extent" ), NULL, extent ); + return ctrl; +} + +// One back panel with a smaller front panel sitting on top of it, and a control +// deeper still inside the front one. Later children are drawn last and hit +// first, so "front" really is in front. +// +// root 0,0 800x600 +// back 100,100 400x300 -> 100..499, 100..399 +// front 200,150 200x100 -> 200..399, 150..249 +// deep 20,20 60x40 -> 220..279, 170..209 in root's coordinates +class GuiHitTestTests : public ::testing::Test +{ +protected: + virtual void SetUp() + { + mWasDesignTime = GuiControl::smDesignTime; + mWasEditorHandle = GuiControl::smEditorHandle; + + mRoot = makeHitControl( "0 0", "800 600" ); + mBack = makeHitControl( "100 100", "400 300" ); + mFront = makeHitControl( "200 150", "200 100" ); + mDeep = makeHitControl( "20 20", "60 40" ); + + mRoot->addObject( mBack ); + mRoot->addObject( mFront ); + mFront->addObject( mDeep ); + + // Exactly what GuiEditCtrl::onWake does when the editor opens. + mEdit = new GuiEditCtrl(); + mEdit->registerObject(); + mEdit->setRoot( mRoot ); + GuiControl::smDesignTime = true; + GuiControl::smEditorHandle = mEdit; + } + + virtual void TearDown() + { + // Put the statics back before anything else can run: leaving the editor + // switched on would follow this suite into the next one. + GuiControl::smDesignTime = mWasDesignTime; + GuiControl::smEditorHandle = mWasEditorHandle; + + mEdit->deleteObject(); + mRoot->deleteObject(); // and the three controls it holds + } + + // A point over deep, and so over front and back as well. + Point2I overDeep() const { return Point2I( 250, 190 ); } + + // Over front and back, but outside deep. + Point2I overFront() const { return Point2I( 380, 240 ); } + + GuiControl* mRoot; + GuiControl* mBack; + GuiControl* mFront; + GuiControl* mDeep; + GuiEditCtrl* mEdit; + bool mWasDesignTime; + GuiEditCtrl* mWasEditorHandle; +}; + +TEST_F( GuiHitTestTests, TheEditRootIsInEditMode ) +{ + ASSERT_TRUE( mRoot->isEditMode() ) << "Everything below depends on this."; + ASSERT_TRUE( mFront->isEditMode() ) << "isEditMode walks up to the edit root."; +} + +TEST_F( GuiHitTestTests, TheFrontOneIsHit ) +{ + ASSERT_EQ( mRoot->findHitControl( overFront() ), mFront ); + ASSERT_EQ( mRoot->findHitControl( overDeep() ), mDeep ); +} + +TEST_F( GuiHitTestTests, HidingTheFrontOneLetsTheClickThrough ) +{ + mFront->setHidden( true ); + + ASSERT_EQ( mRoot->findHitControl( overFront() ), mBack ) + << "The whole point of the eye: a control that is not drawn is not a " + "target, so the click reaches what is behind it."; +} + +TEST_F( GuiHitTestTests, AHiddenControlTakesItsChildrenWithIt ) +{ + mFront->setHidden( true ); + + ASSERT_EQ( mRoot->findHitControl( overDeep() ), mBack ) + << "Hiding a container stops the whole branch being drawn, so the whole " + "branch has to stop being hit -- otherwise a hidden panel's children " + "still eat every click aimed through it."; +} + +TEST_F( GuiHitTestTests, HidingAChildLeavesItsParentAlone ) +{ + mDeep->setHidden( true ); + + ASSERT_EQ( mRoot->findHitControl( overDeep() ), mFront ) + << "One control, not the branch it sits in."; + ASSERT_EQ( mRoot->findHitControl( overFront() ), mFront ); +} + +TEST_F( GuiHitTestTests, ShowingItAgainPutsItBack ) +{ + mFront->setHidden( true ); + mFront->setHidden( false ); + + ASSERT_EQ( mRoot->findHitControl( overFront() ), mFront ); + ASSERT_EQ( mRoot->findHitControl( overDeep() ), mDeep ); +} + +// The flag is editor scaffolding and is never written to a file, so the only +// thing standing between it and a shipped game is isEditMode. These two tests +// are that guarantee, from both ends. + +TEST_F( GuiHitTestTests, OutsideTheEditRootTheFlagMeansNothing ) +{ + GuiControl* stray = makeHitControl( "0 0", "800 600" ); + GuiControl* strayBack = makeHitControl( "100 100", "400 300" ); + GuiControl* strayFront = makeHitControl( "200 150", "200 100" ); + stray->addObject( strayBack ); + stray->addObject( strayFront ); + strayFront->setHidden( true ); + + ASSERT_FALSE( stray->isEditMode() ) << "Never parented into the edit root."; + ASSERT_EQ( stray->findHitControl( overFront() ), strayFront ) + << "A Gui that is not the one being edited must behave exactly as it " + "did before, flag or no flag."; + + stray->deleteObject(); +} + +TEST_F( GuiHitTestTests, WithTheEditorShutTheFlagMeansNothing ) +{ + mFront->setHidden( true ); + GuiControl::smDesignTime = false; + + ASSERT_EQ( mRoot->findHitControl( overFront() ), mFront ) + << "smDesignTime is off the moment the editor sleeps, and a running " + "game must not consult the flag at all."; +} + +#endif // TORQUE_SHIPPING diff --git a/tests/lib/input.ps1 b/tests/lib/input.ps1 index 31a9f1437..1a2266d5a 100644 --- a/tests/lib/input.ps1 +++ b/tests/lib/input.ps1 @@ -98,6 +98,35 @@ function Send-EngineClick { [TorqueInput]::PostMessage($Hwnd, $script:WM_LBUTTONUP, [IntPtr]0, $lp) | Out-Null } +# A press, a run of moves and a release, for a gesture a single click cannot +# reach: a rubber band, a control dragged across the canvas. +# +# The moves carry MK_LBUTTON in wParam, which is what the window proc sees from a +# real drag, and they are stepped rather than jumped: a control that acts on the +# distance covered rather than on the end point would otherwise see one enormous +# move and clamp it. +function Send-EngineDrag { + param([IntPtr]$Hwnd, [int]$FromX, [int]$FromY, [int]$ToX, [int]$ToY, [int]$Steps = 8) + + $from = [IntPtr](($FromY -shl 16) -bor $FromX) + [TorqueInput]::PostMessage($Hwnd, $script:WM_MOUSEMOVE, [IntPtr]0, $from) | Out-Null + Start-Sleep -Milliseconds 200 + [TorqueInput]::PostMessage($Hwnd, $script:WM_LBUTTONDOWN, [IntPtr]$script:MK_LBUTTON, $from) | Out-Null + Start-Sleep -Milliseconds 150 + + for ($i = 1; $i -le $Steps; $i++) { + $x = $FromX + [int]((($ToX - $FromX) * $i) / $Steps) + $y = $FromY + [int]((($ToY - $FromY) * $i) / $Steps) + $lp = [IntPtr](($y -shl 16) -bor $x) + [TorqueInput]::PostMessage($Hwnd, $script:WM_MOUSEMOVE, [IntPtr]$script:MK_LBUTTON, $lp) | Out-Null + Start-Sleep -Milliseconds 60 + } + + Start-Sleep -Milliseconds 150 + $to = [IntPtr](($ToY -shl 16) -bor $ToX) + [TorqueInput]::PostMessage($Hwnd, $script:WM_LBUTTONUP, [IntPtr]0, $to) | Out-Null +} + function Send-EngineKey { param([IntPtr]$Hwnd, [string]$Key) diff --git a/tests/smoke/hiddenClickThrough.cs b/tests/smoke/hiddenClickThrough.cs new file mode 100644 index 000000000..b26b6638d --- /dev/null +++ b/tests/smoke/hiddenClickThrough.cs @@ -0,0 +1,186 @@ +//----------------------------------------------------------------------------- +// Clicking through a control the Gui Editor's eye has hidden. +// +// The rule itself is unit tested (guiHitTestTests.cc) against a GuiEditCtrl the +// test builds and points the statics at. What that cannot check is the wiring: +// that the editor as it really boots has the Gui under edit inside its edit +// root, so isEditMode is true for the controls on the canvas and the eye means +// anything at all. Get that wrong and every unit test still passes while the +// feature does nothing. +// +// So this asks the same question of the real editor, through the same entry +// point the mouse uses -- GuiControl::findHitControl, via its script binding. +// The unhidden answers are asserted first: they are what make the hidden ones +// mean something rather than just meaning the coordinates were wrong. +//----------------------------------------------------------------------------- + +setLogMode(1); +setScriptExecEcho(false); +trace(false); +$Scripts::ignoreDSOs = true; +setCompanyAndProduct("Torque Game Engines", "Torque2D"); +ModuleDatabase.EchoInfo = false; +AssetDatabase.EchoInfo = false; +AssetDatabase.IgnoreAutoUnload = true; + +testExec("editor/main.cs"); + +$Pass = 0; +$Fail = 0; + +function hctCheck(%label, %condition) +{ + if(%condition) + { + $Pass++; + echo("HCT PASS: " @ %label); + } + else + { + $Fail++; + echo("HCT FAIL: " @ %label); + } +} + +schedule(2500, 0, "hctOpenProject"); + +// The long way in. GuiEditor.open() would be enough for a suite that only calls +// methods on it, but the thing under test is whether the Gui being edited is +// really inside the editor's edit root -- so the editor has to be brought up the +// way it comes up for a person. +function hctOpenProject() +{ + ProjectManager.setProjectFolder("PlanetX"); + EditorCore.projectSelector.onProjectSelected(pathConcat(getMainDotCsDir(), "PlanetX")); + schedule(2500, 0, "hctOpenEditor"); +} + +// Pages register in load order: EditorConsole, ProjectManager, AssetAdmin, +// GuiEditor. +function hctOpenEditor() +{ + EditorCore.toggleEditor(); + EditorCore.tabBook.selectPage(3); + schedule(1500, 0, "hctClear"); +} + +// Start from an empty Gui, in a step of its own: PlanetX finishes displaying its +// own Gui after the editor page comes up, so clearing in the same frame as the +// build leaves its controls on the canvas and in the way of every point below. +function hctClear() +{ + GuiEditor.NewGui(); + discardUnsavedPrompt(); + schedule(500, 0, "hctBuild"); +} + +// One back panel, a smaller front panel on top of it, and a control deeper still +// inside the front one. Later children are drawn last and hit first, so "front" +// really is in front. +// +// rootGui 0,0 the simulated canvas +// back 100,100 400x300 -> 100..499, 100..399 +// front 200,150 200x100 -> 200..399, 150..249 +// deep 20,20 60x40 -> 220..279, 170..209 in rootGui's space +// +// Sizing is nailed down rather than left to the default, because a mode that +// computes its position from the parent would move these out from under the +// points below the moment they were added. +function hctBuild() +{ + %root = GuiEditor.rootGui; + + $hctBack = new GuiControl() + { + Position = "100 100"; + Extent = "400 300"; + HorizSizing = "right"; + VertSizing = "bottom"; + }; + %root.add($hctBack); + + $hctFront = new GuiControl() + { + Position = "200 150"; + Extent = "200 100"; + HorizSizing = "right"; + VertSizing = "bottom"; + }; + %root.add($hctFront); + + $hctDeep = new GuiControl() + { + Position = "20 20"; + Extent = "60 40"; + HorizSizing = "right"; + VertSizing = "bottom"; + }; + $hctFront.add($hctDeep); + + // A frame for the canvas to lay everything out and render once, so the + // render insets findHitControl subtracts are the real ones. + schedule(500, 0, "hctRun"); +} + +// Over front, clear of deep. +function hctOverFront() +{ + return "380 240"; +} + +// Over deep, and so over front and back as well. +function hctOverDeep() +{ + return "250 190"; +} + +function hctHit(%point) +{ + return GuiEditor.rootGui.findHitControl(getWord(%point, 0), getWord(%point, 1)); +} + +function hctRun() +{ + // --- The baseline. If these fail, nothing below is about hiding. + hctCheck("the front panel is hit where it covers the back one", + hctHit(hctOverFront()) == $hctFront.getId()); + hctCheck("and its child is hit where the child is", + hctHit(hctOverDeep()) == $hctDeep.getId()); + hctCheck("the back panel is hit where nothing covers it", + hctHit("120 120") == $hctBack.getId()); + + // --- The whole point. + $hctFront.hidden = true; + + hctCheck("hiding the front panel lets the click reach the back one", + hctHit(hctOverFront()) == $hctBack.getId()); + hctCheck("and takes its children with it", + hctHit(hctOverDeep()) == $hctBack.getId()); + + // --- It is out of the way, not gone: the Explorer is the only way back. + %tree = GuiEditor.explorerWindow.tree; + %tree.refresh(); + hctCheck("a hidden control still has a row in the Explorer", + %tree.findItemID($hctFront.getId()) > 0); + hctCheck("and still answers as hidden", + $hctFront.hidden); + + // --- And back. + $hctFront.hidden = false; + + hctCheck("showing it again makes it a target again", + hctHit(hctOverFront()) == $hctFront.getId()); + hctCheck("and its child with it", + hctHit(hctOverDeep()) == $hctDeep.getId()); + + // --- One control, not the branch it sits in. + $hctDeep.hidden = true; + + hctCheck("hiding a child leaves its parent hit-testable", + hctHit(hctOverDeep()) == $hctFront.getId()); + hctCheck("and the parent is still hit where it always was", + hctHit(hctOverFront()) == $hctFront.getId()); + + echo("HCT RESULT: " @ $Pass @ " passed, " @ $Fail @ " failed"); + quit(); +} diff --git a/tests/smoke/hiddenNotATarget.cs b/tests/smoke/hiddenNotATarget.cs new file mode 100644 index 000000000..79b358f51 --- /dev/null +++ b/tests/smoke/hiddenNotATarget.cs @@ -0,0 +1,261 @@ +//----------------------------------------------------------------------------- +// The three ways a hidden control could still be picked up, none of which goes +// through findHitControl. +// +// hiddenClickThrough.cs asks findHitControl directly, which is the rule itself. +// These two get in front of it: +// +// 1. The sizing knobs. GuiEditCtrl::onTouchDown tests them BEFORE it hit tests +// anything, and clicking the eye deliberately does not move the selection -- +// so a control hidden while selected would keep eight invisible grab zones +// straddling its edges, which is exactly where someone aiming at what is +// behind it would click. editGeometryFrozen is what closes that. +// +// 2. The container being worked in. It is where a palette click places the next +// control, and it is not chosen by hit testing at all, so hiding it would +// leave the next control placed inside a hidden parent, appearing nowhere. +// GuiEditCtrl::controlHidden is what closes that. +// +// 3. The rubber band. It walks the children of the container being worked in +// rather than hit testing them, so it would sweep up a control that is not +// drawn along with the ones that are. +// +// All three need real input: the knob test lives in the middle of the touch +// path, only the Explorer's gutter click calls controlHidden, and a band needs a +// press, a run of moves and a release. Driving any of them from script would +// skip the code under test. hiddenNotATarget.input.ps1 posts them. +//----------------------------------------------------------------------------- + +setLogMode(1); +setScriptExecEcho(false); +trace(false); +$Scripts::ignoreDSOs = true; +setCompanyAndProduct("Torque Game Engines", "Torque2D"); +ModuleDatabase.EchoInfo = false; +AssetDatabase.EchoInfo = false; +AssetDatabase.IgnoreAutoUnload = true; + +testExec("editor/main.cs"); + +$Pass = 0; +$Fail = 0; + +function hntCheck(%label, %condition) +{ + if(%condition) + { + $Pass++; + echo("HNT PASS: " @ %label); + } + else + { + $Fail++; + echo("HNT FAIL: " @ %label); + } +} + +// The engine works out where to click and leaves the point for the PowerShell +// side, exactly as explorerGutter.cs does: a hard-coded coordinate that drifted +// off its target would report a control that never reacted, which is what a +// broken hit test reports too. +function hntPostTarget(%point) +{ + createPath(testRoot("shots/")); + %file = new FileObject(); + %file.openForWrite(testRoot("shots/hiddenNotATargetTarget.txt")); + %file.writeLine(%point); + %file.close(); + %file.delete(); +} + +schedule(2500, 0, "hntOpenProject"); + +function hntOpenProject() +{ + ProjectManager.setProjectFolder("PlanetX"); + EditorCore.projectSelector.onProjectSelected(pathConcat(getMainDotCsDir(), "PlanetX")); + schedule(2500, 0, "hntOpenEditor"); +} + +// Pages register in load order: EditorConsole, ProjectManager, AssetAdmin, +// GuiEditor. +function hntOpenEditor() +{ + EditorCore.toggleEditor(); + EditorCore.tabBook.selectPage(3); + schedule(1500, 0, "hntClear"); +} + +function hntClear() +{ + GuiEditor.NewGui(); + discardUnsavedPrompt(); + schedule(500, 0, "hntBuild"); +} + +// rootGui the simulated canvas +// back 100,100 400x300 +// front 200,150 200x100 -- selected, then hidden +// +// front's top left corner sits well inside back, so the click below is both on +// front's top-left sizing knob and over back. Before the fix it resized front. +function hntBuild() +{ + %root = GuiEditor.rootGui; + + $hntBack = new GuiControl() + { + Position = "100 100"; + Extent = "400 300"; + HorizSizing = "right"; + VertSizing = "bottom"; + }; + %root.add($hntBack); + + $hntFront = new GuiControl() + { + Position = "200 150"; + Extent = "200 100"; + HorizSizing = "right"; + VertSizing = "bottom"; + }; + %root.add($hntFront); + + // A frame to lay out and render, so the global positions read below are the + // ones the mouse would land on. + schedule(500, 0, "hntKnobs"); +} + +function hntKnobs() +{ + GuiEditor.brain.select($hntFront); + $hntFront.hidden = true; + + %sel = GuiEditor.brain.getSelected(); + hntCheck("the hidden control is the one selected", + %sel.getCount() == 1 && %sel.getObject(0) == $hntFront.getId()); + + echo("HNT: canvas at " @ GuiEditor.rootGui.getGlobalPosition() @ + " extent " @ GuiEditor.rootGui.getExtent()); + echo("HNT: front's top-left knob at " @ $hntFront.getGlobalPosition()); + + // Its top-left knob: dead centre of the eight, and the one furthest from + // anything else that could claim the press. + hntPostTarget($hntFront.getGlobalPosition()); + + schedule(9000, 0, "hntAfterKnobClick"); +} + +function hntAfterKnobClick() +{ + %sel = GuiEditor.brain.getSelected(); + + hntCheck("clicking a hidden control's sizing knob selects what is behind it", + %sel.getCount() == 1 && %sel.getObject(0) == $hntBack.getId()); + hntCheck("and did not resize the hidden control", + $hntFront.getExtent() $= "200 100"); + hntCheck("nor move it", $hntFront.getPosition() $= "200 150"); + + schedule(200, 0, "hntAddSet"); +} + +// Now the container being worked in. Hiding it has to put the add set back +// somewhere that is still drawn, or the next control placed lands inside it. +function hntAddSet() +{ + $hntBox = new GuiControl() + { + Position = "520 100"; + Extent = "150 150"; + HorizSizing = "right"; + VertSizing = "bottom"; + }; + GuiEditor.rootGui.add($hntBox); + + %tree = GuiEditor.explorerWindow.tree; + %tree.refresh(); + + GuiEditor.brain.setCurrentAddSet($hntBox); + hntCheck("the container being worked in is the one about to be hidden", + GuiEditor.brain.getCurrentAddSet() == $hntBox.getId()); + + $hntBoxIndex = %tree.findItemID($hntBox.getId()); + hntCheck("it has a row in the Explorer", $hntBoxIndex > 0); + + echo("HNT: its eye box at " @ %tree.getGutterPoint($hntBoxIndex, "hidden")); + hntPostTarget(%tree.getGutterPoint($hntBoxIndex, "hidden")); + + schedule(9000, 0, "hntAfterEyeClick"); +} + +function hntAfterEyeClick() +{ + hntCheck("clicking the eye hid the container", $hntBox.hidden); + hntCheck("and came back out of it, to what still draws", + GuiEditor.brain.getCurrentAddSet() == GuiEditor.rootGui.getId()); + + schedule(200, 0, "hntBand"); +} + +// The third path that does not hit test: a rubber band walks the children of the +// container being worked in and takes whatever it encloses. +// +// Both of these have to sit inside the canvas frame, which is a good deal +// smaller than the Gui it is showing -- the band is posted in window +// coordinates, and a point past the frame's edge would land on a tool window +// instead. The band starts clear of every control, or the press would be read as +// selecting one rather than as beginning a band. +function hntBand() +{ + // Said outright rather than inherited from the step above. A band only ever + // looks at the children of the container being worked in, so a phase that + // took the add set on trust would test nothing at all the moment the step + // before it went wrong -- which is precisely how it would go wrong. + GuiEditor.brain.setCurrentAddSet(GuiEditor.rootGui); + GuiEditor.brain.clearSelection(); + + $hntBandShown = new GuiControl() + { + Position = "20 400"; + Extent = "100 60"; + HorizSizing = "right"; + VertSizing = "bottom"; + }; + GuiEditor.rootGui.add($hntBandShown); + + $hntBandHidden = new GuiControl() + { + Position = "140 400"; + Extent = "100 60"; + HorizSizing = "right"; + VertSizing = "bottom"; + }; + GuiEditor.rootGui.add($hntBandHidden); + $hntBandHidden.hidden = true; + + %origin = GuiEditor.rootGui.getGlobalPosition(); + %x = getWord(%origin, 0); + %y = getWord(%origin, 1); + + %band = (%x + 10) SPC (%y + 380) SPC (%x + 280) SPC (%y + 480); + echo("HNT: band " @ %band); + hntPostTarget(%band); + + schedule(12000, 0, "hntAfterBand"); +} + +function hntAfterBand() +{ + %sel = GuiEditor.brain.getSelected(); + + // The band has to have done something, or the two checks below would pass on + // a gesture that never happened. + hntCheck("the band selected what it enclosed", %sel.getCount() > 0); + hntCheck("it took the control that is drawn", + %sel.isMember($hntBandShown)); + hntCheck("and left the hidden one where a band cannot reach it", + !%sel.isMember($hntBandHidden)); + + echo("HNT RESULT: " @ $Pass @ " passed, " @ $Fail @ " failed"); + quit(); +} diff --git a/tests/smoke/hiddenNotATarget.input.ps1 b/tests/smoke/hiddenNotATarget.input.ps1 new file mode 100644 index 000000000..53f47f1ac --- /dev/null +++ b/tests/smoke/hiddenNotATarget.input.ps1 @@ -0,0 +1,59 @@ +# Input for hiddenNotATarget.cs. Posts two real clicks: the top-left sizing knob +# of a control that is hidden while selected, then the eye box of the container +# being worked in. +# +# Neither point is written here. Where the knob lands depends on where the +# frameset put the canvas, and where the eye box lands depends on the theme's +# borders and the row height - so the engine works both out and leaves them for +# this script to pick up, one at a time. +# +# The clicks have to be real. The knob test sits in the middle of +# GuiEditCtrl::onTouchDown, ahead of the hit test, and only the Explorer's gutter +# click calls controlHidden; script could reach neither. +param([IntPtr]$Hwnd) + +. "$PSScriptRoot\..\lib\input.ps1" + +$target = Join-Path $PSScriptRoot "..\..\shots\hiddenNotATargetTarget.txt" + +# Anything left by an earlier run would be clicked before this run has built +# anything to click on. +if (Test-Path $target) { Remove-Item $target -Force } + +function Wait-ForTarget { + param([string]$Path, [int]$Seconds = 25) + + $deadline = (Get-Date).AddSeconds($Seconds) + while ((Get-Date) -lt $deadline) { + if (Test-Path $Path) { + $line = (Get-Content $Path -TotalCount 1) + if ($line -and $line.Trim()) { return $line.Trim().Split(' ') } + } + Start-Sleep -Milliseconds 250 + } + return $null +} + +# Two words is a point to click, four is a band to drag across. +$labels = @("the hidden control's sizing knob", + "the eye box of the add set", + "a band across both controls") +foreach ($label in $labels) { + $point = Wait-ForTarget -Path $target + if (-not $point) { + Write-Host " the engine never reported $label" + return + } + + Remove-Item $target -Force + + if ($point.Count -ge 4) { + Send-EngineDrag -Hwnd $Hwnd -FromX ([int]$point[0]) -FromY ([int]$point[1]) ` + -ToX ([int]$point[2]) -ToY ([int]$point[3]) + Write-Host " dragged $label from ($($point[0]),$($point[1])) to ($($point[2]),$($point[3]))" + } + else { + Send-EngineClick -Hwnd $Hwnd -X ([int]$point[0]) -Y ([int]$point[1]) + Write-Host " clicked $label at ($($point[0]),$($point[1]))" + } +} From 176670ac4ad3a3f1b08614d47c7f365b01943ab4 Mon Sep 17 00:00:00 2001 From: Greenfire27 Date: Fri, 7 Aug 2026 15:06:11 -0400 Subject: [PATCH 52/55] GuiEditorCursorCtrl: the pane lets go of a cursor that is deleted The hot-spot pane held a raw GuiCursor* and nothing told it when that cursor went away. Removing a theme's extra cursor is exactly that sequence, so the next frame drew from freed memory: resolveBitmap asked the dead object for its bitmap name and dereferenced whatever came back. On Linux that is a segfault partway through cursorPane.cs, with mCursor's vptr already NULL and every field garbage. On Windows the freed heap still read plausibly, which is the only reason this looked fine. The registration is tracked in mNotifiedCursor rather than inferred from mCursor, because the two are not always the same. setCursorObject has no callers at all: the pane is bound by writing the "cursor" field, which TypeGuiCursor resolves and stores straight into mCursor with no chance for us to see what was there before. So the notify is reconciled after the write instead -- onStaticModified is the one hook every path goes through, script assignment and .gui.taml load alike -- and setCursorObject reconciles too, so binding either way is safe. onDeleteNotify drops the pointer and releases the decoded art, leaving the pane drawing empty until it is given another cursor, which is what it already does for a cursor whose file is missing. SimObject unwinds both halves of the registration itself in unregisterObject, so there is nothing to undo in the destructor. GuiTextEditCtrl::mEditCursor is exposed the same way through the same console type, and is left alone here: nothing deletes a cursor out from under a text box yet, and that one predates this branch. Co-Authored-By: Claude Opus 5 (1M context) --- .../source/gui/editor/guiEditorCursorCtrl.cc | 51 +++++++++++++++++++ .../source/gui/editor/guiEditorCursorCtrl.h | 20 ++++++++ 2 files changed, 71 insertions(+) diff --git a/engine/source/gui/editor/guiEditorCursorCtrl.cc b/engine/source/gui/editor/guiEditorCursorCtrl.cc index 07b66590a..4f49a40f4 100644 --- a/engine/source/gui/editor/guiEditorCursorCtrl.cc +++ b/engine/source/gui/editor/guiEditorCursorCtrl.cc @@ -35,6 +35,7 @@ IMPLEMENT_CONOBJECT(GuiEditorCursorCtrl); GuiEditorCursorCtrl::GuiEditorCursorCtrl() { mCursor = NULL; + mNotifiedCursor = NULL; mBitmap = NULL; mBitmapKey = StringTable->EmptyString; mZoom = 8; @@ -95,6 +96,56 @@ void GuiEditorCursorCtrl::setCursorObject(GuiCursor* cursor) { mCursor = cursor; mDragging = false; + bindCursorNotify(); +} + +// Both halves of the notification are unwound by SimObject itself when either +// end is deleted, so this only has to keep the registration pointed at the +// current cursor. +void GuiEditorCursorCtrl::bindCursorNotify() +{ + if (mCursor == mNotifiedCursor) + return; + + if (mNotifiedCursor != NULL) + clearNotify(mNotifiedCursor); + + if (mCursor != NULL) + deleteNotify(mCursor); + + mNotifiedCursor = mCursor; + + // Different cursor, so the art decoded for the last one is stale. resolveBitmap + // would notice a changed file name on its own, but not two cursors naming the + // same file with different hot spots. + releaseBitmap(); +} + +void GuiEditorCursorCtrl::onStaticModified(const char* slotName, const char* newValue) +{ + Parent::onStaticModified(slotName, newValue); + + if (dStricmp(slotName, "cursor") == 0) + { + mDragging = false; + bindCursorNotify(); + } +} + +void GuiEditorCursorCtrl::onDeleteNotify(SimObject* object) +{ + if (object == mNotifiedCursor) + { + // The notification is the cursor's last act -- SimObject has already taken + // this end of the registration apart -- so drop the pointer without + // clearing anything, and let the pane draw empty until it is given another. + mCursor = NULL; + mNotifiedCursor = NULL; + mDragging = false; + releaseBitmap(); + } + + Parent::onDeleteNotify(object); } void GuiEditorCursorCtrl::releaseBitmap() diff --git a/engine/source/gui/editor/guiEditorCursorCtrl.h b/engine/source/gui/editor/guiEditorCursorCtrl.h index 239809555..aff7e6151 100644 --- a/engine/source/gui/editor/guiEditorCursorCtrl.h +++ b/engine/source/gui/editor/guiEditorCursorCtrl.h @@ -69,6 +69,16 @@ class GuiEditorCursorCtrl : public GuiControl protected: GuiCursor* mCursor; ///< The cursor being edited. Nothing is drawn without one. + /// The cursor we hold a delete notification from, which is not always the one + /// in mCursor: the "cursor" field is written straight through TypeGuiCursor, + /// which overwrites the pointer without telling us what used to be there. So + /// the registration is tracked separately and reconciled after every write. + GuiCursor* mNotifiedCursor; + + /// Point the delete notification at whatever mCursor now holds. Cheap and + /// idempotent, so it is safe to call after any write to the field. + void bindCursorNotify(); + // Its own decoded copy of the art, because the pixels are the point. A // cursor's TextureHandle is a BitmapTexture, and TextureManager deletes the // CPU-side bitmap once it has uploaded one of those -- so asking the cursor @@ -125,6 +135,16 @@ class GuiEditorCursorCtrl : public GuiControl void onRender(Point2I offset, const RectI& updateRect); + /// A cursor can be deleted while this pane still points at it -- removing a + /// theme's extra cursor does exactly that -- and the next frame would draw + /// from freed memory. Forget it instead. + virtual void onDeleteNotify(SimObject* object); + + /// The "cursor" field bypasses setCursorObject entirely (script assigns it + /// directly, and so does a .gui.taml load), so this is the only place such a + /// write can be noticed. + virtual void onStaticModified(const char* slotName, const char* newValue = NULL); + void onTouchDown(const GuiEvent& event); void onTouchDragged(const GuiEvent& event); void onTouchUp(const GuiEvent& event); From 7e5899bfa0282d104df22ebb743f6e7719f5cccd Mon Sep 17 00:00:00 2001 From: Greenfire27 Date: Fri, 7 Aug 2026 15:06:21 -0400 Subject: [PATCH 53/55] Tests: run.sh finds the input-driven suites instead of naming two of them The bash runner skipped a hardcoded pair, tooltipProfile and textClick, as the suites whose input comes from a Win32-only *.input.ps1 companion. Six more have gained one since, and every one of them was being run without a driver: explorerGutter, hiddenNotATarget, menuBarClick, tabBookClick, textEdit and toggleTip. Five then failed, and failed convincingly -- "clicking the eye hid the control", "the box took the click and the caret is at the end" -- so a Unix run reported nineteen engine failures that were nothing but the missing mouse. The list is now read off the companions actually on disk, which is the same fact the pair was standing in for and cannot fall behind them again. Co-Authored-By: Claude Opus 5 (1M context) --- tests/run.sh | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/tests/run.sh b/tests/run.sh index 5b7ef3b8c..caee27495 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -9,10 +9,10 @@ # # A test passes when it logs no FAIL lines and exits on its own. # -# The two input-driven suites (tooltipProfile, textClick) have Win32-only -# companion scripts (*.input.ps1) that post real mouse/keyboard; there is no -# equivalent here, so those are skipped by default. Pass --with-input to run -# them anyway (they will get no synthetic input and may under-count). +# The input-driven suites -- the ones with a Win32-only *.input.ps1 companion +# posting real mouse/keyboard -- have no equivalent here, so they are skipped by +# default. Pass --with-input to run them anyway (they will get no synthetic input +# and will report their clicks as failures). # # Usage: # tests/run.sh every pass/fail suite @@ -63,14 +63,22 @@ fi KEEP_PROJECT=(bitmapPathRead) # Order matters for one pair only: bitmapPathWrite before bitmapPathRead. The -# input-driven pair is listed so ordering holds when --with-input is passed. +# input-driven suites are listed so ordering holds when --with-input is passed. ORDER=(profileEditor profileForm border borderPane standalone \ headerPane colorPopup themeApply font assetPicker \ tooltipProfile textClick bitmapPathWrite bitmapPathRead \ toybox planetX) -# Tests whose input path is Win32-only; skipped unless --with-input. -INPUT_ONLY=(tooltipProfile textClick) +# Tests whose input path is Win32-only; skipped unless --with-input. Derived from +# the companion scripts actually on disk rather than named here, because a +# hardcoded list goes stale the moment a suite gains one -- which it had: six +# input-driven suites were running without a driver and reporting their clicks as +# engine failures. +INPUT_ONLY=() +for f in "$SCRIPT_DIR/smoke"/*.input.ps1; do + [[ -e "$f" ]] || continue + INPUT_ONLY+=("$(basename "$f" .input.ps1)") +done contains() { local n="$1"; shift; for e in "$@"; do [[ "$e" == "$n" ]] && return 0; done; return 1; } From 34680198e767fbd28f18626a6f0ffe51fde97219 Mon Sep 17 00:00:00 2001 From: Greenfire27 Date: Fri, 7 Aug 2026 22:54:47 -0400 Subject: [PATCH 54/55] Platform (macOS): a file name with no directory names no directory Platform::createPath took the last '/' in the path and terminated the string after it, to drop the file name from the end. A bare file name in the working directory has no '/' at all, so dStrrchr returned NULL and the write went to address 0x1. Windows and Linux both walk the path with dStrchr and so simply do nothing when there is no separator to find; say the same thing here and return, since a path with no directory component has none to create. This crashed the unit suite at test 65 of 199: the profile theme tests round-trip a TAML file named in the working directory. Co-Authored-By: Claude Opus 5 (1M context) --- engine/source/platformOSX/osxFileIO.mm | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/engine/source/platformOSX/osxFileIO.mm b/engine/source/platformOSX/osxFileIO.mm index 2d7571cdc..a33f9105a 100755 --- a/engine/source/platformOSX/osxFileIO.mm +++ b/engine/source/platformOSX/osxFileIO.mm @@ -389,7 +389,7 @@ static void recurseDumpPath(const char* curPath, Vector& fil if ( Ok == currentStatus || EOS == currentStatus ) { struct stat statData; - + if(fstat(fileno((FILE*)handle), &statData) != 0) return 0; @@ -687,6 +687,12 @@ static void recurseDumpPath(const char* curPath, Vector& fil return false; } char* pFinalSlash = dStrrchr(pathBuffer, '/'); + if ( pFinalSlash == NULL ) + { + // A bare file name in the working directory names no directory, so + // there is nothing to create. Windows and Linux both no-op here. + return true; + } if ( pFinalSlash != pathBuffer+pathLength-1 ) { pFinalSlash[1] = 0; From c58193f57011cd74e2862558b0f1620e3429cd2a Mon Sep 17 00:00:00 2001 From: Greenfire27 Date: Fri, 7 Aug 2026 22:55:05 -0400 Subject: [PATCH 55/55] Platform (macOS, iOS): a file's size counts the bytes not yet written out These two are the only platforms whose File holds buffered stdio rather than an unbuffered OS handle: Windows keeps a HANDLE and asks GetFileSize, Linux keeps a descriptor and seeks to the end, and neither can see a size the file does not yet have. Here getSize fstat'd the inode, which knows nothing of what is still sitting in the FILE* buffer, so a file just written to reported a size of zero. Flush before asking. Only when the file can be written to -- a read-only file has nothing outstanding, which leaves the engine's ordinary reading of scripts and assets untouched -- and the one caller that asks often, setPosition, has already flushed by way of its own fseek, so it finds an empty buffer and pays nothing. FileStream::getStreamSize already compensates this way for its own buffer, taking the greater of the file's size and its dirty tail; the stdio buffer beneath it had no such cover. The one place the stale answer reached persisted data is ZipArchive::updateFile, which records getStreamSize as a zip entry's compressed size while the temp stream is still open (that path is now closed, though a short size was never observed in the wild). Fixes PlatformFileIOTests.FileWriteRead, which wrote 21 bytes, agreed the position was 21, and was told the size was 0. The iOS copy is the same edit to identical code; it is unbuilt, as this machine has only the Command Line Tools. Co-Authored-By: Claude Opus 5 (1M context) --- engine/source/platformOSX/osxFileIO.mm | 10 ++++++++++ engine/source/platformiOS/iOSFileio.mm | 12 +++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/engine/source/platformOSX/osxFileIO.mm b/engine/source/platformOSX/osxFileIO.mm index a33f9105a..050212fa4 100755 --- a/engine/source/platformOSX/osxFileIO.mm +++ b/engine/source/platformOSX/osxFileIO.mm @@ -390,6 +390,16 @@ static void recurseDumpPath(const char* curPath, Vector& fil { struct stat statData; + // The handle is buffered stdio, so bytes just written may still be in + // that buffer and not yet in the inode fstat reports. Windows and Linux + // both hold an unbuffered handle and so never see a stale size; push the + // buffer out first so this answers with the same authority they do. A + // read-only file has nothing to push, and the one hot caller + // (setPosition) has already flushed by way of its own fseek, so this + // costs nothing on the paths that ask most often. + if ( hasCapability(FileWrite) ) + fflush((FILE*)handle); + if(fstat(fileno((FILE*)handle), &statData) != 0) return 0; diff --git a/engine/source/platformiOS/iOSFileio.mm b/engine/source/platformiOS/iOSFileio.mm index 96223a97a..ac9b44a6e 100755 --- a/engine/source/platformiOS/iOSFileio.mm +++ b/engine/source/platformiOS/iOSFileio.mm @@ -244,7 +244,17 @@ bool dFileTouch(const char *path) if (Ok == currentStatus || EOS == currentStatus) { struct stat statData; - + + // The handle is buffered stdio, so bytes just written may still be in + // that buffer and not yet in the inode fstat reports. Windows and Linux + // both hold an unbuffered handle and so never see a stale size; push the + // buffer out first so this answers with the same authority they do. A + // read-only file has nothing to push, and the one hot caller + // (setPosition) has already flushed by way of its own fseek, so this + // costs nothing on the paths that ask most often. + if ( hasCapability(FileWrite) ) + fflush((FILE*)handle); + if(fstat(fileno((FILE*)handle), &statData) != 0) return 0;

    KrTptp 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

KrTptp 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