acdream/src/AcDream.App/UI/Layout/CharacterManagementUiController.cs
Erik 997b720455 fix(test): UI probe pointer commands convert canvas to window coordinates; char-select Enter logs its outcome
The automation probe fed element-center CANVAS coordinates straight into
UiRoot.OnMouseMove/Down/Up, which take WINDOW coordinates and map
window->canvas internally (UiRoot.MapWindowToCanvas). The two spaces are
identical on every screen without UiRoot.FixedCanvasSize — every prior
probe gate passed — but the character-select/chargen screens stretch an
authored 800x600 canvas across the window, so every synthetic click and
hover landed at canvas*(canvas/window): nowhere near the target. The
enter-world connected gate's 'click element 0x100003A2' silently did
nothing for two full rounds. Element-derived pointer paths (ClickAt,
DragAt, HoverElement) now convert canvas->window via UiRoot.CanvasScale;
raw 'mousemove x y' stays a passthrough.

CharacterManagementUiController.EnterSelected also logs a once-per-click
outcome line ('[UI] character enter accepted/rejected status=...') — a
refused Enter was previously indistinguishable from a click that never
dispatched (both silent), which cost a connected-gate round to tell
apart.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 10:52:13 +02:00

835 lines
28 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Numerics;
using AcDream.Runtime;
using AcDream.Runtime.Session;
namespace AcDream.App.UI.Layout;
/// <summary>
/// Projects Runtime's one borrowed pre-world character-selection owner through
/// retail <c>gmCharacterManagementUI</c>'s authored retained layout. The list is
/// intentionally flat: the retail class owns no viewport or model preview.
/// </summary>
internal sealed class CharacterManagementUiController : IDisposable
{
internal const uint RootEnum = 0x10000005u;
internal const uint RootElementId = 0x1000039Au;
internal const uint WorldTextElementId = 0x1000039Bu;
internal const uint ListElementId = 0x1000039Du;
internal const uint CreateElementId = 0x100003A0u;
internal const uint EnterElementId = 0x100003A2u;
internal const uint DeleteElementId = 0x1000039Fu;
internal const uint RestoreElementId = 0x1000039Eu;
/// <summary>
/// gmCharacterManagementUI::ListenToElementMessage@0x004ed5a0's element-id
/// switch is keyed off <c>idElement - 0x1000039d</c> (the listbox base);
/// offset 6 -> QueueUIMode(0x10000005), the mode gmCreditsUI registers
/// (Register@0x0047a69e) — out of scope this round (finding 1 note).
/// </summary>
internal const uint CreditsElementId = 0x100003A3u;
/// <summary>Offset 7 from the listbox base -> MakeConfirmExitDialog@0x004ed250.</summary>
internal const uint ExitElementId = 0x100003A4u;
internal sealed record DialogStrings(
Func<string, string> DeleteConfirmation,
string DeleteResponse,
string PleaseWait,
string EnteringWorld,
/// <summary>
/// Retail <c>ID_CharacterManagement_ConfirmExit</c> (table
/// <c>0x23000002</c>) — "Are you sure you want to leave?", the text
/// <c>MakeConfirmExitDialog@0x004ed250</c> resolves via
/// <c>StringInfo::SetStringIDandTableEnum(compute_str_hash(
/// "ID_CharacterManagement_ConfirmExit"), 0x10000002)</c>.
/// </summary>
string ConfirmExit);
private readonly UiRoot _host;
private readonly ImportedLayout _layout;
private readonly UiText _worldText;
private readonly UiTemplateListBox _list;
private readonly UiButton _create;
private readonly UiButton _enter;
private readonly UiButton _delete;
private readonly UiButton _restore;
private readonly UiButton _credits;
private readonly UiButton _exit;
private readonly RetailDialogFactory _dialogs;
private readonly CharacterSelectionRuntimeBindings _bindings;
private readonly DialogStrings _strings;
private readonly List<UiButton> _rows = [];
private readonly Dictionary<UiButton, uint> _rowIds = [];
private Vector2 _authoredCanvas;
private RuntimeGenerationToken _lastGeneration;
private long _lastRevision = long.MinValue;
private string _lastWorldName = string.Empty;
private uint _deleteDialogContext;
private uint _operationWaitContext;
private uint _enterWaitContext;
private uint _errorDialogContext;
private uint _confirmExitDialogContext;
private bool _active;
private bool _restoreCommandInFlight;
private bool _suppressDialogCallbacks;
private bool _disposed;
private CharacterManagementUiController(
UiRoot host,
ImportedLayout layout,
UiText worldText,
UiTemplateListBox list,
UiButton create,
UiButton enter,
UiButton delete,
UiButton restore,
UiButton credits,
UiButton exit,
RetailDialogFactory dialogs,
CharacterSelectionRuntimeBindings bindings,
DialogStrings strings)
{
_host = host;
_layout = layout;
_worldText = worldText;
_list = list;
_create = create;
_enter = enter;
_delete = delete;
_restore = restore;
_credits = credits;
_exit = exit;
_dialogs = dialogs;
_bindings = bindings;
_strings = strings;
Root.Left = 0f;
Root.Top = 0f;
Root.ClickThrough = false;
Root.Visible = false;
// Campaign LA gate round 2 (register AD-98): the root KEEPS its authored
// 800×600 extent — retail never resizes it (zero edge anchors, verified
// against the installed DAT) and its blitter has no stretch mode; the
// whole composed screen stretches once at presentation. Our equivalent:
// while this screen is active, the host stretches the ENTIRE canvas —
// widgets, glyphs, and the painted background (which carries the
// "World"/"Characters" captions as art) — as one unit via
// UiRoot.FixedCanvasSize, declared/revoked through the owner-scoped
// arbiter (review fix round R1, 2026-08-15) rather than written
// directly — character-creation can be simultaneously active on top
// of this screen, and a raw write from either controller is a last-
// writer-wins race with no owner. Resizing the root here instead of
// using the canvas is exactly the half-substitution that misaligned
// the widgets against the stretched art at the 2026-08-15 user gate.
_authoredCanvas = new Vector2(
Root.Width > 0f ? Root.Width : 800f,
Root.Height > 0f ? Root.Height : 600f);
// Campaign CC slice CC7: gmCharacterManagementUI::ListenToElementMessage
// @ 0x004ed5a0 case 3 dispatches Create unconditionally on click
// (QueueUIMode(0x1000000b) — no gate at click time); the gate lives
// entirely in UpdateButtons @ 0x004ec240's own Enabled/ghosted state
// (see ApplyButtons below), so the click handler is wired once here
// and Enabled tracks the borrowed snapshot every tick. Starts
// disabled/ghosted until the first real snapshot arrives.
_create.Visible = true;
_create.Enabled = false;
_create.OnClick = RequestCreate;
_enter.OnClick = EnterSelected;
_delete.OnClick = RequestDelete;
_restore.OnClick = RestoreSelected;
// Credits (retail QueueUIMode(0x10000005) -> gmCreditsUI) is out of
// scope this round (finding 1 note) — same "future campaign, visibly
// ghosted, no invented action" treatment as Create above. Filed as
// issue #400.
_credits.Visible = true;
_credits.Enabled = false;
_credits.OnClick = null;
_exit.OnClick = RequestExit;
// World name (retail UpdateWorldName@0x004ec120 /
// RecvNotice_WorldName@0x004ec360 both just push
// Client::GetWorldName() onto this element). LinesProvider reads the
// live field Tick() updates each time Runtime's snapshot changes.
_worldText.LinesProvider =
() => [new UiText.Line(_lastWorldName, _worldText.DefaultColor)];
}
internal UiElement Root => _layout.Root;
internal IReadOnlyList<UiButton> Rows => _rows;
internal uint DeleteDialogContext => _deleteDialogContext;
internal uint OperationWaitContext => _operationWaitContext;
internal uint EnterWaitContext => _enterWaitContext;
internal uint ErrorDialogContext => _errorDialogContext;
internal uint ConfirmExitDialogContext => _confirmExitDialogContext;
internal void ResetSession()
{
if (_disposed)
return;
Deactivate();
_lastRevision = long.MinValue;
}
internal static CharacterManagementUiController? Bind(
UiRoot host,
ImportedLayout layout,
Func<uint, uint, UiElement?> templateResolver,
RetailDialogFactory dialogs,
CharacterSelectionRuntimeBindings bindings,
DialogStrings strings)
{
CharacterManagementUiController? controller = CreateDetached(
host,
layout,
templateResolver,
dialogs,
bindings,
strings);
if (controller is null)
return null;
try
{
controller.AttachAndTick();
return controller;
}
catch
{
controller.Dispose();
throw;
}
}
internal static CharacterManagementUiController? CreateDetached(
UiRoot host,
ImportedLayout layout,
Func<uint, uint, UiElement?> templateResolver,
RetailDialogFactory dialogs,
CharacterSelectionRuntimeBindings bindings,
DialogStrings strings)
{
ArgumentNullException.ThrowIfNull(host);
ArgumentNullException.ThrowIfNull(layout);
ArgumentNullException.ThrowIfNull(templateResolver);
ArgumentNullException.ThrowIfNull(dialogs);
ArgumentNullException.ThrowIfNull(bindings);
ArgumentNullException.ThrowIfNull(strings);
if (ContainsViewport(layout.Root))
{
Console.WriteLine(
"[UI] character management: refusing an unapproved model-preview viewport.");
return null;
}
if (layout.Root.DatElementId != RootElementId
|| layout.FindElement(WorldTextElementId) is not UiText worldText
|| layout.FindElement(ListElementId) is not UiTemplateListBox list
|| layout.FindElement(CreateElementId) is not UiButton create
|| layout.FindElement(EnterElementId) is not UiButton enter
|| layout.FindElement(DeleteElementId) is not UiButton delete
|| layout.FindElement(RestoreElementId) is not UiButton restore
|| layout.FindElement(CreditsElementId) is not UiButton credits
|| layout.FindElement(ExitElementId) is not UiButton exit)
{
Console.WriteLine(
"[UI] character management: the authored root/list/button contract is incomplete.");
return null;
}
list.TemplateResolver = templateResolver;
try
{
return new CharacterManagementUiController(
host,
layout,
worldText,
list,
create,
enter,
delete,
restore,
credits,
exit,
dialogs,
bindings,
strings);
}
catch
{
list.TemplateResolver = null;
create.OnClick = null;
enter.OnClick = null;
delete.OnClick = null;
restore.OnClick = null;
exit.OnClick = null;
throw;
}
}
internal void AttachAndTick()
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (Root.Parent is null)
_host.AddChild(Root);
Tick();
}
private static bool ContainsViewport(UiElement element)
{
if (element is UiViewport)
return true;
foreach (UiElement child in element.Children)
if (ContainsViewport(child))
return true;
return false;
}
internal void Tick()
{
if (_disposed)
return;
IRuntimeCharacterSelectionView? view = _bindings.View();
RuntimeCharacterSelectionSnapshot snapshot = view?.Snapshot ?? default;
if (view is null || !snapshot.IsActive)
{
Deactivate();
_lastGeneration = snapshot.Generation;
_lastRevision = snapshot.Revision;
return;
}
if (!_active)
{
_active = true;
Root.Visible = true;
_host.DeclareFixedCanvas(this, _authoredCanvas);
_host.BringToFront(Root);
}
// World name rides independently of the roster revision gate below —
// ServerName can arrive slightly before or after CharacterList (see
// RuntimeCharacterSelectionState.ApplyWorldName).
_lastWorldName = snapshot.WorldName;
if (_lastGeneration != snapshot.Generation
|| _lastRevision != snapshot.Revision)
{
if (TryCaptureRoster(view, snapshot, out RuntimeCharacterSelectionEntry[] roster))
{
bool rowsReady;
if (RowsMatchRoster(roster, snapshot.SlotCount))
{
ApplyHighlight(snapshot.HighlightedCharacterId);
rowsReady = true;
}
else
{
rowsReady = RebuildRows(
roster,
snapshot.SlotCount,
snapshot.HighlightedCharacterId);
}
if (rowsReady)
{
_lastGeneration = snapshot.Generation;
_lastRevision = snapshot.Revision;
}
}
else
{
// A receive-thread roster/reset raced the borrowed snapshot.
// Leave the revision unconsumed so the next frame retries from
// one coherent view; never present a partially mixed roster.
_lastRevision = long.MinValue;
snapshot = view.Snapshot;
if (!snapshot.IsActive)
{
Deactivate();
_lastGeneration = snapshot.Generation;
_lastRevision = snapshot.Revision;
return;
}
}
}
else
{
ApplyHighlight(snapshot.HighlightedCharacterId);
}
ApplyButtons(snapshot.Buttons);
ReconcileDialogs(view, snapshot);
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
try
{
CloseAllDialogs(suppressCallbacks: true);
}
finally
{
_host.RevokeFixedCanvas(this);
_enter.OnClick = null;
_delete.OnClick = null;
_restore.OnClick = null;
_exit.OnClick = null;
foreach (UiButton row in _rows)
{
row.OnClick = null;
row.OnDoubleClick = null;
}
_rows.Clear();
_rowIds.Clear();
_list.Flush();
_list.TemplateResolver = null;
_host.RemoveChild(Root);
}
}
private static bool TryCaptureRoster(
IRuntimeCharacterSelectionView view,
RuntimeCharacterSelectionSnapshot expected,
out RuntimeCharacterSelectionEntry[] roster)
{
roster = new RuntimeCharacterSelectionEntry[expected.RosterCount];
for (int i = 0; i < roster.Length; i++)
{
if (!view.TryGetAt(i, out roster[i]))
return false;
}
RuntimeCharacterSelectionSnapshot after = view.Snapshot;
return after.Generation == expected.Generation
&& after.Revision == expected.Revision
&& after.RosterCount == expected.RosterCount;
}
private bool RowsMatchRoster(
IReadOnlyList<RuntimeCharacterSelectionEntry> roster,
int allowedSlotCount)
{
if (_rows.Count != roster.Count)
return false;
int rowHeight = ComputeRowHeight(
_list.Height,
roster.Count,
allowedSlotCount);
for (int i = 0; i < roster.Count; i++)
{
UiButton row = _rows[i];
RuntimeCharacterSelectionEntry character = roster[i];
if (!_rowIds.TryGetValue(row, out uint characterId)
|| characterId != character.CharacterId
|| !string.Equals(row.Label, character.Name, StringComparison.Ordinal)
|| (int)row.Height != rowHeight
|| row.LabelColor != (character.IsPendingDelete
? new Vector4(1f, 0f, 0f, 1f)
: Vector4.One))
{
return false;
}
}
return true;
}
private bool RebuildRows(
IReadOnlyList<RuntimeCharacterSelectionEntry> roster,
int allowedSlotCount,
uint highlightedCharacterId)
{
foreach (UiButton row in _rows)
{
row.OnClick = null;
row.OnDoubleClick = null;
}
_rows.Clear();
_rowIds.Clear();
_list.Flush();
int rowHeight = ComputeRowHeight(
_list.Height,
roster.Count,
allowedSlotCount);
_list.LineHeight = rowHeight;
bool complete = _list.Templates.Count > 0
&& _list.TemplateResolver is not null;
foreach (RuntimeCharacterSelectionEntry character in roster)
{
if (!complete)
break;
UiTemplateListEntry template = _list.Templates[0];
if (_list.TemplateResolver!(
template.TemplateLayoutId,
template.TemplateElementId) is not UiButton row)
{
complete = false;
break;
}
// AddItemFromTemplateList creates the same template, but its
// retained viewport stacks at the template's authored 16px
// height. Retail establishes the computed size on every row; our
// list fixes Top during insertion, so build and resize first to
// make every subsequent Top exact.
row.Height = rowHeight;
_list.AddPrebuiltRow(row);
uint characterId = character.CharacterId;
row.Label = character.Name;
row.LabelColor = character.IsPendingDelete
? new Vector4(1f, 0f, 0f, 1f)
: Vector4.One;
row.Enabled = true;
row.SuppressSelfToggle = true;
row.Selected = characterId == highlightedCharacterId;
row.OnClick = () => Highlight(characterId);
row.OnDoubleClick = EnterSelected;
_rows.Add(row);
_rowIds.Add(row, characterId);
}
if (complete)
return true;
foreach (UiButton row in _rows)
{
row.OnClick = null;
row.OnDoubleClick = null;
}
_rows.Clear();
_rowIds.Clear();
_list.Flush();
_lastRevision = long.MinValue;
return false;
}
internal static int ComputeRowHeight(
float listHeight,
int rosterCount,
int allowedSlotCount)
{
// RebuildCharacterList @ 0x004EC3A0 uses integer UIRegion height and
// signed integer division for both terms. The 0x66666667 multiply/
// shift sequence is compiler output for height / 10.
int height = (int)MathF.Truncate(listHeight);
int denominator = Math.Max(rosterCount, allowedSlotCount);
if (denominator <= 0)
return height / 10;
return Math.Max(height / denominator, height / 10);
}
private void ApplyHighlight(uint highlightedCharacterId)
{
foreach (UiButton row in _rows)
row.Selected = _rowIds.TryGetValue(row, out uint characterId)
&& characterId == highlightedCharacterId;
}
private void ApplyButtons(RuntimeCharacterSelectionButtons buttons)
{
_create.Visible = true;
_create.Enabled = buttons.CanCreate;
_enter.Enabled = buttons.CanEnter;
_delete.Visible = buttons.DeleteVisible;
_delete.Enabled = buttons.CanDelete;
_restore.Visible = buttons.RestoreVisible;
_restore.Enabled = buttons.CanRestore;
}
private void Highlight(uint characterId)
{
if (_disposed)
return;
_bindings.Highlight(characterId);
InvalidateAndTick();
}
/// <summary>
/// Campaign CC slice CC7: <c>ListenToElementMessage</c> case 3 ->
/// <c>QueueUIMode(0x1000000b)</c>. Purely presentational — no Runtime
/// command, no roster/state change here; <see cref="_bindings"/>'
/// <c>RequestCreate</c> is resolved per-call (never captured) so it
/// reflects whatever <see cref="RetailUiRuntime"/> wired at the time of
/// the click, matching every other late-bound seam in this bindings
/// record.
/// </summary>
private void RequestCreate()
{
if (_disposed)
return;
_bindings.RequestCreate?.Invoke();
}
private void EnterSelected()
{
if (_disposed)
return;
// Open retail's wait context before the synchronous Runtime command
// starts its existing ServerReady transaction. The state projection
// remains authoritative and closes it on InWorld/error/reset.
EnsureEnterWait();
RuntimeCommandResult result = _bindings.Enter();
// Enter-world round (2026-08-17): once-per-click outcome line — a
// refused Enter was previously indistinguishable from a click that
// never dispatched (both silent), which cost a full connected-gate
// round to tell apart.
Console.WriteLine(result.Accepted
? "[UI] character enter accepted"
: $"[UI] character enter rejected status={result.Status}");
if (!result.Accepted)
CloseContext(ref _enterWaitContext, suppressCallback: true);
InvalidateAndTick();
}
private void RequestDelete()
{
if (_disposed)
return;
_bindings.RequestDelete();
InvalidateAndTick();
}
private void RestoreSelected()
{
if (_disposed)
return;
// ListenToElementMessage @ 0x004ED5A0 opens Please Wait before it
// calls CPlayerSystem::RestoreCharacter. Keep it modal even if a
// synchronous command callback re-enters Tick before Runtime has
// returned its accepted projection.
EnsureOperationWait();
RuntimeCommandResult result = default;
Exception? failure = null;
_restoreCommandInFlight = true;
try
{
result = _bindings.Restore();
}
catch (Exception error)
{
failure = error;
}
finally
{
_restoreCommandInFlight = false;
}
if (failure is not null)
{
Console.WriteLine(
$"[UI] character restore command failed: {failure.Message}");
CloseContext(ref _operationWaitContext, suppressCallback: true);
InvalidateAndTick();
return;
}
if (!result.Accepted)
CloseContext(ref _operationWaitContext, suppressCallback: true);
InvalidateAndTick();
}
private void RequestExit()
{
if (_disposed)
return;
// MakeConfirmExitDialog @ 0x004ed250's own guard: a second Exit
// click while the confirmation is already open is a no-op.
if (_confirmExitDialogContext != 0u)
return;
_confirmExitDialogContext = _dialogs.MakeConfirmation(
_strings.ConfirmExit,
data =>
{
_confirmExitDialogContext = 0u;
if (_disposed || _suppressDialogCallbacks)
return;
// RecvNotice_CloseDialog @ 0x004ed760 case 1: only a
// confirmed (OK) close proceeds through the SAME graceful
// shutdown path window-close uses; Cancel leaves the screen
// exactly as it was.
if (data.GetBoolean(RetailDialogProperty.ConfirmationResult))
_bindings.RequestExit();
});
}
private void ReconcileDialogs(
IRuntimeCharacterSelectionView view,
RuntimeCharacterSelectionSnapshot snapshot)
{
if (snapshot.Error is { } error)
{
CloseContext(ref _deleteDialogContext, suppressCallback: true);
CloseContext(ref _operationWaitContext, suppressCallback: true);
CloseContext(ref _enterWaitContext, suppressCallback: true);
EnsureError(error.Message);
return;
}
CloseContext(ref _errorDialogContext, suppressCallback: true);
if (snapshot.Lifecycle == RuntimeCharacterSelectionLifecycle.EnteringWorld)
{
CloseContext(ref _deleteDialogContext, suppressCallback: true);
CloseContext(ref _operationWaitContext, suppressCallback: true);
EnsureEnterWait();
return;
}
CloseContext(ref _enterWaitContext, suppressCallback: true);
if (snapshot.PendingDeleteCharacterId != 0u
&& view.TryGet(snapshot.PendingDeleteCharacterId, out RuntimeCharacterSelectionEntry pending))
{
EnsureDeleteConfirmation(pending.Name);
}
else
{
CloseContext(ref _deleteDialogContext, suppressCallback: true);
}
if (_restoreCommandInFlight
|| snapshot.Operation is RuntimeCharacterSelectionOperation.DeleteRequested
or RuntimeCharacterSelectionOperation.DeleteAcknowledged
or RuntimeCharacterSelectionOperation.RestoreRequested)
{
EnsureOperationWait();
}
else
{
CloseContext(ref _operationWaitContext, suppressCallback: true);
}
}
private void EnsureDeleteConfirmation(string characterName)
{
if (_deleteDialogContext != 0u)
return;
_deleteDialogContext = _dialogs.MakeConfirmationTextInput(
_strings.DeleteConfirmation(characterName),
data =>
{
_deleteDialogContext = 0u;
if (_disposed || _suppressDialogCallbacks)
return;
string response = data.GetString(
RetailDialogProperty.TextInputResult) ?? string.Empty;
if (string.Equals(
response,
_strings.DeleteResponse,
StringComparison.OrdinalIgnoreCase))
{
_bindings.ConfirmDelete();
}
else
{
_bindings.Cancel();
}
InvalidateAndTick();
});
}
private void EnsureOperationWait()
{
if (_operationWaitContext == 0u)
_operationWaitContext = _dialogs.MakeWait(_strings.PleaseWait);
}
private void EnsureEnterWait()
{
if (_enterWaitContext == 0u)
_enterWaitContext = _dialogs.MakeWait(_strings.EnteringWorld);
}
private void EnsureError(string message)
{
if (_errorDialogContext != 0u)
return;
_errorDialogContext = _dialogs.MakeMessage(
message,
_ =>
{
_errorDialogContext = 0u;
if (_disposed || _suppressDialogCallbacks)
return;
_bindings.Cancel();
InvalidateAndTick();
});
}
private void InvalidateAndTick()
{
_lastRevision = long.MinValue;
Tick();
}
private void Deactivate()
{
if (_active)
{
_active = false;
Root.Visible = false;
_host.RevokeFixedCanvas(this);
}
foreach (UiButton row in _rows)
{
row.OnClick = null;
row.OnDoubleClick = null;
}
_rows.Clear();
_rowIds.Clear();
_list.Flush();
CloseAllDialogs(suppressCallbacks: true);
}
private void CloseAllDialogs(bool suppressCallbacks)
{
bool previous = _suppressDialogCallbacks;
_suppressDialogCallbacks |= suppressCallbacks;
try
{
CloseContext(ref _deleteDialogContext, suppressCallback: false);
CloseContext(ref _operationWaitContext, suppressCallback: false);
CloseContext(ref _enterWaitContext, suppressCallback: false);
CloseContext(ref _errorDialogContext, suppressCallback: false);
CloseContext(ref _confirmExitDialogContext, suppressCallback: false);
}
finally
{
_suppressDialogCallbacks = previous;
}
}
private void CloseContext(ref uint context, bool suppressCallback)
{
uint closing = context;
if (closing == 0u)
return;
context = 0u;
bool previous = _suppressDialogCallbacks;
_suppressDialogCallbacks |= suppressCallback;
try
{
_dialogs.CloseDialog(closing);
}
finally
{
_suppressDialogCallbacks = previous;
}
}
}