diff --git a/.github/Feature-Prompts/Net11/01-B05-HighPrecisionTimer-Rework-WorkOrder.md b/.github/Feature-Prompts/Net11/01-B05-HighPrecisionTimer-Rework-WorkOrder.md new file mode 100644 index 00000000000..470d3584b5b --- /dev/null +++ b/.github/Feature-Prompts/Net11/01-B05-HighPrecisionTimer-Rework-WorkOrder.md @@ -0,0 +1,157 @@ +# Work Order: HighPrecisionTimer Rework (Animation Timing) + +**Branch:** `Net11/Integration-2` (KlausLoeffelmann/winforms) +**Files:** +- `src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Animation/HighPrecisionTimer.cs` +- `src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Animation/HighPrecisionTimerTick.cs` +- `src/System.Windows.Forms.Primitives/tests/UnitTests/System/Windows/Forms/Animation/HighPrecisionTimerTests.cs` +- Consumer: `src/System.Windows.Forms/System/Windows/Forms/Rendering/Animation/AnimationManager.cs` + +This work order is self-contained; it results from a code review of the current implementation. +Read the current sources first, verify each finding against the code as it stands (the branch may have +moved), then implement. + +**Keep as-is (explicitly not up for redesign):** the registration model — per-registration +`SynchronizationContext` capture, `InFlight` CAS-based frame coalescing with a `DroppedFrames` counter +surfaced in `HighPrecisionTimerTick`, and the id-based `TimerRegistration` disposable struct +(double-dispose safe, `default` safe). The *clock/pacing core* is what gets replaced. + +--- + +## Finding 1 (critical): fixed-cadence pacer + spin causes a sawtooth burning ~50% of a core + +Current design: `PeriodicTimer` at 14 ms (60 Hz path), then `SpinToTarget` spins to the 16.667 ms +frame target with `SpinOnce(sleep1Threshold: -1)` (never sleeps). + +`PeriodicTimer` fires on its own **fixed** cadence (14, 28, 42, 56, …) and does not re-phase per +`WaitForNextTickAsync` call, while frame targets are 16.67, 33.33, 50, 66.67, … The phase slips +2.67 ms per frame, so spin duration grows every frame — wake 28 → spin 5.3 ms; wake 42 → spin 8 ms; +wake 56 → spin 10.7 ms — until phases wrap and the sawtooth restarts. Average spin ≈ half a frame +≈ 8 ms of every 16.67 ms ⇒ ~50% of one core, continuously, for as long as **any** animation is +registered. Infinite cycles (e.g. a pulsing focus indicator) make this permanent. The 30 Hz path +(30 ms tick vs 33.33 ms frame) has the identical slip. + +**Required fix (architecture, not constant-tuning):** absolute schedule. Due times are +`epoch + frameIndex * period` against a single clock; wait on a mechanism that can hit them +(Finding 3), with at most a sub-millisecond residual spin. Overshoot must be amortized against the +absolute schedule (no `lastTick + period` relative scheduling — that accumulates and runs slow). + +## Finding 2: integer-millisecond arithmetic throughout + +`stopwatch.ElapsedMilliseconds` (truncated `long`) feeds `lastTickTimestamp`, `elapsed`, the spin +target, drift detection, and the `Timestamp`/`Elapsed` values delivered to consumers — ±1 ms +quantization (6% of a 16.667 ms budget) plus systematic truncation bias. Use `ElapsedTicks` / +`Elapsed.TotalMilliseconds` (double) end to end; `HighPrecisionTimerTick` fields stay `TimeSpan`, +constructed from ticks. + +## Finding 3: replace `timeBeginPeriod(1)` with a high-resolution waitable timer + +The code gates on `windows10.0.17134` — which `timeBeginPeriod` (ancient) does not need, but which is +exactly the build (1803) that introduced `CreateWaitableTimerExW` with +`CREATE_WAITABLE_TIMER_HIGH_RESOLUTION`. Use it: + +- absolute due times (negative-relative or absolute FILETIME) with sub-ms accuracy, +- no process-wide timer-resolution raise (current code holds 1 ms resolution for the entire lifetime + of any animation — a documented power/battery anti-pattern, and post-Win11 the effective resolution + changes for occluded windows, silently shifting the timing floor), +- composes directly with Finding 1's absolute schedule and eliminates the spin loop almost entirely. + +Fallback below 17134: keep a coarse path (30 Hz, plain waits, no `timeBeginPeriod`) — document that +sub-frame precision is not attempted there. Remove `TimeBeginPeriod`/`TimeEndPeriod` P/Invokes if no +longer referenced. + +## Finding 4: `Register`/`Unregister` vs `StopTimer` race strands registrations + +`Unregister` does `TryRemove` → `IsEmpty?` → `StopTimer()` without coordinating with `Register`. +Interleaving: A removes the last entry and observes empty; B adds a registration and `EnsureRunning` +sees `s_loopTask != null` (still running) and returns; A stops the timer. Result: live registration, +dead timer — animation frozen until an unrelated `Register` restarts the loop. +**Fix:** perform the emptiness check + stop decision under `s_lock` together with loop-state +transitions, or introduce a generation counter that `StopTimer` validates before actually stopping. + +## Finding 5: per-frame, per-registration closure allocations on the hot path + +`SyncContext.Post(_ => _ = InvokeCallbackAsync(registration, tick, cancellationToken), null)` +allocates closure + delegate per registration per frame (60 Hz × N renderers of steady GC pressure). +**Fix:** one cached `static SendOrPostCallback`; pass state via a per-registration state object — +`InFlight` guarantees exclusivity, so tick data can be written into a reusable per-registration slot +before posting. Target: zero allocations per frame in steady state. + +## Finding 6: drift `Debug.Assert` is an assert storm + +Drift >20% for 10 frames is normal under a debugger, breakpoints, or CI load. Once tripped, +`consecutiveDriftFrames` keeps incrementing, so the assert fires **every subsequent frame**. +**Fix:** replace with tracing/EventSource counters (drift, dropped frames, spin time). If any assert +remains, reset the counter after firing once. + +## Finding 7: lifecycle edges + +- `StopTimer` never observes `s_loopTask`; a stop/start pair can transiently run two loops, and a + stopping loop can dispatch one final frame with a canceled token. Decide and document: either join + the old loop (bounded) or make late dispatch provably benign. +- Post-unregister ticks can still be in flight toward a disposed consumer; `AnimationManager` / + `AnimatedControlRenderer` must tolerate late callbacks — add a test. +- `Reset()` (test hook) mutates state without locking — document the serialization requirement or + lock it. +- Dead code: `Registration.Id` is never read. +- `InvokeCallbackAsync` swallows non-OCE exceptions with `Debug.Fail` and keeps invoking the same + callback forever; consider auto-unregistering a registration after N consecutive faults. + +## Finding 8: single-SyncContext funnel in `AnimationManager` (design note) + +`HighPrecisionTimer` correctly captures a `SynchronizationContext` **per registration**, but +`AnimationManager` is a process-wide singleton with a single registration, so every animation in a +multi-message-loop application marshals to the first UI thread — and dies with it. Minimum: document +the constraint. Better: make the manager per-UI-thread (e.g. `[ThreadStatic]` instance keyed off the +message-loop thread), preserving one timer-registration-per-UI-thread. Also note the duplicate +timeline: the manager keeps its own `Stopwatch` instead of deriving progress from +`HighPrecisionTimerTick.Timestamp/Elapsed`; animation progress should use the tick's timeline so frame +coalescing (`DroppedFrames`) is accounted for consistently. + +## Finding 9: 60 Hz is a settled ceiling; the period is a pacer-owned runtime value for *downshift* + +**Decision (do not relitigate):** the timer targets a hard 60 Hz ceiling (30 Hz fallback). Do not +add refresh-rate matching or raise the cadence. Rationale, for the record: + +- Under DWM, windowed GDI/GDI+ apps do not tear (composition is tear-free from the redirection + surface); the only artifact of an unsynced 60 Hz timer is judder, which is imperceptible for this + content class (focus pulses, hover fades, toggle transitions — not motion/scrolling). +- GDI+ raster cost scales linearly with rate; driving N animated controls at 120–144 Hz multiplies + compute for no perceptible gain (e.g. batched MVVM-driven updates across many controls). +- A process-wide timer cannot refresh-match on mixed-rate multi-monitor setups ("the" refresh rate + is ill-defined), and `DwmFlush`-style vblank pacing blocks per frame, binds to one monitor, and + misbehaves under RDP — unfit for a process-wide UI timer by construction. + +**Required now (structural):** the frame period must be a runtime value owned by the pacer +(queryable/settable internally), not compile-time constants woven through the loop. The 30 Hz +fallback already makes the period variable; the forward-looking motivation is **downshifting**, not +matching: future power-driven reductions (30 Hz or full pause for occluded/minimized windows — where +Windows 11 timer coalescing already alters the effective cadence — and battery-saver scenarios) +must be addable without another rework. + +--- + +## Acceptance criteria + +1. Steady-state CPU of the timer loop with one registered infinite animation: **< 2% of one core** + (measure; the current implementation is the ~50% baseline per Finding 1). +2. No `timeBeginPeriod` while animations run (verify via `powercfg /energy` or timer-resolution + query on a Win10 1803+ box). +3. Frame delivery: mean interval within ±0.5 ms of target over a 10 s run on an idle machine at + 60 Hz; no monotonic slow drift (absolute-schedule check: 600th frame due time within one frame of + `epoch + 600 × period`). +4. Zero per-frame heap allocations in steady state (verify with an allocation-tracking test or + `GC.GetAllocatedBytesForCurrentThread` bracketing). +5. Race test for Finding 4: concurrent register/unregister stress leaves no registration without a + running loop. +6. Existing `HighPrecisionTimerTests` updated/extended accordingly; late-callback tolerance test for + consumers (Finding 7). +7. `HighPrecisionTimerTick` surface unchanged (internal consumers depend on it); all other churn is + internal to the pacer. + +## Constraints + +- Everything stays `internal`; no public API review implications. +- Follow repo conventions (`LibraryImport`, nullable enabled, existing XML-doc voice). +- Windows-only paths gated with `[SupportedOSPlatform]` / `OperatingSystem.IsWindowsVersionAtLeast` + as currently practiced in the file. diff --git a/.github/Feature-Prompts/Net11/02-B01-SystemVisualSettings-Implementation.md b/.github/Feature-Prompts/Net11/02-B01-SystemVisualSettings-Implementation.md new file mode 100644 index 00000000000..56070f96b3e --- /dev/null +++ b/.github/Feature-Prompts/Net11/02-B01-SystemVisualSettings-Implementation.md @@ -0,0 +1,158 @@ +# Work Order: SystemVisualSettings (Implementation) + +**Branch:** `Net11/Integration-2` (KlausLoeffelmann/winforms) +**Scope:** Code changes only. The GitHub proposal issue is handled by a separate work order +(`ApiReview-IssueUpdates.md`, Section 3) and run only after this implementation stands. +**Supersedes:** `Application.GetWindowsAccentColor` and the standalone accessibility text-size change +event currently on the branch (see item 6). + +--- + +## Background + +Windows delivers visual/accessibility setting changes through four channels — `WM_SETTINGCHANGE`, +`WM_DWMCOLORIZATIONCOLORCHANGED`, `WM_THEMECHANGED`, `WM_SYSCOLORCHANGE` — and today each consumer +normalizes that zoo individually. This work order introduces one typed, read-only snapshot plus one +unified change notification, with a **leak-free consumption path for controls** (virtual cascade, no +static-event subscription — the `SystemEvents.UserPreferenceChanged` leak class must be structurally +impossible for the default path). + +Deliberate non-goals, enforced by the type system: **no settable properties.** Perceptual adjustments +(contrast, color filtering, text scale, focus prominence, motion) are user-owned via Windows +accessibility settings; app-side theming is the business of control vendor partners. Renderers derive +output from this snapshot combined with `EffectiveVisualStylesMode`. + +--- + +## 1. New types (`System.Windows.Forms`) + +```csharp +public sealed class SystemVisualSettings +{ + public Color AccentColor { get; } + public float TextScaleFactor { get; } // 1.0–2.25, Windows a11y text scale + public bool HighContrastEnabled { get; } + public bool ClientAreaAnimationEnabled { get; } // SPI_GETCLIENTAREAANIMATION + public bool KeyboardCuesVisible { get; } // SPI_GETKEYBOARDCUES (system default) + public Size FocusBorderMetrics { get; } // SPI_GETFOCUSBORDERWIDTH/HEIGHT, pixels +} + +[Flags] +public enum SystemVisualSettingsCategories +{ + None = 0, + AccentColor = 1 << 0, + TextScale = 1 << 1, + HighContrast = 1 << 2, + Animations = 1 << 3, + KeyboardCues = 1 << 4, + FocusMetrics = 1 << 5 +} + +public class SystemVisualSettingsChangedEventArgs : EventArgs +{ + public SystemVisualSettings OldSettings { get; } + public SystemVisualSettings NewSettings { get; } + public SystemVisualSettingsCategories Changed { get; } +} +``` + +Immutable snapshot semantics — values must not shift under an event handler comparing old vs new. +XML remarks per the design notes: `HighContrastEnabled` cross-references the effective-mode clamp; +`KeyboardCuesVisible` documents the system-default vs per-window (`WM_UPDATEUISTATE`) distinction; +`FocusBorderMetrics` is documented as the baseline input for border/focus prominence in renderers +(scaled for DPI and `TextScaleFactor`), replacing fixed constants; the class remarks state the +no-app-side-overrides principle explicitly. + +## 2. `Application` surface + +```csharp +public static SystemVisualSettings SystemVisualSettings { get; } +public static event EventHandler? SystemVisualSettingsChanged; +``` + +Event XML remarks must state: (a) raised once per settings transition, normalized across the four +underlying messages; (b) handlers should early-out via `e.Changed`; (c) **audience is app-lifetime +consumers** (theming engines, services); components with shorter lifetime than the application must +unsubscribe; **controls and forms should use the `Control`-level virtual/instance event instead, +which requires no unsubscription** (see item 3). This positioning is the leak fix — make the +leak-free path the documented default. + +## 3. `Control`-level consumption (the leak-free path) + +```csharp +protected virtual void OnSystemVisualSettingsChanged(SystemVisualSettingsChangedEventArgs e); +public event EventHandler? SystemVisualSettingsChanged; +``` + +- Model the cascade on the existing `OnSystemColorsChanged` pattern in `Control.cs`: virtual + dispatch parent→children, instance event raised from within the virtual. No subscription to any + static event exists anywhere in this path — lifetime coupling is structural. +- Keep the cascade pattern-identical to `OnVisualStylesModeChanged` / `OnParentVisualStylesModeChanged` + (see the VisualStylesMode impact work order). A `HighContrast` category change resolves as an + effective-visual-styles-mode change for affected controls and must route through that machinery's + early-out/dispatch — no duplicate HC handling in this cascade. +- **Remove/replace the existing Form-level replicated text-size event**: it generalizes into this + cascade and disappears as a special case. Migrate in-box usages. +- Staleness rule (document in XML remarks, mirroring `OnSystemColorsChanged` folklore — this time + written down): the cascade only reaches parented controls; a control created but not yet parented + misses transitions and must re-query `Application.SystemVisualSettings` on handle creation / + `OnParentChanged`. + +## 4. Message plumbing and normalization + +Central internal tracker (e.g. `SystemVisualSettingsTracker`) holding the current snapshot: + +- Every **top-level** window already receives the four raw messages; handle them in the existing + top-level `WndProc` paths. +- On receipt: the window asks the tracker to re-query. The **first** arriver computes the diff + against the current snapshot, atomically swaps it (`Interlocked` reference swap), raises the static + `Application` event **once**, and cascades into its own tree. Subsequent top-levels re-query, see + no diff, raise nothing at Application level, but **still cascade into their own trees** using the + already-computed args. +- Threading falls out for free: each tree is notified on the thread owning its top-level — no + marshaling, correct for multi-message-loop applications. Do not centralize onto one thread. +- Coalesce message storms: a single user action can produce several of the four messages; debounce + within a message-pump iteration (re-query once per burst per top-level, not per message). + +## 5. In-box consumption cleanup + +- Audit in-box `Microsoft.Win32.SystemEvents` subscriptions (`UserPreferenceChanged` et al.) in + controls/renderers; migrate those covered by the new categories to the cascade. Document any that + must remain (categories outside this surface) — do not expand the snapshot to chase them in this + work order. +- `TextBoxBase` Net11 border rendering: consume `FocusBorderMetrics` + `TextScaleFactor` as the + border-prominence input where fixed constants are currently used (coordinate with the animation / + focus-indicator renderer as applicable). +- Animated renderers (`AnimatedControlRenderer` / `AnimationManager`): honor + `ClientAreaAnimationEnabled == false` by rendering final state immediately and suppressing + transitions; react to the `Animations` category change at runtime. + +## 6. Supersede the piecemeal APIs + +- `Application.GetWindowsAccentColor` → `Application.SystemVisualSettings.AccentColor`. The method + has not shipped stable: **remove it** on this branch (preferred) rather than obsoleting, to avoid + two sources of truth. If removal is blocked by preview-compat policy, `[Obsolete]` with pointer. +- The standalone text-size change event (Application- and/or Form-level) → `Changed.HasFlag(TextScale)` + on the unified event / cascade. Same removal-vs-obsolete decision, same preference. +- Migrate all in-box call sites. + +## 7. Tests + +- Snapshot immutability and correct SPI mapping per property (mock/native-shim as the repo's test + infra allows). +- Normalization: N top-level windows + one settings transition ⇒ exactly one Application-level raise; + every window's tree cascaded exactly once; delivery on each tree's own thread. +- Flags correctness per category, including multi-category transitions (HC toggle typically changes + colors + HC + metrics in one burst — must coalesce to one event with combined flags). +- Leak test: create/dispose forms subscribing to the **control-level** event in a loop; assert + collectability (`WeakReference`), proving no static rooting. Counter-test documenting that the + static event does root (expected, documented behavior). +- HC toggle end-to-end: cascade triggers effective-mode change path once, no duplicate layout. + +## Constraints + +- All new public surface XML-documented in the branch's voice; internal tracker fully internal. +- No settable members anywhere on the new types — if implementation pressure suggests one, stop and + flag rather than adding it. +- Windows-only P/Invoke via `LibraryImport`, gated per existing repo practice. diff --git a/.github/Feature-Prompts/Net11/03-B05-VisualStylesMode-ImpactApi-Implementation.md b/.github/Feature-Prompts/Net11/03-B05-VisualStylesMode-ImpactApi-Implementation.md new file mode 100644 index 00000000000..a1d56d1aa0b --- /dev/null +++ b/.github/Feature-Prompts/Net11/03-B05-VisualStylesMode-ImpactApi-Implementation.md @@ -0,0 +1,137 @@ +# Work Order: VisualStylesMode — Change-Impact API (Implementation) + +**Branch:** `Net11/Integration-2` (KlausLoeffelmann/winforms) +**Scope:** Code changes only. GitHub issue updates are handled by a separate work order +(`ApiReview-IssueUpdates.md`, Section 1) and run only after this implementation stands. +**Out of scope:** `HighPrecisionTimer` (separate work order), `SystemVisualSettings` (separate work order — +but see the composition note in item 4, the two cascades share a pattern). + +--- + +## Background + +The `VisualStylesMode` API is implemented on this branch. Two gaps motivate this change: + +1. **Correctness/ordering:** Base `Control.OnVisualStylesModeChanged` only invalidates, raises the + event, and cascades — no preferred-size cache invalidation, no layout transaction (unlike + `OnFontChanged`). Every metric-changing control reimplements a four-step protocol; `TextBoxBase` + currently runs `LayoutTransaction.DoLayoutIf(...)` **before** `UpdateStyles()`, so containers + measure against pre-transition metrics — a live mode switch clips/overlaps until controls are + recreated. +2. **Performance:** A top-level switch triggers one full parent layout per metric-affected control, + each against partially-transitioned state; repaint-only controls pay costs they don't need. + +Fix: a declarative impact model — derived controls state **what** changes, the base class owns +**how and in what order** to react, with one coalesced layout pass per container. + +--- + +## Changes + +### 1. `Control.cs` — visibility promotion + +`EffectiveVisualStylesMode`: `private protected` → `protected`. **Non-virtual** (deliberate — it is +the accessibility policy enforcement point; the customization hook is the already-virtual +`DefaultVisualStylesMode`). Extend XML docs: this is the value controls must honor for rendering; +reflects High Contrast (⇒ `Classic`) and `Disabled`; cross-reference `DefaultVisualStylesMode`. + +### 2. `Control.cs` — impact enum + virtual + +```csharp +protected enum VisualStylesModeChangeImpact +{ + None, // no rendering difference; skip all work + Repaint, // client-area rendering only; metrics identical + NonClientUpdate, // NC frame changes; style/frame update, no size change + Metrics // preferred size / layout metrics change; full invalidation + layout +} + +protected virtual VisualStylesModeChangeImpact GetVisualStylesModeChangeImpact( + VisualStylesMode oldMode, + VisualStylesMode newMode) + => VisualStylesModeChangeImpact.Repaint; +``` + +Parameters are **effective** modes. Document: implementations may consult instance state +(`Multiline`, `BorderStyle`, …) but the value must be stable for the duration of the change handling. + +### 3. `Control.cs` — setter early-out on effective equality + +In the `VisualStylesMode` setter and `OnParentVisualStylesModeChanged`: capture effective mode before +the change; if unchanged after, skip `OnVisualStylesModeChanged` entirely (no invalidate, no cascade +into that subtree). Preserve the existing shadowing behavior (child with explicit local value already +stops the ambient cascade) and add the effective-equality check on top. High Contrast active ⇒ raw +`Net11 → Latest` switch is a complete no-op. + +### 4. `Control.cs` — `OnVisualStylesModeChanged` becomes the dispatcher + +Preserve the disposal guard and public event raise. Then: + +- `impact = GetVisualStylesModeChangeImpact(oldEffective, newEffective)` — plumb old/new effective + values from the setter/cascade (least-invasive mechanism consistent with how `OnParentFontChanged` + plumbs state). +- `None` → raise event, skip rendering/layout work, still cascade (children's impact may differ). +- `Repaint` → `Invalidate()`. +- `NonClientUpdate` → `UpdateStyles()` + NC frame refresh (`SetWindowPos` + `FRAMECHANGED`, per the + branch's existing pattern), then `Invalidate()`. +- `Metrics` → in order: `CommonProperties.xClearPreferredSizeCache(this)` → `UpdateStyles()` / frame + refresh → layout deferred to the coalesced step: + +```csharp +if (ChildControls is { } children) +{ + using (new LayoutTransaction(this, this, PropertyNames.VisualStylesMode, resumeLayout: false)) + { + for (int i = 0; i < children.Count; i++) + { + children[i].OnParentVisualStylesModeChanged(e); + } + } +} + +LayoutTransaction.DoLayout(this, this, PropertyNames.VisualStylesMode); +``` + +Add `PropertyNames.VisualStylesMode` if missing. + +**Composition note:** the `SystemVisualSettings` work order introduces a structurally identical +parent→child cascade (`OnSystemVisualSettingsChanged`, modeled on `OnSystemColorsChanged`). Keep the +two cascades pattern-identical; a High Contrast toggle arriving via that path resolves here as an +effective-mode change and must take the same early-out/dispatch route — no duplicate handling. + +### 5. `TextBoxBase.cs` — adopt the model, delete the hand-rolled protocol + +- Override `GetVisualStylesModeChangeImpact`: crossing `Classic`/`Disabled` ↔ `>= Net11` ⇒ `Metrics` + (`PreferredHeight` selection and NC padding both change). Within `>= Net11` (`Net11 → Latest`) ⇒ + `Repaint` **unless** the branch's renderer shows metric differences between those modes — verify + against `PreferredHeightCore` and the NC padding tables; document the decision in XML remarks. +- Slim `OnVisualStylesModeChanged`: remove manual `xClearPreferredSizeCache` / `DoLayoutIf` / + `AdjustHeight` sequencing — base owns it. Keep control-specific work + (`_focusIndicatorRenderer?.Synchronize(...)`, `_triggerNewClientSizeRequest = false`), with the + ordering contract: latch reset **before** `base.OnVisualStylesModeChanged(e)` (which runs + `UpdateStyles()`); any residual `AdjustHeight` need goes **after** `base` — verify with the + live-switch repro (TableLayoutPanel, AutoSize rows, anchored single-line TextBoxes; runtime mode + switch; rows must resize without reopening the form). +- This closes the `DoLayoutIf(AutoSize: false)` hole: base-owned coalesced layout means + `AutoSize == false` TextBoxes no longer skip parent re-measurement. + +### 6. Documentation & tests + +- `docs/Net11Api_05_VisualStylesMode.HighRiskReview.md`: add the behavioral change (base + `OnVisualStylesModeChanged` now clears caches, updates styles, requests layout; overriders calling + `base` inherit this). If the branch's `WFCC`/`ComponentChange` infrastructure is present, annotate; + otherwise the high-risk doc entry suffices. +- `ControlTests.VisualStylesMode.cs`: + - effective-equality early-out (HC active ⇒ no event storm, no layout), + - default impact is `Repaint`, + - `Metrics` path clears preferred-size cache; exactly one layout per container for a multi-control + subtree switch (layout-count instrumentation or `LayoutEventArgs` assertion), + - `TextBoxBase` live-switch regression test: preferred-height change reflected in an AutoSize + `TableLayoutPanel` row without control recreation. + +## Constraints + +- No binary breaking changes (`protected` promotion + new `protected` members on unsealed public + class are additive). +- Do not change the `EffectiveVisualStylesMode` clamping logic. +- Match repo analyzers/style and the branch's XML-doc voice. diff --git a/.github/Feature-Prompts/Net11/Application.SystemTextAwareness/Application.SystemTextAwareness.md b/.github/Feature-Prompts/Net11/Application.SystemTextAwareness/Application.SystemTextAwareness.md new file mode 100644 index 00000000000..da7edcf854e --- /dev/null +++ b/.github/Feature-Prompts/Net11/Application.SystemTextAwareness/Application.SystemTextAwareness.md @@ -0,0 +1,180 @@ +# Task: Create a new API — `Application` system-text-size awareness — and the respective API proposal + +## What to do + +Create a **new API proposal / API review issue in the upstream `dotnet/winforms` repo** +(`origin` = my fork, `upstream` = the Microsoft repo where this lands). Write it per the +WinForms repo conventions and the relevant skills for authoring new-API issues. Apply the +skills; don't ask me for boilerplate. + +This proposal introduces **runtime awareness of the Windows Accessibility text-size +setting** at the `Application` level, plus a per-`Form` change notification. It is the +**foundation** proposal; a companion `TreeView.NodeLeading` proposal references this one. + +## Before you implement anything + +**Verify every premise below against current source before committing to the design.** +Verify, don't trust. If any premise is wrong, stop and tell me. Key files (VMR @ +`96982699e0dd8c046f397541dc0eb235ea8a4958`): + +- `src/winforms/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Internals/ScaleHelper.cs` +- `src/winforms/src/System.Windows.Forms/System/Windows/Forms/Application.cs` +- `src/winforms/src/System.Windows.Forms/System/Windows/Forms/Application.ThreadContext.cs` +- `src/winforms/src/System.Windows.Forms/System/Windows/Forms/Application.ParkingWindow.cs` +- `src/runtime/src/libraries/Microsoft.Win32.SystemEvents/...` (SystemEvents / UserPreferenceCategory) + +## Rationale — surface and complete an existing partial implementation + +This is **not** a net-new feature; it **finishes a half-built one**. WinForms already reads +the Accessibility text-size setting, but only once and only for the default font: + +`ScaleHelper.ScaleToSystemTextSize(Font?)` reads +`HKEY_CURRENT_USER\Software\Microsoft\Accessibility` → value `TextScaleFactor` +(REG_DWORD, clamped 100–225), and returns the font scaled by `TextScaleFactor / 100` +(or `null` if 100, if the font `IsSystemFont`, or if the OS is < Windows 10 1507). It is +documented in-source as the **Settings → Display → Make Text Bigger** setting. + +The gaps: (1) the value is **never surfaced** to developers; (2) it is read **once**, at +default-font construction, and the app **never reacts** when the user changes the setting at +runtime; (3) there is **no notification** mechanism. This proposal fills those three gaps. + +## Three-knob disambiguation (MANDATORY callout — reviewers will conflate these) + +Windows surfaces three *different* sizing mechanisms; the Settings UI even puts two on one +page (`System → Display → Custom scaling` shows a "Custom scaling 100–500%" box AND a +"Text size" link). The proposal MUST state plainly which one it targets: + +1. **Display / Custom scaling (100–500%)** → this is **DPI**. Already handled by + `HighDpiMode` and the DPI events (`WM_DPICHANGED`, `Control.DpiChanged*`). **NOT** this + proposal. +2. **Accessibility → Text size (100–225%)** → registry `TextScaleFactor` under + `HKCU\Software\Microsoft\Accessibility`; WinRT `UISettings.TextScaleFactor` / + `TextScaleFactorChanged`. **THIS is the target.** Independent of DPI. +3. **Legacy pre-Win10 per-element text sizing** (title bars/menus) → **removed** in Windows + 10 1703; the Accessibility slider replaced it. Mentioned only to close the loop. + +State explicitly that `Application.SystemTextSize` reflects **#2 only**, and is orthogonal to +DPI (#1). + +## Proposed API + +### `Application` (process-static) + +- **`public static double SystemTextSize { get; }`** — the current Accessibility text-scale + factor (1.0–2.25; i.e. `TextScaleFactor / 100`). **Live getter** — re-reads the value, does + not cache, because it is a system setting that changes at runtime. Well-defined regardless + of whether any `Form` exists or how many UI threads are running (it is process-global). +- **`public static SystemTextSizeAwareness SystemTextSizeAwareness { get; set; }`** — the + mode. **Enum, not bool**, deliberately, to reserve room for a future `Automatic`: + - `Unaware` (default) — no notification raised; fully back-compatible, nothing changes. + - `Notify` — raise change notifications (see below); the app decides how to respond. + - *(reserved, NOT implemented now: `Automatic` — framework re-flows for you. Reserving the + enum slot now avoids a future breaking bool→enum change, the same lesson `HighDpiMode` + learned.)* +- **`public static event EventHandler? SystemTextSizeChanged`** — fires once per process when + the setting changes (only when awareness is `Notify`). + +### `Form` (instance) + +- **`public event EventHandler? SystemTextSizeChanged`** — instance event, raised on each + top-level `Form` when the setting changes. +- **`protected virtual void OnSystemTextSizeChanged(EventArgs e)`** — overridable, fires the + instance event via the `EventHandlerList` pattern (`Events[s_systemTextSizeChangedEvent]`). + +## The trigger architecture (the leak-critical part — get this exactly right) + +A naive design — a static `Application.SystemTextSizeChanged` that `Form`s/`Control`s +subscribe to — **leaks**: the static event strongly roots every subscriber, so no `Form` +that subscribes is ever collected. Avoid this by **mirroring the DPI architecture**, where +`Control`/`Form` learn of DPI changes from their **own `WndProc`** (`WM_DPICHANGED` → +`OnDpiChanged` → instance event via `EventHandlerList`), **not** from a static subscription. + +Verified facts that constrain the design: + +- **`WM_SETTINGCHANGE` is broadcast** (`HWND_BROADCAST`) and delivered directly to top-level + windows' `WndProc`s — it bypasses the thread message queue, so **`IMessageFilter` does NOT + see it.** Do not use a message filter. +- **The WinForms parking window is message-only** (`CreateParams.Parent = HWND_MESSAGE`). + Message-only windows are **excluded from broadcasts**, so the parking window **cannot** + receive `WM_SETTINGCHANGE`. Do not use it. +- **`Application` has no `MainForm`.** The main form lives on `ApplicationContext` (per-run, + per-UI-thread), can be `null` (tray/loop-only apps), and is mutable (splash→main handoff). + So the main form is **not** a reliable receiver. Do not anchor the app-level event to it. +- **`SystemEvents` already owns a hidden top-level broadcast-receiving window** (its + `.NET-BroadcastEventWindow`), and exposes `UserPreferenceChanged` with a + `UserPreferenceCategory` (the relevant value is `Accessibility`, which is **coarse** — it + covers any accessibility change, so you must re-read `TextScaleFactor` and diff to confirm + it was text-scale). + +**Resulting design:** + +- **App-level:** `Application` makes **one internal, process-lifetime** subscription to + `SystemEvents.UserPreferenceChanged`, filters `Category == Accessibility`, re-reads + `TextScaleFactor`, diffs against the cached value, and if changed raises the static + `SystemTextSizeChanged`. This is a single framework-static→framework-static link — it does + **not** root any user object, so it is **not** the leak hazard. Reuses the existing hidden + broadcast window; **no new HWND** required. +- **Form-level:** each top-level `Form` handles `WM_SETTINGCHANGE` in its **own `WndProc`** + (it is a broadcast — every top-level window receives it), re-reads + diffs, and raises its + **instance** `SystemTextSizeChanged` via `OnSystemTextSizeChanged`. `Form`s do **not** + subscribe to `Application` — no rooting, lifetime = the window. + +Caveat to document: there is **no dedicated `WM_TEXTSCALECHANGED`** message. Both paths must +recognize a *relevant* change by re-reading `TextScaleFactor` and comparing, not by the +message alone. + +## Aids vs. leave-it-to-the-user + +**Notify-only. No automatic re-layout / font-rescaling aids in this proposal.** Reasons: +text scale interacts with `AutoScaleMode`, anchored/docked layout, and explicitly-set fonts +in app-specific ways; a generic "scale all fonts by the factor" helper breaks more than it +fixes. The reserved `Automatic` enum value is exactly where such behavior would live later. +`Notify` gives the developer the factor and the event; they decide. (Consistent with the +companion `NodeLeading` proposal's conservative-default philosophy.) + +## Why this matters across controls (evidence — include the matrix) + +Multiple text-measuring controls derive item/row/tile extents from a `Font` that today only +reacts to the text-size setting once at startup (via `ScaleToSystemTextSize` on the default +font) and never again. So **any single cached height scalar is wrong the moment text size +changes at runtime** — which is the core argument for runtime awareness: + +- **ListBox / ComboBox** — have `MeasureItem` + `OwnerDrawVariable` (a real per-item measure + hatch) and `ItemHeight`. Their gap is the legacy default base calc + the missing runtime + text-scale reaction — i.e. exactly this proposal. +- **TreeView** — has neither `MeasureItem` nor a wrapped native height API; only a uniform + native item height. Worst-positioned for a managed fix; addressed by the companion + `TreeView.NodeLeading` proposal, which depends on this one. +- **ListView (Details)** — no `MeasureItem`, no native row-height message; row height is + comctl-computed from `SmallImageList` + control font, while per-item/subitem fonts are + honored via `NM_CUSTOMDRAW` (`CDRF_NEWFONT`). Userland workarounds (phantom `SmallImageList`; + `LVS_OWNERDRAWFIXED` + one-shot reflected `WM_MEASUREITEM`; "inflate control font / shrink + item fonts") each have holes (header leak, set-once, exhaustive per-item font setting, + owner-draw-all). **No clean complete userland solution exists** — strengthening the case + that text-size reaction belongs in the framework. + +## XML doc requirements + +- Document that `SystemTextSize` is the **Accessibility text-size** factor (Settings → + Display → Make Text Bigger), **not** DPI/display scaling, and is process-global / live. +- Document the `Unaware`/`Notify` semantics and that `Automatic` is reserved for future use. +- Document the no-rooting design note on the static event (so consumers understand instance + vs. static). + +## Open questions for review + +- Should `SystemTextSize` be `double` (1.0–2.25) or expose the raw int percent (100–225)? +- Behavior on OS < Windows 10 1507 (where `ScaleToSystemTextSize` no-ops): `SystemTextSize` + returns 1.0 and no events fire? +- Whether to also expose the value/event on `Application` only, leaving `Form` consumers to + use their own `WndProc` override — or provide the `Form` instance event as proposed + (recommended, for parity with the DPI event model). + +## Output + +The upstream issue per the skills: summary; the "complete a partial implementation" +rationale; the mandatory three-knob disambiguation; the proposed API; the leak-safe trigger +architecture with the four verified constraints (broadcast vs. IMessageFilter, message-only +parking window, no MainForm on Application, SystemEvents reuse); Notify-only stance; the +cross-control matrix; XML-doc requirements; open questions. Flag anything the source +contradicts. diff --git a/.github/Feature-Prompts/Net11/Application/net11-VisualStylesMode/TextBoxBase-VisualStyles-WorkOrder.md b/.github/Feature-Prompts/Net11/Application/net11-VisualStylesMode/TextBoxBase-VisualStyles-WorkOrder.md new file mode 100644 index 00000000000..09894f9d23f --- /dev/null +++ b/.github/Feature-Prompts/Net11/Application/net11-VisualStylesMode/TextBoxBase-VisualStyles-WorkOrder.md @@ -0,0 +1,137 @@ +# Work Order — Port `TextBoxBase` NC-Painting + `VisualStylesMode` Chrome onto `VisualStylesNet11` + +**Target branch:** `KlausLoeffelmann/winforms` → `VisualStylesNet11` (current-`main`-based; modern path layout, no `/src/src/` doubling; `TextBoxBase.cs` ≈ 2130 lines). + +**Source of the original implementation (pinned):** `KlausLoeffelmann/winforms` @ `cf32e9c4efeba9d77e1a14025a8590b104e3c705` (old `/src/System.Windows.Forms/src/...` layout; `TextBoxBase.cs` ≈ 2562 lines). Relevant files: +- `.../Controls/TextBox/TextBoxBase.cs` — NC paint, NC calc, focus invalidation, `GetVisualStylesPadding`, helpers. +- `.../Controls/TextBox/TextBoxBase.NonClientBitmapCache.cs` — the offscreen cache class. +- `.../Controls/TextBox/TextBox.cs` — derived-level overrides (`CreateParams`, `WndProc`, `OnBackColorChanged`, `PRF_NONCLIENT` path). + +**Standing approval:** API review board sign-off from the .NET 9 cycle is still valid; DRI owns the call. This is a port + cleanup, **not** a redesign. Do not invent new public API beyond what the original exposed plus the `Padding` unshadowing called out below. + +**House style (apply throughout):** namespaces globally imported (no `using`/`imports` noise); C# 13/14; NRTs on; `var` only for long type names or when the type is obvious from the RHS, explicit type names for primitives; blank line between a new block and a following `return`; pattern matching / `is` / `and` / `or` / switch expressions preferred; collection expressions (`List x = [];`); expression-bodied members for single-line methods/read-only props formatted with the `=>` on the next line, 1-space-indented. Generate XML doc comments; use ``/``. + +--- + +## PROMPT 1 — Carry-over / Port + +> **Role:** You are porting a working-but-imperfect feature across a 3-year gap in the surrounding file. Faithfully reproduce the *mechanism*; do not improve, simplify, or "modernize" the algorithm except where this document explicitly says to. Where the surrounding `VisualStylesNet11` code has moved on, rebase onto the new shape rather than pasting. + +**Task.** Bring the non-client (NC) painting feature for `TextBoxBase` (and the `TextBox` derived touchpoints) from the pinned SHA `cf32e9c4…` onto `VisualStylesNet11`. The target branch currently has **none** of this work (no `WmNcPaint`/`WmNcCalcSize`/`OnNcPaint`/`GetVisualStylesPadding`/`NonClientBitmapCache`), but it **does** have the `VisualStylesMode` property infrastructure referenced in doc comments — wire into that, don't redeclare it. + +**Steps, in order:** + +1. **Fetch and diff the three source files** at SHA `cf32e9c4…` against their `VisualStylesNet11` counterparts. Produce a short inventory of every member you intend to add or modify, grouped by file, before writing any code. + +2. **Port `TextBoxBase.cs` members:** + - `WmNcPaint` / `OnNcPaint` (offscreen bitmap fill → AA rounded/single chrome → blit; `GetWindowDC`/`ReleaseDC` in `finally`). + - `WmNcCalcSize` (carves the padding band from `NCCALCSIZE_PARAMS->rgrc[0]`; gated on the `_triggerNewClientSizeRequest` latch). + - `InitializeClientArea` (the one-shot `SetWindowPos(SWP_FRAMECHANGED|NOMOVE|NOSIZE|NOZORDER|NOACTIVATE)` that provokes the single NC-calc). + - `GetVisualStylesPadding` / `GetScrollBarPadding` and the `VisualStyles{Fixed3D|FixedSingle|NoBorder}BorderPadding`, `BorderThickness` consts. + - The `WM_NCCALCSIZE` / `WM_NCPAINT` cases in `WndProc`. + - `OnGotFocus` / `OnLostFocus` / `OnSizeChanged` NC-frame invalidation (`RedrawWindow` with `RDW_FRAME|RDW_INVALIDATE`). + - `PreferredHeight` split (`PreferredHeightClassic` vs `PreferredHeightCore` selected by `VisualStylesMode`). + - Reconcile `GetPreferredSizeCore` with the modern branch already present on target. + +3. **Port `TextBoxBase.NonClientBitmapCache.cs`** — BUT this is a **decision point**, see Step 6. + +4. **Reconcile `TextBox.cs` (derived):** the target already has `CreateParams`, `WndProc`, `OnBackColorChanged` (special-casing `Fixed3D`), `OnGotFocus`, and a `WM_PRINTCLIENT`/`PRF_NONCLIENT` + `Application.RenderWithVisualStyles` path. Merge the NC behavior so the derived overrides cooperate with the new base NC painting (no double border draw, no fighting the `PRF_NONCLIENT` path). Call out every conflict you resolve. + +5. **Unshadow `Padding`.** On target it is still the neutered shadow (`[Browsable(false)]`, `EditorBrowsableState.Never`, `DesignerSerializationVisibility.Hidden`, `get/set => base.Padding`). Make it a real, browsable, serializable property that feeds the NC band via `GetVisualStylesPadding`. Preserve classic-mode behavior when `VisualStylesMode` is `Disabled`/`Classic`. Note the designer-serialization and back-compat implications in the PR description. + +6. **Retarget the rounded-rectangle helpers to the framework.** The original called fork-local `FillRoundedRectangle`/`DrawRoundedRectangle`. These now ship as **`System.Drawing.Graphics` instance methods** (`(Pen, Rectangle, Size)` + `RectangleF`/`SizeF` overloads; landed in the .NET 9 wave, current on target). **Delete the fork-local helpers and call the shipped methods.** ⚠️ Verify a **`FillRoundedRectangle(Brush, …)`** overload actually exists on target — the public ref only enumerates the `Draw`(`Pen`) overloads. If the `Fill`/`Brush` overload is absent, STOP and flag it; do not re-add a private helper without surfacing the gap. + +7. **Cache → `BufferedGraphics` (DECIDED — implement, do not re-evaluate).** The original used a hand-rolled per-instance `NonClientBitmapCache` (`CreateCompatibleBitmap` + `Image.FromHbitmap` + manual `DeleteObject`, `EnsureSize` realloc). Jeremy's late "use the existing cached bitmap" is confirmed to mean WinForms' own **`BufferedGraphics`/`BufferedGraphicsContext`** (the engine behind `OptimizedDoubleBuffer`; in `System.Drawing.Common` since .NET Framework 2.0, present on target). It is **not** `System.Drawing.Imaging.CachedBitmap` — that type is a read-only, device-dependent, blit-only frozen copy (no `Graphics`, translation-only, dies on bit-depth change) and cannot be a render target. **Delete `NonClientBitmapCache` entirely** (and its file `TextBoxBase.NonClientBitmapCache.cs`) and the `_cachedBitmap` field; replace with the shared buffer. Exact wiring: + - Inside `OnNcPaint`, get the shared context and allocate the buffer against the **window-DC `Graphics` already created in `WmNcPaint`**, sized to the window `bounds`: + `BufferedGraphicsContext context = BufferedGraphicsManager.Current;` + `using BufferedGraphics buffer = context.Allocate(graphics, bounds);` + `Graphics offscreenGraphics = buffer.Graphics;` + - **Do NOT `using`/dispose `buffer.Graphics`** — the `buffer` owns it; `using` the **buffer** only. (The original `using`-disposed its `GetNewGraphics()` because it owned that `Graphics`; that ownership is now the buffer's.) + - **All drawing into `offscreenGraphics` is unchanged** — the `FillRectangle(parentBackgroundBrush…)` corner-fill, the `BorderStyle` switch, the focus line: byte-for-byte identical, just a different `Graphics` target. + - **`ExcludeClip(clientBounds)` stays on the *target* `graphics`** (the window-DC one), exactly where it is now, set *before* the buffer draws. It governs where `Render()` may blit, protecting the client area — unchanged semantics. + - Replace the final blit `graphics.DrawImageUnscaled(offscreenBitmap, Point.Empty);` with **`buffer.Render();`** (no argument — it blits to the `graphics` captured at `Allocate` time). + - While here, fix the pre-existing **double-dispose** in `WmNcPaint`: it has both `using Graphics graphics = …` and an explicit `graphics.Dispose()` in `finally`. Drop the explicit `Dispose()`; keep the `using` (or keep explicit and drop `using` — one, not both). + - **Rationale to record in PR notes:** WinForms paints NC serially (one HWND at a time on the UI thread), so a single shared buffer suffices for any number of controls; the per-instance cache kept N resident GDI bitmaps to serve a one-deep queue. Steady-state allocation is unchanged (zero — shared buffer is reused when size fits); resident GDI memory drops from N× to 1×. The only cost is buffer-resize churn if controls of *wildly varying* sizes paint in a grow/shrink-alternating order — see smoke scenario 7 instrumentation. + +8. **Carve clamp + chrome degradation (settled design — implement exactly as stated, do NOT add a minimum size).** The original `WmNcCalcSize` does raw subtraction on `rgrc[0]` with **no clamp**, so a large `Padding` (made worse because `GetVisualStylesPadding(true)` *adds* the live scrollbar allowance from `GetScrollBarPadding` on top of the border padding) can drive the carved client rect to **zero or inverted**. Two separate fixes, and they are deliberately *not* a `MinimumSize`: + - **(8a) Never-invert clamp in `WmNcCalcSize`.** Floor each carved extent so the client rect can never invert: after the four adjustments, ensure `bottom >= top` and `right >= left` (e.g. clamp so the resulting client width/height is `Math.Max(0, …)`). A 0–1px client area is **acceptable and intended** — shipping multiline `TextBox` already shrinks to ~1px with scrollbars present, and we match that exactly. **Do NOT introduce a min-height/`MinimumSize`**, and do NOT make sizing behavior differ by `VisualStylesMode` (that would fracture the appearance-only contract of the opt-in). The clamp only prevents *underflow past zero*, which raw subtraction does and plain shrinking does not. + - **(8b) Paint-time chrome degradation in `OnNcPaint`.** The rounded `Fixed3D` chrome (15px radius) renders as a broken lozenge below roughly `2 × cornerRadius + BorderThickness` in height. When the available band/height is below that viable threshold, **fall back to the original/simple chrome render** (flat or single-style border) instead of the rounded path. This is a *rendering* fallback only — it does not change size or layout. Rationale on record: if the box is so small there's no usable client area, the control isn't usable anyway, so graceful visual degradation (not a size floor) is the correct response. + +9. **Build** `System.Windows.Forms` for the target TFM. Resolve all errors. Do not suppress new analyzer warnings without a one-line justification each. + +**Deliverable:** a single commit (or tight series) on `VisualStylesNet11` plus a PR description that lists: members added/modified per file, every `TextBox.cs` conflict resolved, confirmation that `NonClientBitmapCache` was removed and replaced by `BufferedGraphics` (Step 7) with the resize-churn rationale, the clamp/degradation (Step 8) confirmed as render-only with no size minimum, the `Padding` unshadowing implications, and any flagged gaps (Step 6 `Fill` overload). + +--- + +## PROMPT 2 — Critical Review (run AFTER Prompt 1, BEFORE smoke test) + +> **Role:** Adversarial reviewer. The author wants the issues a sharp WinForms maintainer would catch, not reassurance. Cite file + line for every finding. Classify each as **MUST-FIX**, **SHOULD-FIX**, or **PRESERVE (do not 'improve')**. + +Audit the ported code against this checklist. For each item, state the finding and the exact location. + +1. **DPI scaling of the corner radius.** The original hardcodes `const int cornerRadius = 15` and `BorderThickness = 1` in device-independent units, then uses them inside a DPI-scaled NC band, while `GetVisualStylesPadding` *does* take a DPI path (`_deviceDpi`). Confirm whether the radius/thickness now scale Per-Monitor-V2. If not → **MUST-FIX** (corners look proportionally too tight at 150/200%). + +2. **Full-frame NC repaint vs. partial `hrgnClip`.** `WmNcPaint` ignores the wParam clip region and repaints the whole frame. This is the **intended** fix for the offscreen-restore "dirty corners" artifact — verify it's preserved. But confirm `base.WndProc(ref m)` is still invoked with the original message and isn't double-painting the native border under the custom chrome. Classify the "ignore clip" behavior as **PRESERVE**. + +3. **Corner-blend source = `Parent?.BackColor ?? BackColor`.** This is the known ceiling: corners blend against the parent's flat back color, so they mismatch over a gradient/image/Mica/sibling. For the common case (solid form/panel) it's correct. **PRESERVE** — do not let it be "improved" into a fake general-case solution. Note it as a documented limitation only. + +4. **`WM_NCCALCSIZE` ↔ `Padding` round-trip + underflow.** Verify the band carved in `WmNcCalcSize` matches what `GetVisualStylesPadding(true)` reports and what `GetPreferredSizeCore`/`SizeFromClientSize` assume, for all three `BorderStyle` values × `Multiline` × scrollbars. Off-by-one here clips text or the caret. **Additionally** confirm the never-invert clamp (Prompt 1 Step 8a) is present and correct: with large `Padding` on a small multiline box *with both scrollbars*, the carved client rect must floor at 0, never invert. Remember the threshold is **border padding + live scrollbar padding** (`GetScrollBarPadding` reads `WS_HSCROLL`/`WS_VSCROLL`), so underflow hits sooner than the `Padding` value alone implies. Classify "0–1px client area is allowed, no min-size" as **PRESERVE** — do not let a reviewer or the agent add a `MinimumSize` floor. + +4b. **Chrome degradation below viable height.** Confirm `OnNcPaint` (Prompt 1 Step 8b) falls back to simple/flat chrome when height < ≈`2 × cornerRadius + BorderThickness`, instead of drawing a corrupted rounded rect. This is **render-only**; assert it does **not** alter size, layout, or `ClientSize`. Verify the fallback path itself is DPI-correct (the threshold scales with the radius, which per item 1 must scale). + +5. **`BorderStyle` fork + native edge suppression.** `Fixed3D` → rounded chrome, `FixedSingle` → single + underline, `None` → fill. Confirm the native `WS_EX_CLIENTEDGE`/`WS_BORDER` from `CreateParams` is suppressed when NC chrome is active, so the native edge isn't drawn under the custom one. + +6. **`VisualStylesMode` gating is total.** Every NC entry point (`WmNcPaint`, `WmNcCalcSize`, `InitializeClientArea`, the focus/size invalidations) must early-out to byte-for-byte classic behavior when `VisualStylesMode` is `Disabled`/`Classic`. One missing guard = a back-compat regression. Note the original's `OnLostFocus` was **missing** the guard that `OnGotFocus` had — verify the port fixed this asymmetry. + +7. **DC / GDI lifetime.** `GetWindowDC`→`ReleaseDC` in `finally`, and `Graphics.FromHdc`+`Dispose` ordering: correct **only** because `FromHdc` doesn't own the DC. **PRESERVE** — flag any "tidy into a single `using`" as a regression. Confirm the `WmNcPaint` **double-dispose** was fixed (it had both `using Graphics` and an explicit `graphics.Dispose()`). For the `BufferedGraphics` swap (Prompt 1 Step 7): verify the **buffer** is `using`-scoped but **`buffer.Graphics` is NOT separately disposed**; verify `Allocate` targets the window-DC `graphics` and `Render()` is called with no argument; confirm `NonClientBitmapCache` and the `_cachedBitmap` field are fully removed with no dangling refs. Audit for any HBITMAP/HDC leak in the new path (there should be none — the buffer owns it). + +8. **DPI-change without handle recreate.** `_triggerNewClientSizeRequest` is a one-shot latch reset on handle recreate. Does a DPI change that does *not* recreate the handle re-carve the band with new padding? If the band can go stale on monitor move → **MUST-FIX** or at least an explicit tracked issue. + +9. **Caret / IME / selection repaint.** The native `EDIT` invalidates aggressively. Confirm NC chrome doesn't go stale on caret blink/IME composition, and conversely that NC isn't thrashing-repainting on every caret tick. (Author never confirmed this was clean in the original.) + +10. **`TextBox.cs` derived reconciliation.** Verify the derived `WndProc`, `OnBackColorChanged` `Fixed3D` special-case, and the `PRF_NONCLIENT`/`Application.RenderWithVisualStyles` path don't conflict with base NC painting (double draw, wrong-mode paint). + +11. **Allocation churn.** Brushes/pens use cached scopes (`GetCachedSolidBrushScope`/`GetCachedPenScope`) — good; confirm preserved. Confirm the offscreen surface isn't reallocated per paint (only on size change). + +**Deliverable:** a findings list (file:line, severity, recommendation). MUST-FIX items get fixed in this pass; SHOULD-FIX either fixed or filed; PRESERVE items annotated in code with a brief `// Intentional:` comment so the next reader doesn't "fix" them. + +--- + +## PROMPT 3 — Smoke Test Harness + +> *("Smoke test" = the shallow "does it power on without catching fire" pass — from hardware bring-up, where first power-on literally checked for smoke — run before any deep/perf testing. Goal here: broad coverage that it comes up and behaves on the obvious axes, with the two known-fragile cases as explicit named tests.)* + +**Task.** Build a throwaway WinForms test app (separate project, not shipped) that exercises the ported feature across its permutation space and **specifically reproduces the two regressions this feature is prone to.** + +**Permutation grid** — generate a form populated with `TextBox`es (and at least one `RichTextBox`, since `TextBoxBase` is the shared base) covering the cross-product of: +- `BorderStyle`: `None` × `FixedSingle` × `Fixed3D` +- `Multiline`: `false` × `true` (+ `WordWrap` on/off for multiline) +- `Padding`: `Empty` × asymmetric (e.g. `2,6,2,6`) × large (`12`) +- Scrollbars: none × vertical × both +- `VisualStylesMode`: `Disabled`/`Classic` (must look exactly like today) × `Net10`+ (new chrome) +- Focused vs unfocused (drive focus programmatically to capture the adorner/underline) + +**Named, must-pass scenarios (the ones that silently regress):** + +1. **Offscreen-restore ("dirty corners").** Move the window partly off the left/top screen edge, then back. Assert the NC corner regions are repainted clean (no stale pixels). This is the artifact that drove the full-frame-repaint design. Automate the drag via `SetWindowPos`/`MoveWindow`; capture before/after. + +2. **Partial NC invalidation.** Trigger a partial `WM_NCPAINT` (e.g. overlap then reveal a sliver of the frame) and assert the whole chrome is coherent, not just the revealed strip. + +3. **Per-Monitor-V2 DPI.** Run DPI-aware; move forms between a 100% and a 150%/200% monitor (or fake via `LogicalToDeviceUnits`/DPI-changed messages). Assert corner radius, border thickness, and padding band all scale; assert no clipped text/caret. + +4. **Classic-mode parity.** With `VisualStylesMode = Disabled`, assert the control is pixel-identical to baseline `main` (native edge, no custom NC). A regression here is the back-compat line breaking. + +5. **`BorderStyle` switch at runtime** (`Fixed3D`↔`FixedSingle`↔`None`) and **`Padding` change at runtime** — assert the band re-carves and chrome redraws without artifacts (exercises the `_triggerNewClientSizeRequest` reset path). + +6. **Focus transitions** — tab through the grid; assert the focus underline (single) / 3D focus line (Fixed3D, shortened to clear the corner curve) appears/clears correctly. + +7. **Shrink-to-collapse (clamp + degradation).** Take a multiline `Fixed3D` box with large `Padding` (e.g. `12`) and **both** scrollbars visible, then programmatically drag/resize its height down toward 1px. Assert: (a) **no crash / no inverted client rect** handed to the native `EDIT` — the carve floors at 0 (Step 8a); (b) below ≈`2 × cornerRadius + thickness` the chrome **falls back to flat/simple render** rather than drawing a corrupted lozenge (Step 8b); (c) the control **still collapses** to ~1px exactly like classic multiline — assert it is **not** held open by any min-size (regression if a floor appeared). Repeat at 150%/200% DPI so the degradation threshold is verified scaled, not fixed at 96-dpi pixels. + +8. **BufferedGraphics allocation churn (perf sanity).** Build two forms: (a) **40 same-size** textboxes, (b) **40 wildly varying-size** textboxes (mix tiny and large), all `VisualStylesMode ≥ Net10`. Force a full repaint storm (invalidate all NC frames repeatedly; resize the form to cascade re-layout). Instrument the shared buffer: wrap/observe `BufferedGraphicsManager.Current` and count actual **bitmap (re)allocations** vs. reuses across the storm. Assert: case (a) allocates the buffer ≈once then reuses (steady-state alloc ≈ 0); case (b) may reallocate on grow but must **not** allocate-per-paint. Log alloc count per case. This empirically confirms the shared-buffer reasoning from Prompt 1 Step 7 and catches any accidental per-paint allocation regression. + +**Harness mechanics:** +- A "capture all" button that screenshots each form to disk per `VisualStylesMode`, for eyeball diffing classic-vs-modern and pre-vs-post-DPI. +- A console/log line per assertion (pass/fail) so it can run semi-automated. +- Keep it dependency-light: raw WinForms + `SetWindowPos`/`RedrawWindow` P/Invoke for the offscreen and invalidation drivers. + +**Deliverable:** the test project + a one-screen README naming the six scenarios and how to run them, plus a results log from one full run on the porter's machine (note DPI of monitors used). diff --git a/.github/Feature-Prompts/Net11/SuspendRelocationPaintingFlashPreventing/Create-Github-API-Proposal-Prompt.md b/.github/Feature-Prompts/Net11/SuspendRelocationPaintingFlashPreventing/Create-Github-API-Proposal-Prompt.md new file mode 100644 index 00000000000..ce3ad461cab --- /dev/null +++ b/.github/Feature-Prompts/Net11/SuspendRelocationPaintingFlashPreventing/Create-Github-API-Proposal-Prompt.md @@ -0,0 +1,120 @@ +# Copilot Prompt 1 — Author the WinForms API Suggestion + +## Your task + +Write a complete API suggestion for the `dotnet/winforms` repository, ready to be filed +as a GitHub issue with the `api-suggestion` label. The issue covers two related features +for reducing visible intermediate states while WinForms applications update their UI: + +1. Painting suspension with optional layout suspension across a control tree. +2. Deferred top-level form reveal. + +Use these issue sections: + +- `## Rationale` +- `## API Proposal` +- `## API Usage` +- `## Alternative Designs` +- `## Risks` +- `## Will this feature affect UI controls?` +- `### Status Checklist` + +## Sub-feature A — painting and layout suspension + +### Settled API surface + +```csharp +namespace System.Windows.Forms; + +public interface ISupportSuspendPainting +{ + void BeginSuspendPainting(); + void EndSuspendPainting(); +} + +public enum LayoutSuspendTraversal +{ + None = 0, + TopLevelOnly = 1, + Traverse = 2, +} + +public sealed class SuspendPaintingScope : IDisposable +{ + public SuspendPaintingScope(ISupportSuspendPainting? target); + public void Dispose(); +} + +public static class ControlMutationExtensions +{ + public static SuspendPaintingScope SuspendPainting( + this ISupportSuspendPainting target); + + public static SuspendPaintingScope SuspendPainting( + this ISupportSuspendPainting target, + LayoutSuspendTraversal layoutSuspendTraversal); + + public static SuspendPaintingScope SuspendPainting( + this ISupportSuspendPainting target, + Func suspendLayoutContainerFilter); +} +``` + +- `Control` implements `ISupportSuspendPainting` explicitly and exposes protected virtual + `BeginSuspendPaintingCore` / `EndSuspendPaintingCore` hooks. +- `ListView`, `ListBox`, `ComboBox`, `TreeView`, and `RichTextBox` route painting + suspension through their existing `BeginUpdate` / `EndUpdate` paths. +- `None` suspends painting only. +- `TopLevelOnly` suspends layout on the target `Control`. +- `Traverse` suspends layout on the target and all existing descendants. +- The predicate is evaluated for the target and every descendant. A `false` result skips + that node but does not prune traversal. +- Selected nodes are suspended even when they currently have no children. +- Layout-aware overloads require the target to derive from `Control`. +- The scope is a sealed class so it can span `await`. + +### Design considerations + +- Snapshot the selected controls when the scope starts. +- Suspend layout root-to-leaf and resume it deepest-first. +- Resume layout before ending painting so recursive invalidation occurs after layout. +- Keep disposal idempotent and preserve nesting through the existing ref counts. +- Explain that controls added after the snapshot are covered by their parent's suspended + layout but do not receive an independently balanced suspension. +- Discuss traversal cost for large control trees and exceptions thrown by predicates. +- Include invalid and non-`Control` target behavior in the proposal. + +## Sub-feature B — deferred form reveal + +Use the current `FormRevealMode` design from the tracked API proposal: + +```csharp +namespace System.Windows.Forms; + +public enum FormRevealMode +{ + Inherit = -1, + Classic = 0, + Deferred = 1, +} + +public partial class Form +{ + public virtual FormRevealMode FormRevealMode { get; set; } +} + +public partial class Application +{ + public static FormRevealMode DefaultFormRevealMode { get; } + public static void SetDefaultFormRevealMode(FormRevealMode mode); + public static bool IsFormRevealDeferred { get; } +} +``` + +Describe DWM cloaking, dark-mode-aware default resolution, designer serialization, +top-level-window limitations, and conservative fallback behavior on unsupported systems. + +## Filing instruction + +Produce a complete proposal rather than an implementation plan. Clearly distinguish fixed +API shape from implementation choices that remain open for review. diff --git a/.github/Feature-Prompts/Net11/SuspendRelocationPaintingFlashPreventing/SuspendRelocationAndPainting-API-Feature-Prompt.md b/.github/Feature-Prompts/Net11/SuspendRelocationPaintingFlashPreventing/SuspendRelocationAndPainting-API-Feature-Prompt.md new file mode 100644 index 00000000000..35d765ad3ce --- /dev/null +++ b/.github/Feature-Prompts/Net11/SuspendRelocationPaintingFlashPreventing/SuspendRelocationAndPainting-API-Feature-Prompt.md @@ -0,0 +1,76 @@ +# Copilot Prompt 2 — Implement the flicker-free UI mutation APIs + +## Prerequisite + +Read the current upstream API suggestion before implementation. Treat its latest +`API Proposal` section as the public contract and report any conflict instead of silently +choosing a different shape. + +## Scope + +### A — painting suspension with optional layout traversal + +- Implement `ISupportSuspendPainting` on `Control` with ref-counted + `BeginSuspendPaintingCore` / `EndSuspendPaintingCore` hooks. +- Route `ListView`, `ListBox`, `ComboBox`, `TreeView`, and `RichTextBox` through their + existing `BeginUpdate` / `EndUpdate` mechanisms. +- Keep `SuspendPaintingScope` as a sealed, idempotent `IDisposable` class so it can span + `await`. +- Provide these extension methods: + + ```csharp + public static SuspendPaintingScope SuspendPainting( + this ISupportSuspendPainting target); + + public static SuspendPaintingScope SuspendPainting( + this ISupportSuspendPainting target, + LayoutSuspendTraversal layoutSuspendTraversal); + + public static SuspendPaintingScope SuspendPainting( + this ISupportSuspendPainting target, + Func suspendLayoutContainerFilter); + ``` + +- `LayoutSuspendTraversal` has `None`, `TopLevelOnly`, and `Traverse`. +- Snapshot selected controls before suspension. Suspend root-to-leaf and resume + deepest-first. +- The predicate is evaluated for the target and every descendant. A `false` result skips + suspension for that node without pruning its descendants. +- Suspend selected controls even when they currently have no children. +- Validate layout-aware calls before beginning painting. A non-`Control` target is invalid. +- Resume all selected layout scopes before ending painting so the recursive invalidation + occurs after layout. + +### B — deferred form reveal + +- Implement the approved `FormRevealMode`, `Form.FormRevealMode`, and + `Application` configuration APIs. +- Preserve classic behavior when deferred reveal is not active. +- Cloak eligible top-level form handles and uncloak according to the timing strategy in the + proposal. +- Keep unsupported DWM and window configurations inert and safe. + +## Engineering requirements + +- Match the current repository language version, nullable annotations, code style, and + interop conventions. +- Add XML documentation for every public or protected API. +- Update `PublicAPI.Unshipped.txt`. +- Keep state lazy where existing `Control` property-store patterns apply. +- Do not change existing public `BeginUpdate` / `EndUpdate` behavior. + +## Tests + +- Painting ref-count balance, nesting, idempotent disposal, handle creation, and handle + recreation. +- `None`, `TopLevelOnly`, and `Traverse` layout selection. +- Predicate selection, continued traversal after a rejected node, and empty containers. +- Deepest-first layout resume and recursive invalidation after layout. +- Null, invalid-enum, and non-`Control` target failures without partial suspension. +- Existing native update paths for the selected built-in controls. +- Classic/deferred form reveal behavior and unsupported-OS fallback. + +## Deliverable + +Provide production code, focused tests, updated API tracking, and an updated API proposal. +Call out any behavior that the implementation proves unsafe or unnecessarily costly. diff --git a/.github/copilot/Async/invokeAsync_generate_test_instructions.md b/.github/Feature-Prompts/Net9/Async/invokeAsync_generate_test_instructions.md similarity index 100% rename from .github/copilot/Async/invokeAsync_generate_test_instructions.md rename to .github/Feature-Prompts/Net9/Async/invokeAsync_generate_test_instructions.md diff --git a/.github/copilot/GDI/DarkModeButtonRendererCodeGenerationInstructions.md b/.github/Feature-Prompts/Net9/GDI/DarkModeButtonRendererCodeGenerationInstructions.md similarity index 100% rename from .github/copilot/GDI/DarkModeButtonRendererCodeGenerationInstructions.md rename to .github/Feature-Prompts/Net9/GDI/DarkModeButtonRendererCodeGenerationInstructions.md diff --git a/.github/skills/building-code/SKILL.md b/.github/skills/building-code/SKILL.md index 040dd437081..67fd8c7ba30 100644 --- a/.github/skills/building-code/SKILL.md +++ b/.github/skills/building-code/SKILL.md @@ -11,6 +11,22 @@ metadata: # Building the WinForms Repository +> ## 🛑 TENET — Build the solution ONLY with `build.cmd` +> +> **Never** build, validate, or declare the WinForms solution "clean" with a plain +> `dotnet build` / `dotnet msbuild` of `Winforms.sln`. Only **`build.cmd`** (Arcade) applies the +> repository's CI configuration — the **PublicAPI analyzer (RS0016/RS0017)**, the code-style and +> documentation analyzers, and **`-warnAsError`**. A plain `dotnet build` silently downgrades or +> skips these, so **"0 warnings" there does NOT mean CI is green** — the very same change can fail +> the official build with errors. +> +> * **Full / release / package / "is it clean?" verification → always `build.cmd`** (see §2). +> * A single-project `dotnet build` (see §3) is an **inner-loop convenience only**. It is fine while +> iterating, but you **must re-verify with `build.cmd` before claiming a change builds cleanly**. +> * If `build.cmd` cannot run in your environment, the closest fallback is +> `dotnet build /p:ContinuousIntegrationBuild=true /p:TreatWarningsAsErrors=true` — and +> you must say so explicitly rather than implying a `build.cmd` result. + ## Prerequisites * Windows is required for WinForms runtime scenarios, test execution, and Visual @@ -45,6 +61,12 @@ You can pass any extra `Build.ps1` flags after `Restore.cmd`, e.g. ## 2 Full Solution Build (preferred) +> **Always use `build.cmd` (Arcade) for full, release, and package builds.** Do **not** use a plain +> `dotnet build` of the solution for these — only `build.cmd` guarantees the Arcade-supported build +> options and the download of the correct base SDK (`global.json`) needed to compile. Plain +> `dotnet build` is reserved for the fast single-project inner loop (see Section 3), and even then +> only after at least one successful `build.cmd` / `Restore.cmd`. + ``` .\build.cmd ``` @@ -57,6 +79,56 @@ Under the hood this runs: eng\common\Build.ps1 -NativeToolsOnMachine -restore -build -bl ``` +### 2.1 Full (clean) test build — required workflow + +For a full, clean build of the whole solution, **clean the artifacts first, then build**: + +```powershell +# 1. Clean the artifacts folder. +.\build -clean + +# 2. Build the full solution. +.\build +``` + +**Reporting requirement:** a full build is long-running, so while it runs **report progress back to +the user in the console to bridge the wait and give early orientation.** As assemblies complete, +report which assemblies have been **built successfully** and which **failed and with how many +errors**. Prefer running the build with a binary log (the default `-bl`) and/or stream the console +output so per-project results can be surfaced as they happen rather than only at the end. + +### 2.2 Release build + +```powershell +.\build -configuration release +``` + +### 2.3 Creating packages + +```powershell +# Debug packages +.\build -pack + +# Release packages +.\build -configuration release -pack +``` + +### 2.4 Full `Build.ps1` parameter list + +`build.cmd` forwards every extra argument to `eng\common\Build.ps1`. The full surface is: + +``` +Build.ps1 [-configuration ] [-platform ] [-projects ] + [-verbosity ] [-msbuildEngine ] [-warnAsError ] + [-warnNotAsError ] [-nodeReuse ] [-buildCheck] [-restore] + [-deployDeps] [-build] [-rebuild] [-deploy] [-test] [-integrationTest] + [-performanceTest] [-sign] [-pack] [-publish] [-clean] [-productBuild] + [-fromVMR] [-binaryLog] [-binaryLogName ] [-excludeCIBinarylog] + [-ci] [-prepareMachine] [-runtimeSourceFeed ] + [-runtimeSourceFeedKey ] [-excludePrereleaseVS] + [-nativeToolsOnMachine] [-help] [-properties ] [] +``` + ### Common flags | Flag | Short | Description | @@ -90,6 +162,12 @@ eng\common\Build.ps1 -NativeToolsOnMachine -restore -build -bl ## 3 Optimized Building a Single Project (fast inner-loop) +> **Inner-loop only.** Use plain `dotnet build` of a single project **only** for quick iteration on +> one project, and **only after** at least one successful `.\build.cmd` / `.\Restore.cmd`. It does +> **not** guarantee the Arcade-supported build options or the download of the correct base SDK, so it +> must **never** be used for a full solution build, a release build, packaging, or any build whose +> result you intend to report as authoritative. For those, always use `build.cmd` (Section 2). + Prefer rebuilding just the project(s) with recent changes by using the standard `dotnet build` command, **after** at least one initial successful full restore (via `.\Restore.cmd` or `.\build.cmd`). diff --git a/.github/skills/control-api-tests/SKILL.md b/.github/skills/control-api-tests/SKILL.md index 5badcc2547c..ff4e9304230 100644 --- a/.github/skills/control-api-tests/SKILL.md +++ b/.github/skills/control-api-tests/SKILL.md @@ -66,6 +66,28 @@ The project uses **xUnit** with **FluentAssertions**. Key attributes: These are custom xUnit attributes that ensure tests run on an STA thread, which WinForms requires for COM interop and UI operations. +### 1.4 Async tests: pass a CancellationToken, respect `#nullable` + +The repository runs **xUnit v3** and enforces the relevant analyzers as **errors** under the CI +build (`build.cmd`). Two pitfalls fail CI even though a plain `dotnet build` may not flag them: + +* **CA2016 / xUnit1051 — always pass a `CancellationToken` to async calls.** Methods such as + `Task.Delay` must receive a token so a cancelled test run stops promptly. In xUnit v3 use + `TestContext.Current.CancellationToken`: + + ```csharp + await Task.Delay(25, TestContext.Current.CancellationToken); + ``` + + When you receive a `CancellationToken ct` (e.g. in a callback), **forward it** rather than dropping it. + +* **CS8632 — nullable annotations need a `#nullable` context.** If a test file uses `?` reference + annotations (e.g. `object? sender`) but the project does not enable nullable, add `#nullable enable` + at the top of the file (or remove the annotation). Match the surrounding files' convention. + +> Verify with `build.cmd` (CI parity) — see the `building-code` skill's build tenet. A plain +> single-project `dotnet build` can report these as 0 warnings while CI fails them as errors. + --- ## 2. Test Method Naming diff --git a/.github/skills/new-control-api/SKILL.md b/.github/skills/new-control-api/SKILL.md index ce6c58b6d2e..3030cf92ef7 100644 --- a/.github/skills/new-control-api/SKILL.md +++ b/.github/skills/new-control-api/SKILL.md @@ -148,7 +148,34 @@ System.Windows.Forms.MyEnum.Value2 = 1 -> System.Windows.Forms.MyEnum **Nullable annotations:** `?` = nullable reference, `!` = non-nullable reference. Value types do not carry these markers unless `Nullable`. -### 2.4 Publicly accessible interfaces +### 2.4 New `override` members must be tracked too + +The PublicAPI analyzer (RS0016) treats a **newly introduced `override`** of a public or +protected member as new API surface — even though the base member is already public. Whenever +you **add an `override` that did not previously exist on that type**, add a line for it to +`PublicAPI.Unshipped.txt` with the `override` prefix. This is easy to miss for paint/lifecycle +overrides added to support a feature. Examples: + +```text +override System.Windows.Forms.CheckBox.OnPaint(System.Windows.Forms.PaintEventArgs! pevent) -> void +override System.Windows.Forms.CheckBox.Dispose(bool disposing) -> void +override System.Windows.Forms.ButtonBase.OnVisualStylesModeChanged(System.EventArgs! e) -> void +``` + +> **CI catches this, a plain `dotnet build` may not.** RS0016 is enforced as an **error** under +> the CI/Arcade build (`build.cmd`); a single-project `dotnet build` can report it as 0 warnings. +> Always re-verify API tracking with `build.cmd` (see the `building-code` skill's build tenet). + +### 2.5 Related pitfalls when adding members to a control + +* **Hiding an inherited member (CS0114):** if your new member intentionally hides an inherited + one (e.g. a `private new bool ShouldSerializePadding()` shadowing `Control.ShouldSerializePadding()`), + you **must** use the `new` keyword, or the CI build fails. +* **`cref` to internal types in another assembly (CS1574):** XML-doc `` cannot + resolve a type that is `internal` in a *different* assembly (even via `InternalsVisibleTo`). Use + `TypeName` (plain code font) instead of a `cref` for such references. + +### 2.6 Publicly accessible interfaces If a new **public or protected interface** is introduced (or an existing one gains new members), every member that is publicly accessible must also appear @@ -468,51 +495,73 @@ protected virtual void OnMyPropertyChanged(EventArgs e) --- -## 7. .NET Version Guard — Mandatory +## 7. API Stability: Experimental vs. Stable — and Version Guards -All new public APIs **must** be guarded with a preprocessor directive for the -target .NET version. Currently, new APIs target at least **.NET 11**: +### 7.1 New APIs are STABLE by default — do NOT mark them `[Experimental]` -```csharp -#if NET11_0_OR_GREATER - /// - /// Gets or sets the corner radius for the control's border. - /// - public int CornerRadius - { - get => Properties.GetValueOrDefault(s_cornerRadiusProperty, 0); - set - { - ArgumentOutOfRangeException.ThrowIfNegative(value); +New public APIs ship as **normal, stable APIs by default**. Do **not** add the +`[Experimental(...)]` attribute, a `WFO5xxx` diagnostic ID, or `[WFO5xxx]` +PublicAPI prefixes unless the work item **explicitly** asks for an experimental +API. - if (Properties.GetValueOrDefault(s_cornerRadiusProperty, 0) != value) - { - Properties.AddOrRemoveValue(s_cornerRadiusProperty, value, defaultValue: 0); - OnCornerRadiusChanged(EventArgs.Empty); - } - } - } -#endif -``` +> **Never make an API experimental implicitly.** Experimental status is a +> deliberate, requested decision (it changes the customer contract and requires a +> diagnostic ID + suppression to consume). If the context does not explicitly call +> for it, the API is stable. + +### 7.2 When an experimental API *is* explicitly requested + +Only when the task explicitly requests an experimental API: + +1. Add (or reuse) a diagnostic ID in the `WFO500x` group in + `src\System.Windows.Forms.Analyzers\src\System\Windows\Forms\Analyzers\Diagnostics\DiagnosticIDs.cs` + (e.g. `ExperimentalDarkMode = "WFO5001"`, `ExperimentalAsync = "WFO5002"`, + `ExperimentalAsyncDropTarget = "WFO5003"`). New IDs continue the sequence. +2. Decorate the API: + ```csharp + [Experimental(DiagnosticIDs.ExperimentalXxx, UrlFormat = DiagnosticIDs.UrlFormat)] + ``` +3. Prefix every PublicAPI entry for that API with the diagnostic ID, e.g. + `[WFO5001]System.Windows.Forms.SomeNewApi.get -> ...`. +4. Add a row to **both** `docs\analyzers\Experimental.Help.md` and + `docs\list-of-diagnostics.md`. +5. Suppress the diagnostic where the framework itself consumes the API + (`#pragma warning disable WFOxxxx` / `#Disable Warning WFOxxxx` in VB). -> **Why?** Version guards ensure new APIs are only available on the .NET version -> they were approved for, preventing accidental use on older runtimes. The guard -> applies to the entire API surface: property, event, `On` method, and any -> associated types. +When the API later **graduates to stable** (typically the next release), reverse +all five steps: remove the attribute, the `[WFOxxxx]` PublicAPI prefixes, the +suppressions, the docs rows, and the unused diagnostic ID. -The matching tests must use the **same** preprocessor guard: +### 7.3 Version guards + +This repository **single-targets the current in-development .NET** (see +`TargetFramework` / `NetCurrent`), so source is **not** wrapped in +`#if NETxx_0_OR_GREATER` guards — there are none in `System.Windows.Forms`. Do +**not** add `#if NET11_0_OR_GREATER` blocks around new APIs. Add the member +directly: ```csharp -#if NET11_0_OR_GREATER - [WinFormsFact] - public void MyControl_CornerRadius_Set_GetReturnsExpected() +/// +/// Gets or sets the corner radius for the control's border. +/// +public int CornerRadius +{ + get => Properties.GetValueOrDefault(s_cornerRadiusProperty, 0); + set { - using MyControl control = new() { CornerRadius = 5 }; - Assert.Equal(5, control.CornerRadius); + ArgumentOutOfRangeException.ThrowIfNegative(value); + + if (Properties.GetValueOrDefault(s_cornerRadiusProperty, 0) != value) + { + Properties.AddOrRemoveValue(s_cornerRadiusProperty, value, defaultValue: 0); + OnCornerRadiusChanged(EventArgs.Empty); + } } -#endif +} ``` +Tests do not need a version guard either. + --- ## 8. Checklist Before Submitting @@ -521,7 +570,8 @@ Before considering the implementation complete, verify: * [ ] API proposal issue exists (upstream or fork) with full proposal format * [ ] All new public/protected members are in `PublicAPI.Unshipped.txt` -* [ ] New APIs guarded with `#if NET11_0_OR_GREATER` (or appropriate version) +* [ ] API is **stable** (no `[Experimental]`/`WFO5xxx`) unless experimental was + explicitly requested; no `#if NETxx_0_OR_GREATER` guards * [ ] Property values stored via `PropertyStore` (not backing fields) * [ ] Every property has a CodeDOM serialization strategy * [ ] Every property has `On[Property]Changed` + `[Property]Changed` event @@ -533,7 +583,7 @@ Before considering the implementation complete, verify: * [ ] XML documentation on every new public/protected member * [ ] Naming follows precedent on the control and its base classes * [ ] Publicly accessible interface members are tracked in PublicAPI files -* [ ] Unit tests cover the new API surface (with matching version guard) +* [ ] Unit tests cover the new API surface ### 8.1 API issue checklist diff --git a/docs/Net11Api_04_KioskManagerComponent.HighRiskReview.md b/docs/Net11Api_04_KioskManagerComponent.HighRiskReview.md new file mode 100644 index 00000000000..b3cb075a1a1 --- /dev/null +++ b/docs/Net11Api_04_KioskManagerComponent.HighRiskReview.md @@ -0,0 +1,38 @@ +# Net11Api_04 KioskModeManager High-Risk Review + +This document records review findings that require an API or lifecycle design decision before they can be patched safely. + +## FullScreen requested state and actual state differ + +`KioskModeManager.FullScreen` currently returns `true` when fullscreen has been requested but no target form is available. `FullScreenChanged`, however, is raised only when the resolved form actually enters or exits fullscreen. This creates two observable state models: + +- The property reports requested state through `_pendingFullScreen || _isFullScreen`. +- The event reports actual window state through changes to `_isFullScreen`. + +Consequently, setting `FullScreen = true` without a resolved form changes the property without raising the event. When a form later becomes available, the event is raised even though the property value remains `true`. + +Before changing this behavior, decide whether `FullScreen` represents requested state or actual window state. The decision must cover: + +- initialization and delayed parenting; +- missing, replaced, reparented, and disposed container controls; +- event ordering and two-way data binding; +- `ToggleFullScreen` behavior while a request is pending; +- disposal and failed fullscreen transitions; +- compatibility for applications already observing the property or event. + +Tests should define the complete transition matrix for requested, pending, entered, exited, reparented, and disposed states. + +## Session-notification registration is not retried + +`KioskModeFormObserver.RegisterSessionNotifications` records a failed `WTSRegisterSessionNotification` call as `false` and retries only after form handle recreation. During Windows startup, registration can fail with `RPC_S_INVALID_BINDING` before `Global\TermSrvReadyEvent` is signaled. A kiosk application started during that interval can therefore miss session notifications for the lifetime of its form handle. + +A safe fix needs a non-blocking retry design that specifies: + +- which Win32 errors are retryable and which are terminal; +- how waiting for `Global\TermSrvReadyEvent` is cancelled; +- how completion is marshalled to the form's UI thread; +- how stale callbacks are rejected after handle recreation; +- how registration and unregistration remain paired; +- how disposal races with an outstanding wait. + +Tests should cover service-not-ready startup, successful retry, handle recreation during the wait, and disposal before the wait completes. diff --git a/src/BuildAssist/BuildAssist.msbuildproj b/src/BuildAssist/BuildAssist.msbuildproj index 2e188f76ff6..400c78f44e6 100644 --- a/src/BuildAssist/BuildAssist.msbuildproj +++ b/src/BuildAssist/BuildAssist.msbuildproj @@ -37,7 +37,7 @@ System.Windows.Forms.ContainerControl? +System.Windows.Forms.KioskModeManager.ContainerControl.set -> void +System.Windows.Forms.KioskModeManager.ContainerControlChanged -> System.EventHandler? +override System.Windows.Forms.KioskModeManager.Dispose(bool disposing) -> void +System.Windows.Forms.KioskModeManager.EscapeExitsFullScreen.get -> bool +System.Windows.Forms.KioskModeManager.EscapeExitsFullScreen.set -> void +System.Windows.Forms.KioskModeManager.FullScreen.get -> bool +System.Windows.Forms.KioskModeManager.FullScreen.set -> void +System.Windows.Forms.KioskModeManager.FullScreenChanged -> System.EventHandler? +System.Windows.Forms.KioskModeManager.HideTaskbar.get -> bool +System.Windows.Forms.KioskModeManager.HideTaskbar.set -> void +System.Windows.Forms.KioskModeManager.KioskModeManager() -> void +System.Windows.Forms.KioskModeManager.KioskModeManager(System.ComponentModel.IContainer! container) -> void +System.Windows.Forms.KioskModeManager.MousePointerAutoHideDelay.get -> int +System.Windows.Forms.KioskModeManager.MousePointerAutoHideDelay.set -> void +virtual System.Windows.Forms.KioskModeManager.OnContainerControlChanged(System.EventArgs! e) -> void +virtual System.Windows.Forms.KioskModeManager.OnFullScreenChanged(System.EventArgs! e) -> void +virtual System.Windows.Forms.KioskModeManager.OnWakeup(System.Windows.Forms.KioskModeWakeupEventArgs! e) -> void +override System.Windows.Forms.KioskModeManager.Site.get -> System.ComponentModel.ISite? +override System.Windows.Forms.KioskModeManager.Site.set -> void +System.Windows.Forms.KioskModeManager.SuppressPowerSaving.get -> bool +System.Windows.Forms.KioskModeManager.SuppressPowerSaving.set -> void +System.Windows.Forms.KioskModeManager.ToggleFullScreen() -> void +System.Windows.Forms.KioskModeManager.ToggleFullScreenKey.get -> System.Windows.Forms.Keys +System.Windows.Forms.KioskModeManager.ToggleFullScreenKey.set -> void +System.Windows.Forms.KioskModeManager.TopMostInFullScreen.get -> bool +System.Windows.Forms.KioskModeManager.TopMostInFullScreen.set -> void +System.Windows.Forms.KioskModeManager.Wakeup -> System.Windows.Forms.KioskModeWakeupEventHandler? +System.Windows.Forms.KioskModeManager.WakeUpCommand.get -> System.Windows.Input.ICommand? +System.Windows.Forms.KioskModeManager.WakeUpCommand.set -> void +System.Windows.Forms.KioskModeWakeupEventArgs +System.Windows.Forms.KioskModeWakeupEventArgs.KioskModeWakeupEventArgs(System.Windows.Forms.KioskModeWakeupSource source) -> void +System.Windows.Forms.KioskModeWakeupEventArgs.Source.get -> System.Windows.Forms.KioskModeWakeupSource +System.Windows.Forms.KioskModeWakeupEventHandler +virtual System.Windows.Forms.KioskModeWakeupEventHandler.Invoke(object? sender, System.Windows.Forms.KioskModeWakeupEventArgs! e) -> void +System.Windows.Forms.KioskModeWakeupSource +System.Windows.Forms.KioskModeWakeupSource.Keyboard = 0 -> System.Windows.Forms.KioskModeWakeupSource +System.Windows.Forms.KioskModeWakeupSource.Mouse = 1 -> System.Windows.Forms.KioskModeWakeupSource +System.Windows.Forms.KioskModeWakeupSource.PowerResume = 2 -> System.Windows.Forms.KioskModeWakeupSource +System.Windows.Forms.KioskModeWakeupSource.Session = 3 -> System.Windows.Forms.KioskModeWakeupSource diff --git a/src/System.Windows.Forms/Resources/SR.resx b/src/System.Windows.Forms/Resources/SR.resx index 53d5073b748..650e07c9e20 100644 --- a/src/System.Windows.Forms/Resources/SR.resx +++ b/src/System.Windows.Forms/Resources/SR.resx @@ -2679,6 +2679,45 @@ To replace this default dialog please handle the DataError event. Manages a collection of images that are typically used by other controls such as ListView, TreeView, or ToolStrip. + + Manages common fullscreen, input, cursor, and power behavior for kiosk-style WinForms applications. + + + The container whose containing form is controlled by the kiosk mode manager. + + + Occurs when the ContainerControl property value changes. + + + Indicates whether pressing Escape exits fullscreen mode. + + + Gets or sets a value indicating whether the resolved form is currently fullscreen. + + + Occurs when the fullscreen state changes. + + + Indicates whether fullscreen mode covers the Windows taskbar. + + + The delay, in milliseconds, before the mouse pointer is hidden while fullscreen. A value of 0 disables automatic pointer hiding. + + + Indicates whether Windows should keep the display and system awake while this component is active. + + + The key that toggles between fullscreen and restored mode. + + + Indicates whether the form is topmost while fullscreen. + + + Occurs when keyboard, mouse, power resume, or session activity wakes the kiosk experience. + + + The command that is executed whenever the kiosk experience is woken. + Provides run-time information or descriptive text for a control. @@ -7045,4 +7084,4 @@ Stack trace where the illegal operation occurred was: The FormScreenCaptureMode property can only be changed on top-level Forms with their TopLevel property set to true. - \ No newline at end of file + diff --git a/src/System.Windows.Forms/Resources/xlf/SR.cs.xlf b/src/System.Windows.Forms/Resources/xlf/SR.cs.xlf index ce2bc9260f9..e839c13ce48 100644 --- a/src/System.Windows.Forms/Resources/xlf/SR.cs.xlf +++ b/src/System.Windows.Forms/Resources/xlf/SR.cs.xlf @@ -4519,6 +4519,11 @@ Chcete-li nahradit toto výchozí dialogové okno, nastavte popisovač události Spravuje kolekci obrázků, které standardně používají ovládací prvky, jako například ListView, TreeView nebo ToolStrip. + + Manages common fullscreen, input, cursor, and power behavior for kiosk-style WinForms applications. + Manages common fullscreen, input, cursor, and power behavior for kiosk-style WinForms applications. + + Provides run-time information or descriptive text for a control. Poskytuje běhové informace nebo popisný text pro ovládací prvek. @@ -6104,6 +6109,66 @@ Trasování zásobníku, kde došlo k neplatné operaci: Kombinace klíčů je neplatná. + + Occurs when the ContainerControl property value changes. + Occurs when the ContainerControl property value changes. + + + + The container whose containing form is controlled by the kiosk mode manager. + The container whose containing form is controlled by the kiosk mode manager. + + + + Indicates whether pressing Escape exits fullscreen mode. + Indicates whether pressing Escape exits fullscreen mode. + + + + Gets or sets a value indicating whether the resolved form is currently fullscreen. + Gets or sets a value indicating whether the resolved form is currently fullscreen. + + + + Occurs when the fullscreen state changes. + Occurs when the fullscreen state changes. + + + + Indicates whether fullscreen mode covers the Windows taskbar. + Indicates whether fullscreen mode covers the Windows taskbar. + + + + The delay, in milliseconds, before the mouse pointer is hidden while fullscreen. A value of 0 disables automatic pointer hiding. + The delay, in milliseconds, before the mouse pointer is hidden while fullscreen. A value of 0 disables automatic pointer hiding. + + + + Indicates whether Windows should keep the display and system awake while this component is active. + Indicates whether Windows should keep the display and system awake while this component is active. + + + + The key that toggles between fullscreen and restored mode. + The key that toggles between fullscreen and restored mode. + + + + Indicates whether the form is topmost while fullscreen. + Indicates whether the form is topmost while fullscreen. + + + + Occurs when keyboard, mouse, power resume, or session activity wakes the kiosk experience. + Occurs when keyboard, mouse, power resume, or session activity wakes the kiosk experience. + + + + The command that is executed whenever the kiosk experience is woken. + The command that is executed whenever the kiosk experience is woken. + + Enables the automatic handling of text that extends beyond the width of the label control. Umožňuje automatické zpracování textu, který je delší než šířka ovládacího prvku popisku. diff --git a/src/System.Windows.Forms/Resources/xlf/SR.de.xlf b/src/System.Windows.Forms/Resources/xlf/SR.de.xlf index b5cf47b1170..54b10c59f65 100644 --- a/src/System.Windows.Forms/Resources/xlf/SR.de.xlf +++ b/src/System.Windows.Forms/Resources/xlf/SR.de.xlf @@ -4519,6 +4519,11 @@ Behandeln Sie das DataError-Ereignis, um dieses Standarddialogfeld zu ersetzen.< Verwaltet eine Sammlung von Bildern, die in der Regel von anderen Steuerelementen wie ListView, TreeView oder ToolStrip verwendet werden. + + Manages common fullscreen, input, cursor, and power behavior for kiosk-style WinForms applications. + Manages common fullscreen, input, cursor, and power behavior for kiosk-style WinForms applications. + + Provides run-time information or descriptive text for a control. Stellt Laufzeitinformationen oder deskriptiven Text für ein Steuerelement bereit. @@ -6104,6 +6109,66 @@ Stapelüberwachung, in der der unzulässige Vorgang auftrat: Ungültige Schlüsselkombination. + + Occurs when the ContainerControl property value changes. + Occurs when the ContainerControl property value changes. + + + + The container whose containing form is controlled by the kiosk mode manager. + The container whose containing form is controlled by the kiosk mode manager. + + + + Indicates whether pressing Escape exits fullscreen mode. + Indicates whether pressing Escape exits fullscreen mode. + + + + Gets or sets a value indicating whether the resolved form is currently fullscreen. + Gets or sets a value indicating whether the resolved form is currently fullscreen. + + + + Occurs when the fullscreen state changes. + Occurs when the fullscreen state changes. + + + + Indicates whether fullscreen mode covers the Windows taskbar. + Indicates whether fullscreen mode covers the Windows taskbar. + + + + The delay, in milliseconds, before the mouse pointer is hidden while fullscreen. A value of 0 disables automatic pointer hiding. + The delay, in milliseconds, before the mouse pointer is hidden while fullscreen. A value of 0 disables automatic pointer hiding. + + + + Indicates whether Windows should keep the display and system awake while this component is active. + Indicates whether Windows should keep the display and system awake while this component is active. + + + + The key that toggles between fullscreen and restored mode. + The key that toggles between fullscreen and restored mode. + + + + Indicates whether the form is topmost while fullscreen. + Indicates whether the form is topmost while fullscreen. + + + + Occurs when keyboard, mouse, power resume, or session activity wakes the kiosk experience. + Occurs when keyboard, mouse, power resume, or session activity wakes the kiosk experience. + + + + The command that is executed whenever the kiosk experience is woken. + The command that is executed whenever the kiosk experience is woken. + + Enables the automatic handling of text that extends beyond the width of the label control. Aktiviert die automatische Behandlung von Text, der über die Breite des Bezeichnungssteuerelements hinausgeht. diff --git a/src/System.Windows.Forms/Resources/xlf/SR.es.xlf b/src/System.Windows.Forms/Resources/xlf/SR.es.xlf index 82eb0cd40bb..a2aeb3df177 100644 --- a/src/System.Windows.Forms/Resources/xlf/SR.es.xlf +++ b/src/System.Windows.Forms/Resources/xlf/SR.es.xlf @@ -4519,6 +4519,11 @@ Para reemplazar este cuadro de diálogo predeterminado controle el evento DataEr Controla una colección de imágenes que suelen utilizar otros controles como ListView, TreeView o ToolStrip. + + Manages common fullscreen, input, cursor, and power behavior for kiosk-style WinForms applications. + Manages common fullscreen, input, cursor, and power behavior for kiosk-style WinForms applications. + + Provides run-time information or descriptive text for a control. Proporciona información en tiempo de ejecución o texto descriptivo para un control. @@ -6104,6 +6109,66 @@ El seguimiento de la pila donde tuvo lugar la operación no válida fue: La combinación de teclas no es válida. + + Occurs when the ContainerControl property value changes. + Occurs when the ContainerControl property value changes. + + + + The container whose containing form is controlled by the kiosk mode manager. + The container whose containing form is controlled by the kiosk mode manager. + + + + Indicates whether pressing Escape exits fullscreen mode. + Indicates whether pressing Escape exits fullscreen mode. + + + + Gets or sets a value indicating whether the resolved form is currently fullscreen. + Gets or sets a value indicating whether the resolved form is currently fullscreen. + + + + Occurs when the fullscreen state changes. + Occurs when the fullscreen state changes. + + + + Indicates whether fullscreen mode covers the Windows taskbar. + Indicates whether fullscreen mode covers the Windows taskbar. + + + + The delay, in milliseconds, before the mouse pointer is hidden while fullscreen. A value of 0 disables automatic pointer hiding. + The delay, in milliseconds, before the mouse pointer is hidden while fullscreen. A value of 0 disables automatic pointer hiding. + + + + Indicates whether Windows should keep the display and system awake while this component is active. + Indicates whether Windows should keep the display and system awake while this component is active. + + + + The key that toggles between fullscreen and restored mode. + The key that toggles between fullscreen and restored mode. + + + + Indicates whether the form is topmost while fullscreen. + Indicates whether the form is topmost while fullscreen. + + + + Occurs when keyboard, mouse, power resume, or session activity wakes the kiosk experience. + Occurs when keyboard, mouse, power resume, or session activity wakes the kiosk experience. + + + + The command that is executed whenever the kiosk experience is woken. + The command that is executed whenever the kiosk experience is woken. + + Enables the automatic handling of text that extends beyond the width of the label control. Permite el control automático del texto que se extiende más allá del ancho del control de la etiqueta. diff --git a/src/System.Windows.Forms/Resources/xlf/SR.fr.xlf b/src/System.Windows.Forms/Resources/xlf/SR.fr.xlf index f02c36b8e65..c2f6d9b79b8 100644 --- a/src/System.Windows.Forms/Resources/xlf/SR.fr.xlf +++ b/src/System.Windows.Forms/Resources/xlf/SR.fr.xlf @@ -4519,6 +4519,11 @@ Pour remplacer cette boîte de dialogue par défaut, traitez l'événement DataE Gère une collection d'images qui sont généralement utilisées par d'autres contrôles tels que ListView, TreeView et ToolStrip. + + Manages common fullscreen, input, cursor, and power behavior for kiosk-style WinForms applications. + Manages common fullscreen, input, cursor, and power behavior for kiosk-style WinForms applications. + + Provides run-time information or descriptive text for a control. Fournit des informations d'exécution ou un texte descriptif pour un contrôle. @@ -6104,6 +6109,66 @@ Cette opération non conforme s'est produite sur la trace de la pile : La combinaison de touches n'est pas valide. + + Occurs when the ContainerControl property value changes. + Occurs when the ContainerControl property value changes. + + + + The container whose containing form is controlled by the kiosk mode manager. + The container whose containing form is controlled by the kiosk mode manager. + + + + Indicates whether pressing Escape exits fullscreen mode. + Indicates whether pressing Escape exits fullscreen mode. + + + + Gets or sets a value indicating whether the resolved form is currently fullscreen. + Gets or sets a value indicating whether the resolved form is currently fullscreen. + + + + Occurs when the fullscreen state changes. + Occurs when the fullscreen state changes. + + + + Indicates whether fullscreen mode covers the Windows taskbar. + Indicates whether fullscreen mode covers the Windows taskbar. + + + + The delay, in milliseconds, before the mouse pointer is hidden while fullscreen. A value of 0 disables automatic pointer hiding. + The delay, in milliseconds, before the mouse pointer is hidden while fullscreen. A value of 0 disables automatic pointer hiding. + + + + Indicates whether Windows should keep the display and system awake while this component is active. + Indicates whether Windows should keep the display and system awake while this component is active. + + + + The key that toggles between fullscreen and restored mode. + The key that toggles between fullscreen and restored mode. + + + + Indicates whether the form is topmost while fullscreen. + Indicates whether the form is topmost while fullscreen. + + + + Occurs when keyboard, mouse, power resume, or session activity wakes the kiosk experience. + Occurs when keyboard, mouse, power resume, or session activity wakes the kiosk experience. + + + + The command that is executed whenever the kiosk experience is woken. + The command that is executed whenever the kiosk experience is woken. + + Enables the automatic handling of text that extends beyond the width of the label control. Active la gestion automatique du texte qui dépasse les limites de largeur du contrôle label. diff --git a/src/System.Windows.Forms/Resources/xlf/SR.it.xlf b/src/System.Windows.Forms/Resources/xlf/SR.it.xlf index fc9f709a0be..4e9d5d5ac3e 100644 --- a/src/System.Windows.Forms/Resources/xlf/SR.it.xlf +++ b/src/System.Windows.Forms/Resources/xlf/SR.it.xlf @@ -4519,6 +4519,11 @@ Per sostituire questa finestra di dialogo, gestisci l'evento DataError. Gestisce una raccolta di immagini utilizzate in genere da altri controlli, quali ListView, TreeView o ToolStrip. + + Manages common fullscreen, input, cursor, and power behavior for kiosk-style WinForms applications. + Manages common fullscreen, input, cursor, and power behavior for kiosk-style WinForms applications. + + Provides run-time information or descriptive text for a control. Fornisce informazioni di runtime o testo descrittivo per un controllo. @@ -6104,6 +6109,66 @@ Analisi dello stack dove si è verificata l'operazione non valida: Combinazione di tasti non valida. + + Occurs when the ContainerControl property value changes. + Occurs when the ContainerControl property value changes. + + + + The container whose containing form is controlled by the kiosk mode manager. + The container whose containing form is controlled by the kiosk mode manager. + + + + Indicates whether pressing Escape exits fullscreen mode. + Indicates whether pressing Escape exits fullscreen mode. + + + + Gets or sets a value indicating whether the resolved form is currently fullscreen. + Gets or sets a value indicating whether the resolved form is currently fullscreen. + + + + Occurs when the fullscreen state changes. + Occurs when the fullscreen state changes. + + + + Indicates whether fullscreen mode covers the Windows taskbar. + Indicates whether fullscreen mode covers the Windows taskbar. + + + + The delay, in milliseconds, before the mouse pointer is hidden while fullscreen. A value of 0 disables automatic pointer hiding. + The delay, in milliseconds, before the mouse pointer is hidden while fullscreen. A value of 0 disables automatic pointer hiding. + + + + Indicates whether Windows should keep the display and system awake while this component is active. + Indicates whether Windows should keep the display and system awake while this component is active. + + + + The key that toggles between fullscreen and restored mode. + The key that toggles between fullscreen and restored mode. + + + + Indicates whether the form is topmost while fullscreen. + Indicates whether the form is topmost while fullscreen. + + + + Occurs when keyboard, mouse, power resume, or session activity wakes the kiosk experience. + Occurs when keyboard, mouse, power resume, or session activity wakes the kiosk experience. + + + + The command that is executed whenever the kiosk experience is woken. + The command that is executed whenever the kiosk experience is woken. + + Enables the automatic handling of text that extends beyond the width of the label control. Consente la gestione automatica del testo che non è contenuto all'interno della larghezza del controllo etichetta. diff --git a/src/System.Windows.Forms/Resources/xlf/SR.ja.xlf b/src/System.Windows.Forms/Resources/xlf/SR.ja.xlf index 4511ba6cf76..6582377a233 100644 --- a/src/System.Windows.Forms/Resources/xlf/SR.ja.xlf +++ b/src/System.Windows.Forms/Resources/xlf/SR.ja.xlf @@ -4519,6 +4519,11 @@ To replace this default dialog please handle the DataError event. ListView、TreeView、または ToolStrip のような他のコントロールが通常使用するイメージのコレクションを管理します。 + + Manages common fullscreen, input, cursor, and power behavior for kiosk-style WinForms applications. + Manages common fullscreen, input, cursor, and power behavior for kiosk-style WinForms applications. + + Provides run-time information or descriptive text for a control. コントロールの実行時の情報または説明用のテキストを提供します。 @@ -6104,6 +6109,66 @@ Stack trace where the illegal operation occurred was: キーの組み合わせが有効ではありません。 + + Occurs when the ContainerControl property value changes. + Occurs when the ContainerControl property value changes. + + + + The container whose containing form is controlled by the kiosk mode manager. + The container whose containing form is controlled by the kiosk mode manager. + + + + Indicates whether pressing Escape exits fullscreen mode. + Indicates whether pressing Escape exits fullscreen mode. + + + + Gets or sets a value indicating whether the resolved form is currently fullscreen. + Gets or sets a value indicating whether the resolved form is currently fullscreen. + + + + Occurs when the fullscreen state changes. + Occurs when the fullscreen state changes. + + + + Indicates whether fullscreen mode covers the Windows taskbar. + Indicates whether fullscreen mode covers the Windows taskbar. + + + + The delay, in milliseconds, before the mouse pointer is hidden while fullscreen. A value of 0 disables automatic pointer hiding. + The delay, in milliseconds, before the mouse pointer is hidden while fullscreen. A value of 0 disables automatic pointer hiding. + + + + Indicates whether Windows should keep the display and system awake while this component is active. + Indicates whether Windows should keep the display and system awake while this component is active. + + + + The key that toggles between fullscreen and restored mode. + The key that toggles between fullscreen and restored mode. + + + + Indicates whether the form is topmost while fullscreen. + Indicates whether the form is topmost while fullscreen. + + + + Occurs when keyboard, mouse, power resume, or session activity wakes the kiosk experience. + Occurs when keyboard, mouse, power resume, or session activity wakes the kiosk experience. + + + + The command that is executed whenever the kiosk experience is woken. + The command that is executed whenever the kiosk experience is woken. + + Enables the automatic handling of text that extends beyond the width of the label control. ラベル コントロールの幅を越えるテキストの自動処理を有効にします。 diff --git a/src/System.Windows.Forms/Resources/xlf/SR.ko.xlf b/src/System.Windows.Forms/Resources/xlf/SR.ko.xlf index 2924f72aab7..d8ab2c31f24 100644 --- a/src/System.Windows.Forms/Resources/xlf/SR.ko.xlf +++ b/src/System.Windows.Forms/Resources/xlf/SR.ko.xlf @@ -4519,6 +4519,11 @@ To replace this default dialog please handle the DataError event. ListView, TreeView 또는 ToolStrip과 같은 다른 컨트롤에서 일반적으로 사용하는 이미지 컬렉션을 관리합니다. + + Manages common fullscreen, input, cursor, and power behavior for kiosk-style WinForms applications. + Manages common fullscreen, input, cursor, and power behavior for kiosk-style WinForms applications. + + Provides run-time information or descriptive text for a control. 컨트롤에 대한 설명 텍스트나 런타임 정보를 제공합니다. @@ -6104,6 +6109,66 @@ Stack trace where the illegal operation occurred was: 키 조합이 잘못되었습니다. + + Occurs when the ContainerControl property value changes. + Occurs when the ContainerControl property value changes. + + + + The container whose containing form is controlled by the kiosk mode manager. + The container whose containing form is controlled by the kiosk mode manager. + + + + Indicates whether pressing Escape exits fullscreen mode. + Indicates whether pressing Escape exits fullscreen mode. + + + + Gets or sets a value indicating whether the resolved form is currently fullscreen. + Gets or sets a value indicating whether the resolved form is currently fullscreen. + + + + Occurs when the fullscreen state changes. + Occurs when the fullscreen state changes. + + + + Indicates whether fullscreen mode covers the Windows taskbar. + Indicates whether fullscreen mode covers the Windows taskbar. + + + + The delay, in milliseconds, before the mouse pointer is hidden while fullscreen. A value of 0 disables automatic pointer hiding. + The delay, in milliseconds, before the mouse pointer is hidden while fullscreen. A value of 0 disables automatic pointer hiding. + + + + Indicates whether Windows should keep the display and system awake while this component is active. + Indicates whether Windows should keep the display and system awake while this component is active. + + + + The key that toggles between fullscreen and restored mode. + The key that toggles between fullscreen and restored mode. + + + + Indicates whether the form is topmost while fullscreen. + Indicates whether the form is topmost while fullscreen. + + + + Occurs when keyboard, mouse, power resume, or session activity wakes the kiosk experience. + Occurs when keyboard, mouse, power resume, or session activity wakes the kiosk experience. + + + + The command that is executed whenever the kiosk experience is woken. + The command that is executed whenever the kiosk experience is woken. + + Enables the automatic handling of text that extends beyond the width of the label control. 레이블 컨트롤의 너비보다 큰 텍스트를 자동으로 처리합니다. diff --git a/src/System.Windows.Forms/Resources/xlf/SR.pl.xlf b/src/System.Windows.Forms/Resources/xlf/SR.pl.xlf index f86297cb7f6..a8779547946 100644 --- a/src/System.Windows.Forms/Resources/xlf/SR.pl.xlf +++ b/src/System.Windows.Forms/Resources/xlf/SR.pl.xlf @@ -4519,6 +4519,11 @@ Aby zamienić to domyślne okno dialogowe, obsłuż zdarzenie DataError.Zarządza kolekcją obrazów, które są zazwyczaj używane przez inne formanty, takie jak ListView, TreeView lub ToolStrip. + + Manages common fullscreen, input, cursor, and power behavior for kiosk-style WinForms applications. + Manages common fullscreen, input, cursor, and power behavior for kiosk-style WinForms applications. + + Provides run-time information or descriptive text for a control. Wyświetla informacje o wykonaniu lub tekst opisowy dla formantu. @@ -6104,6 +6109,66 @@ Stos śledzenia, w którym wystąpiła zabroniona operacja: Kombinacja kluczy jest nieprawidłowa. + + Occurs when the ContainerControl property value changes. + Occurs when the ContainerControl property value changes. + + + + The container whose containing form is controlled by the kiosk mode manager. + The container whose containing form is controlled by the kiosk mode manager. + + + + Indicates whether pressing Escape exits fullscreen mode. + Indicates whether pressing Escape exits fullscreen mode. + + + + Gets or sets a value indicating whether the resolved form is currently fullscreen. + Gets or sets a value indicating whether the resolved form is currently fullscreen. + + + + Occurs when the fullscreen state changes. + Occurs when the fullscreen state changes. + + + + Indicates whether fullscreen mode covers the Windows taskbar. + Indicates whether fullscreen mode covers the Windows taskbar. + + + + The delay, in milliseconds, before the mouse pointer is hidden while fullscreen. A value of 0 disables automatic pointer hiding. + The delay, in milliseconds, before the mouse pointer is hidden while fullscreen. A value of 0 disables automatic pointer hiding. + + + + Indicates whether Windows should keep the display and system awake while this component is active. + Indicates whether Windows should keep the display and system awake while this component is active. + + + + The key that toggles between fullscreen and restored mode. + The key that toggles between fullscreen and restored mode. + + + + Indicates whether the form is topmost while fullscreen. + Indicates whether the form is topmost while fullscreen. + + + + Occurs when keyboard, mouse, power resume, or session activity wakes the kiosk experience. + Occurs when keyboard, mouse, power resume, or session activity wakes the kiosk experience. + + + + The command that is executed whenever the kiosk experience is woken. + The command that is executed whenever the kiosk experience is woken. + + Enables the automatic handling of text that extends beyond the width of the label control. Włącza automatyczną obsługę tekstu, który mieści się w szerokości formantu etykiety. diff --git a/src/System.Windows.Forms/Resources/xlf/SR.pt-BR.xlf b/src/System.Windows.Forms/Resources/xlf/SR.pt-BR.xlf index 5dcda8ac793..21c36f13fb4 100644 --- a/src/System.Windows.Forms/Resources/xlf/SR.pt-BR.xlf +++ b/src/System.Windows.Forms/Resources/xlf/SR.pt-BR.xlf @@ -4519,6 +4519,11 @@ Para substituir a caixa de diálogo padrão, manipule o evento DataError.Gerencia uma coleção de imagens geralmente usadas por outros controles, como ListView, TreeView ou ToolStrip. + + Manages common fullscreen, input, cursor, and power behavior for kiosk-style WinForms applications. + Manages common fullscreen, input, cursor, and power behavior for kiosk-style WinForms applications. + + Provides run-time information or descriptive text for a control. Fornece informações em tempo de execução ou texto descritivo para um controle. @@ -6104,6 +6109,66 @@ O rastreamento de pilha em que a operação ilegal ocorreu foi: A combinação de chave não é válida. + + Occurs when the ContainerControl property value changes. + Occurs when the ContainerControl property value changes. + + + + The container whose containing form is controlled by the kiosk mode manager. + The container whose containing form is controlled by the kiosk mode manager. + + + + Indicates whether pressing Escape exits fullscreen mode. + Indicates whether pressing Escape exits fullscreen mode. + + + + Gets or sets a value indicating whether the resolved form is currently fullscreen. + Gets or sets a value indicating whether the resolved form is currently fullscreen. + + + + Occurs when the fullscreen state changes. + Occurs when the fullscreen state changes. + + + + Indicates whether fullscreen mode covers the Windows taskbar. + Indicates whether fullscreen mode covers the Windows taskbar. + + + + The delay, in milliseconds, before the mouse pointer is hidden while fullscreen. A value of 0 disables automatic pointer hiding. + The delay, in milliseconds, before the mouse pointer is hidden while fullscreen. A value of 0 disables automatic pointer hiding. + + + + Indicates whether Windows should keep the display and system awake while this component is active. + Indicates whether Windows should keep the display and system awake while this component is active. + + + + The key that toggles between fullscreen and restored mode. + The key that toggles between fullscreen and restored mode. + + + + Indicates whether the form is topmost while fullscreen. + Indicates whether the form is topmost while fullscreen. + + + + Occurs when keyboard, mouse, power resume, or session activity wakes the kiosk experience. + Occurs when keyboard, mouse, power resume, or session activity wakes the kiosk experience. + + + + The command that is executed whenever the kiosk experience is woken. + The command that is executed whenever the kiosk experience is woken. + + Enables the automatic handling of text that extends beyond the width of the label control. Permite a manipulação automática de texto que ultrapassa a largura do controle de rótulo. diff --git a/src/System.Windows.Forms/Resources/xlf/SR.ru.xlf b/src/System.Windows.Forms/Resources/xlf/SR.ru.xlf index 89af4e75583..0724e2c1995 100644 --- a/src/System.Windows.Forms/Resources/xlf/SR.ru.xlf +++ b/src/System.Windows.Forms/Resources/xlf/SR.ru.xlf @@ -4519,6 +4519,11 @@ To replace this default dialog please handle the DataError event. Управляет коллекцией изображений, которые обычно используются другими элементами управления (например, ListView, TreeView или ToolStrip). + + Manages common fullscreen, input, cursor, and power behavior for kiosk-style WinForms applications. + Manages common fullscreen, input, cursor, and power behavior for kiosk-style WinForms applications. + + Provides run-time information or descriptive text for a control. Предоставляет элементу управления текст описания либо информацию во время выполнения. @@ -6104,6 +6109,66 @@ Stack trace where the illegal operation occurred was: Недопустимая комбинация ключа. + + Occurs when the ContainerControl property value changes. + Occurs when the ContainerControl property value changes. + + + + The container whose containing form is controlled by the kiosk mode manager. + The container whose containing form is controlled by the kiosk mode manager. + + + + Indicates whether pressing Escape exits fullscreen mode. + Indicates whether pressing Escape exits fullscreen mode. + + + + Gets or sets a value indicating whether the resolved form is currently fullscreen. + Gets or sets a value indicating whether the resolved form is currently fullscreen. + + + + Occurs when the fullscreen state changes. + Occurs when the fullscreen state changes. + + + + Indicates whether fullscreen mode covers the Windows taskbar. + Indicates whether fullscreen mode covers the Windows taskbar. + + + + The delay, in milliseconds, before the mouse pointer is hidden while fullscreen. A value of 0 disables automatic pointer hiding. + The delay, in milliseconds, before the mouse pointer is hidden while fullscreen. A value of 0 disables automatic pointer hiding. + + + + Indicates whether Windows should keep the display and system awake while this component is active. + Indicates whether Windows should keep the display and system awake while this component is active. + + + + The key that toggles between fullscreen and restored mode. + The key that toggles between fullscreen and restored mode. + + + + Indicates whether the form is topmost while fullscreen. + Indicates whether the form is topmost while fullscreen. + + + + Occurs when keyboard, mouse, power resume, or session activity wakes the kiosk experience. + Occurs when keyboard, mouse, power resume, or session activity wakes the kiosk experience. + + + + The command that is executed whenever the kiosk experience is woken. + The command that is executed whenever the kiosk experience is woken. + + Enables the automatic handling of text that extends beyond the width of the label control. Включает автоматическую обработку текста, выходящего за пределы ширины элемента управления с подписью. diff --git a/src/System.Windows.Forms/Resources/xlf/SR.tr.xlf b/src/System.Windows.Forms/Resources/xlf/SR.tr.xlf index 9e72305a890..ef83edf49a1 100644 --- a/src/System.Windows.Forms/Resources/xlf/SR.tr.xlf +++ b/src/System.Windows.Forms/Resources/xlf/SR.tr.xlf @@ -4519,6 +4519,11 @@ Bu varsayılan iletişim kutusunu değiştirmek için, lütfen DataError olayın Genellikle ListView, TreeView veya ToolStrip gibi diğer denetimler tarafından kullanılan resim koleksiyonunu düzenler. + + Manages common fullscreen, input, cursor, and power behavior for kiosk-style WinForms applications. + Manages common fullscreen, input, cursor, and power behavior for kiosk-style WinForms applications. + + Provides run-time information or descriptive text for a control. Bir denetim için çalışma zamanı bilgileri veya açıklayıcı metin sağlar. @@ -6104,6 +6109,66 @@ Geçersiz işlemin gerçekleştiği yığın izi: Anahtar birleşimi geçerli değil. + + Occurs when the ContainerControl property value changes. + Occurs when the ContainerControl property value changes. + + + + The container whose containing form is controlled by the kiosk mode manager. + The container whose containing form is controlled by the kiosk mode manager. + + + + Indicates whether pressing Escape exits fullscreen mode. + Indicates whether pressing Escape exits fullscreen mode. + + + + Gets or sets a value indicating whether the resolved form is currently fullscreen. + Gets or sets a value indicating whether the resolved form is currently fullscreen. + + + + Occurs when the fullscreen state changes. + Occurs when the fullscreen state changes. + + + + Indicates whether fullscreen mode covers the Windows taskbar. + Indicates whether fullscreen mode covers the Windows taskbar. + + + + The delay, in milliseconds, before the mouse pointer is hidden while fullscreen. A value of 0 disables automatic pointer hiding. + The delay, in milliseconds, before the mouse pointer is hidden while fullscreen. A value of 0 disables automatic pointer hiding. + + + + Indicates whether Windows should keep the display and system awake while this component is active. + Indicates whether Windows should keep the display and system awake while this component is active. + + + + The key that toggles between fullscreen and restored mode. + The key that toggles between fullscreen and restored mode. + + + + Indicates whether the form is topmost while fullscreen. + Indicates whether the form is topmost while fullscreen. + + + + Occurs when keyboard, mouse, power resume, or session activity wakes the kiosk experience. + Occurs when keyboard, mouse, power resume, or session activity wakes the kiosk experience. + + + + The command that is executed whenever the kiosk experience is woken. + The command that is executed whenever the kiosk experience is woken. + + Enables the automatic handling of text that extends beyond the width of the label control. Etiket denetimi genişliğini aşan metnin otomatik işlenmesini sağlar. diff --git a/src/System.Windows.Forms/Resources/xlf/SR.zh-Hans.xlf b/src/System.Windows.Forms/Resources/xlf/SR.zh-Hans.xlf index 6827f20269f..21a4bbd7ef5 100644 --- a/src/System.Windows.Forms/Resources/xlf/SR.zh-Hans.xlf +++ b/src/System.Windows.Forms/Resources/xlf/SR.zh-Hans.xlf @@ -4519,6 +4519,11 @@ To replace this default dialog please handle the DataError event. 管理通常由其他控件(如 ListView、TreeView 或 ToolStrip)使用的图像集合。 + + Manages common fullscreen, input, cursor, and power behavior for kiosk-style WinForms applications. + Manages common fullscreen, input, cursor, and power behavior for kiosk-style WinForms applications. + + Provides run-time information or descriptive text for a control. 为控件提供运行时信息或说明性文字。 @@ -6104,6 +6109,66 @@ Stack trace where the illegal operation occurred was: 组合键无效。 + + Occurs when the ContainerControl property value changes. + Occurs when the ContainerControl property value changes. + + + + The container whose containing form is controlled by the kiosk mode manager. + The container whose containing form is controlled by the kiosk mode manager. + + + + Indicates whether pressing Escape exits fullscreen mode. + Indicates whether pressing Escape exits fullscreen mode. + + + + Gets or sets a value indicating whether the resolved form is currently fullscreen. + Gets or sets a value indicating whether the resolved form is currently fullscreen. + + + + Occurs when the fullscreen state changes. + Occurs when the fullscreen state changes. + + + + Indicates whether fullscreen mode covers the Windows taskbar. + Indicates whether fullscreen mode covers the Windows taskbar. + + + + The delay, in milliseconds, before the mouse pointer is hidden while fullscreen. A value of 0 disables automatic pointer hiding. + The delay, in milliseconds, before the mouse pointer is hidden while fullscreen. A value of 0 disables automatic pointer hiding. + + + + Indicates whether Windows should keep the display and system awake while this component is active. + Indicates whether Windows should keep the display and system awake while this component is active. + + + + The key that toggles between fullscreen and restored mode. + The key that toggles between fullscreen and restored mode. + + + + Indicates whether the form is topmost while fullscreen. + Indicates whether the form is topmost while fullscreen. + + + + Occurs when keyboard, mouse, power resume, or session activity wakes the kiosk experience. + Occurs when keyboard, mouse, power resume, or session activity wakes the kiosk experience. + + + + The command that is executed whenever the kiosk experience is woken. + The command that is executed whenever the kiosk experience is woken. + + Enables the automatic handling of text that extends beyond the width of the label control. 启用对扩展到标签控件宽度以外的文本的自动处理。 diff --git a/src/System.Windows.Forms/Resources/xlf/SR.zh-Hant.xlf b/src/System.Windows.Forms/Resources/xlf/SR.zh-Hant.xlf index 15f5f06c126..18dad56f9ac 100644 --- a/src/System.Windows.Forms/Resources/xlf/SR.zh-Hant.xlf +++ b/src/System.Windows.Forms/Resources/xlf/SR.zh-Hant.xlf @@ -4519,6 +4519,11 @@ To replace this default dialog please handle the DataError event. 管理通常由其他控制項 (例如 ListView、TreeView 或 ToolStrip) 使用的影像集合。 + + Manages common fullscreen, input, cursor, and power behavior for kiosk-style WinForms applications. + Manages common fullscreen, input, cursor, and power behavior for kiosk-style WinForms applications. + + Provides run-time information or descriptive text for a control. 提供控制項的執行階段資訊或描述文字。 @@ -6104,6 +6109,66 @@ Stack trace where the illegal operation occurred was: 組合鍵無效。 + + Occurs when the ContainerControl property value changes. + Occurs when the ContainerControl property value changes. + + + + The container whose containing form is controlled by the kiosk mode manager. + The container whose containing form is controlled by the kiosk mode manager. + + + + Indicates whether pressing Escape exits fullscreen mode. + Indicates whether pressing Escape exits fullscreen mode. + + + + Gets or sets a value indicating whether the resolved form is currently fullscreen. + Gets or sets a value indicating whether the resolved form is currently fullscreen. + + + + Occurs when the fullscreen state changes. + Occurs when the fullscreen state changes. + + + + Indicates whether fullscreen mode covers the Windows taskbar. + Indicates whether fullscreen mode covers the Windows taskbar. + + + + The delay, in milliseconds, before the mouse pointer is hidden while fullscreen. A value of 0 disables automatic pointer hiding. + The delay, in milliseconds, before the mouse pointer is hidden while fullscreen. A value of 0 disables automatic pointer hiding. + + + + Indicates whether Windows should keep the display and system awake while this component is active. + Indicates whether Windows should keep the display and system awake while this component is active. + + + + The key that toggles between fullscreen and restored mode. + The key that toggles between fullscreen and restored mode. + + + + Indicates whether the form is topmost while fullscreen. + Indicates whether the form is topmost while fullscreen. + + + + Occurs when keyboard, mouse, power resume, or session activity wakes the kiosk experience. + Occurs when keyboard, mouse, power resume, or session activity wakes the kiosk experience. + + + + The command that is executed whenever the kiosk experience is woken. + The command that is executed whenever the kiosk experience is woken. + + Enables the automatic handling of text that extends beyond the width of the label control. 啟用自動處理,以處理超出標籤控制項寬度的文件。 diff --git a/src/System.Windows.Forms/System/Windows/Forms/ErrorProvider/ErrorBlinkStyle.cs b/src/System.Windows.Forms/System/Windows/Forms/Components/ErrorProvider/ErrorBlinkStyle.cs similarity index 100% rename from src/System.Windows.Forms/System/Windows/Forms/ErrorProvider/ErrorBlinkStyle.cs rename to src/System.Windows.Forms/System/Windows/Forms/Components/ErrorProvider/ErrorBlinkStyle.cs diff --git a/src/System.Windows.Forms/System/Windows/Forms/ErrorProvider/ErrorIconAlignment.cs b/src/System.Windows.Forms/System/Windows/Forms/Components/ErrorProvider/ErrorIconAlignment.cs similarity index 100% rename from src/System.Windows.Forms/System/Windows/Forms/ErrorProvider/ErrorIconAlignment.cs rename to src/System.Windows.Forms/System/Windows/Forms/Components/ErrorProvider/ErrorIconAlignment.cs diff --git a/src/System.Windows.Forms/System/Windows/Forms/ErrorProvider/ErrorProvider.ControlItem.ControlItemAccessibleObject.cs b/src/System.Windows.Forms/System/Windows/Forms/Components/ErrorProvider/ErrorProvider.ControlItem.ControlItemAccessibleObject.cs similarity index 100% rename from src/System.Windows.Forms/System/Windows/Forms/ErrorProvider/ErrorProvider.ControlItem.ControlItemAccessibleObject.cs rename to src/System.Windows.Forms/System/Windows/Forms/Components/ErrorProvider/ErrorProvider.ControlItem.ControlItemAccessibleObject.cs diff --git a/src/System.Windows.Forms/System/Windows/Forms/ErrorProvider/ErrorProvider.ControlItem.cs b/src/System.Windows.Forms/System/Windows/Forms/Components/ErrorProvider/ErrorProvider.ControlItem.cs similarity index 100% rename from src/System.Windows.Forms/System/Windows/Forms/ErrorProvider/ErrorProvider.ControlItem.cs rename to src/System.Windows.Forms/System/Windows/Forms/Components/ErrorProvider/ErrorProvider.ControlItem.cs diff --git a/src/System.Windows.Forms/System/Windows/Forms/ErrorProvider/ErrorProvider.ErrorProviderStates.cs b/src/System.Windows.Forms/System/Windows/Forms/Components/ErrorProvider/ErrorProvider.ErrorProviderStates.cs similarity index 100% rename from src/System.Windows.Forms/System/Windows/Forms/ErrorProvider/ErrorProvider.ErrorProviderStates.cs rename to src/System.Windows.Forms/System/Windows/Forms/Components/ErrorProvider/ErrorProvider.ErrorProviderStates.cs diff --git a/src/System.Windows.Forms/System/Windows/Forms/ErrorProvider/ErrorProvider.ErrorWindow.ErrorWindowAccessibleObject.cs b/src/System.Windows.Forms/System/Windows/Forms/Components/ErrorProvider/ErrorProvider.ErrorWindow.ErrorWindowAccessibleObject.cs similarity index 100% rename from src/System.Windows.Forms/System/Windows/Forms/ErrorProvider/ErrorProvider.ErrorWindow.ErrorWindowAccessibleObject.cs rename to src/System.Windows.Forms/System/Windows/Forms/Components/ErrorProvider/ErrorProvider.ErrorWindow.ErrorWindowAccessibleObject.cs diff --git a/src/System.Windows.Forms/System/Windows/Forms/ErrorProvider/ErrorProvider.ErrorWindow.cs b/src/System.Windows.Forms/System/Windows/Forms/Components/ErrorProvider/ErrorProvider.ErrorWindow.cs similarity index 100% rename from src/System.Windows.Forms/System/Windows/Forms/ErrorProvider/ErrorProvider.ErrorWindow.cs rename to src/System.Windows.Forms/System/Windows/Forms/Components/ErrorProvider/ErrorProvider.ErrorWindow.cs diff --git a/src/System.Windows.Forms/System/Windows/Forms/ErrorProvider/ErrorProvider.IconRegion.cs b/src/System.Windows.Forms/System/Windows/Forms/Components/ErrorProvider/ErrorProvider.IconRegion.cs similarity index 100% rename from src/System.Windows.Forms/System/Windows/Forms/ErrorProvider/ErrorProvider.IconRegion.cs rename to src/System.Windows.Forms/System/Windows/Forms/Components/ErrorProvider/ErrorProvider.IconRegion.cs diff --git a/src/System.Windows.Forms/System/Windows/Forms/ErrorProvider/ErrorProvider.cs b/src/System.Windows.Forms/System/Windows/Forms/Components/ErrorProvider/ErrorProvider.cs similarity index 100% rename from src/System.Windows.Forms/System/Windows/Forms/ErrorProvider/ErrorProvider.cs rename to src/System.Windows.Forms/System/Windows/Forms/Components/ErrorProvider/ErrorProvider.cs diff --git a/src/System.Windows.Forms/System/Windows/Forms/Help/HelpProvider.cs b/src/System.Windows.Forms/System/Windows/Forms/Components/HelpProvider.cs similarity index 100% rename from src/System.Windows.Forms/System/Windows/Forms/Help/HelpProvider.cs rename to src/System.Windows.Forms/System/Windows/Forms/Components/HelpProvider.cs diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ImageList/ColorDepth.cs b/src/System.Windows.Forms/System/Windows/Forms/Components/ImageList/ColorDepth.cs similarity index 100% rename from src/System.Windows.Forms/System/Windows/Forms/Controls/ImageList/ColorDepth.cs rename to src/System.Windows.Forms/System/Windows/Forms/Components/ImageList/ColorDepth.cs diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ImageList/ImageList.ImageCollection.ImageInfo.cs b/src/System.Windows.Forms/System/Windows/Forms/Components/ImageList/ImageList.ImageCollection.ImageInfo.cs similarity index 100% rename from src/System.Windows.Forms/System/Windows/Forms/Controls/ImageList/ImageList.ImageCollection.ImageInfo.cs rename to src/System.Windows.Forms/System/Windows/Forms/Components/ImageList/ImageList.ImageCollection.ImageInfo.cs diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ImageList/ImageList.ImageCollection.cs b/src/System.Windows.Forms/System/Windows/Forms/Components/ImageList/ImageList.ImageCollection.cs similarity index 100% rename from src/System.Windows.Forms/System/Windows/Forms/Controls/ImageList/ImageList.ImageCollection.cs rename to src/System.Windows.Forms/System/Windows/Forms/Components/ImageList/ImageList.ImageCollection.cs diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ImageList/ImageList.Indexer.cs b/src/System.Windows.Forms/System/Windows/Forms/Components/ImageList/ImageList.Indexer.cs similarity index 100% rename from src/System.Windows.Forms/System/Windows/Forms/Controls/ImageList/ImageList.Indexer.cs rename to src/System.Windows.Forms/System/Windows/Forms/Components/ImageList/ImageList.Indexer.cs diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ImageList/ImageList.NativeImageList.cs b/src/System.Windows.Forms/System/Windows/Forms/Components/ImageList/ImageList.NativeImageList.cs similarity index 100% rename from src/System.Windows.Forms/System/Windows/Forms/Controls/ImageList/ImageList.NativeImageList.cs rename to src/System.Windows.Forms/System/Windows/Forms/Components/ImageList/ImageList.NativeImageList.cs diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ImageList/ImageList.Original.cs b/src/System.Windows.Forms/System/Windows/Forms/Components/ImageList/ImageList.Original.cs similarity index 100% rename from src/System.Windows.Forms/System/Windows/Forms/Controls/ImageList/ImageList.Original.cs rename to src/System.Windows.Forms/System/Windows/Forms/Components/ImageList/ImageList.Original.cs diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ImageList/ImageList.OriginalOptions.cs b/src/System.Windows.Forms/System/Windows/Forms/Components/ImageList/ImageList.OriginalOptions.cs similarity index 100% rename from src/System.Windows.Forms/System/Windows/Forms/Controls/ImageList/ImageList.OriginalOptions.cs rename to src/System.Windows.Forms/System/Windows/Forms/Components/ImageList/ImageList.OriginalOptions.cs diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ImageList/ImageList.cs b/src/System.Windows.Forms/System/Windows/Forms/Components/ImageList/ImageList.cs similarity index 100% rename from src/System.Windows.Forms/System/Windows/Forms/Controls/ImageList/ImageList.cs rename to src/System.Windows.Forms/System/Windows/Forms/Components/ImageList/ImageList.cs diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ImageList/ImageListConverter.cs b/src/System.Windows.Forms/System/Windows/Forms/Components/ImageList/ImageListConverter.cs similarity index 100% rename from src/System.Windows.Forms/System/Windows/Forms/Controls/ImageList/ImageListConverter.cs rename to src/System.Windows.Forms/System/Windows/Forms/Components/ImageList/ImageListConverter.cs diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ImageList/ImageListStreamer.cs b/src/System.Windows.Forms/System/Windows/Forms/Components/ImageList/ImageListStreamer.cs similarity index 100% rename from src/System.Windows.Forms/System/Windows/Forms/Controls/ImageList/ImageListStreamer.cs rename to src/System.Windows.Forms/System/Windows/Forms/Components/ImageList/ImageListStreamer.cs diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ImageList/RelatedImageListAttribute.cs b/src/System.Windows.Forms/System/Windows/Forms/Components/ImageList/RelatedImageListAttribute.cs similarity index 100% rename from src/System.Windows.Forms/System/Windows/Forms/Controls/ImageList/RelatedImageListAttribute.cs rename to src/System.Windows.Forms/System/Windows/Forms/Components/ImageList/RelatedImageListAttribute.cs diff --git a/src/System.Windows.Forms/System/Windows/Forms/Components/KioskModeManager/KioskModeManager.cs b/src/System.Windows.Forms/System/Windows/Forms/Components/KioskModeManager/KioskModeManager.cs new file mode 100644 index 00000000000..0a7a98fede8 --- /dev/null +++ b/src/System.Windows.Forms/System/Windows/Forms/Components/KioskModeManager/KioskModeManager.cs @@ -0,0 +1,1224 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; +using System.ComponentModel.Design; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Windows.Input; +using Windows.Win32.System.Power; +using Windows.Win32.System.Threading; + +namespace System.Windows.Forms; + +#if NET11_0_OR_GREATER +/// +/// Manages common kiosk-mode behavior for a WinForms form. +/// +/// +/// +/// is a component that can be dropped on a +/// form or on a user control at design time. Set +/// to the owning container and the component resolves the containing +/// when fullscreen behavior is needed. +/// +/// +/// The component can make the resolved form fullscreen, optionally cover the +/// taskbar, keep the form topmost while fullscreen, suppress display and +/// system sleep, hide the mouse pointer after inactivity, and notify the +/// application when user or system activity wakes the kiosk experience. +/// +/// +/// +/// +/// public partial class MainForm : Form +/// { +/// private readonly KioskModeManager _kioskModeManager; +/// +/// public MainForm() +/// { +/// InitializeComponent(); +/// +/// _kioskModeManager = new KioskModeManager +/// { +/// ContainerControl = this, +/// HideTaskbar = true, +/// TopMostInFullScreen = true, +/// MousePointerAutoHideDelay = 3000, +/// SuppressPowerSaving = true +/// }; +/// +/// _kioskModeManager.Wakeup += (sender, e) => +/// { +/// if (e.Source == KioskModeWakeupSource.Mouse) +/// { +/// // Refresh the kiosk surface after mouse activity. +/// } +/// }; +/// +/// if (!_kioskModeManager.FullScreen) +/// { +/// _kioskModeManager.ToggleFullScreen(); +/// } +/// } +/// } +/// +/// +[DefaultProperty(nameof(ToggleFullScreenKey))] +[ToolboxItemFilter("System.Windows.Forms")] +[SRDescription(nameof(SR.DescriptionKioskModeManager))] +public class KioskModeManager : Component, ISupportInitialize +{ + private const uint PowerRequestContextVersion = 0; + private const POWER_REQUEST_CONTEXT_FLAGS PowerRequestContextSimpleString = (POWER_REQUEST_CONTEXT_FLAGS)0x00000001; + private const uint NotifyForThisSession = 0; + private const nint PbtApmResumeAutomatic = 0x0012; + private const nint SpiSetWorkArea = 0x002F; + private const nint WtsConsoleConnect = 0x0001; + private const nint WtsRemoteConnect = 0x0003; + private const nint WtsSessionLogon = 0x0005; + private const nint WtsSessionUnlock = 0x0008; + private const string PowerRequestReason = "WinForms KioskModeManager: Preventing screen saver and sleep"; + + private static readonly object s_containerControlChangedEvent = new(); + private static readonly object s_fullScreenChangedEvent = new(); + private static readonly object s_wakeupEvent = new(); + + private ContainerControl? _containerControl; + private bool _containerControlExplicitlySet; + private Form? _targetForm; + private bool _isFullScreen; + private bool _pendingFullScreen; + private bool _initializing; + private bool _isCursorHidden; + private readonly List _parentChain = []; + private KioskModeMessageFilter? _messageFilter; + private KioskModeFormObserver? _formObserver; + private Timer? _mousePointerAutoHideTimer; + + private FormBorderStyle _savedBorderStyle; + private FormWindowState _savedWindowState; + private Rectangle _savedBounds; + private bool _savedTopMost; + + private bool _hideTaskbar; + private bool _topMostInFullScreen; + private Keys _toggleFullScreenKey = Keys.F11; + private bool _escapeExitsFullScreen = true; + private bool _suppressPowerSaving; + private int _mousePointerAutoHideDelay; + private ICommand? _wakeUpCommand; + + private HANDLE _powerRequestHandle; + + /// + /// Initializes a new instance of the class. + /// + public KioskModeManager() + { + } + + /// + /// Initializes a new instance of the class + /// and adds it to the specified container. + /// + /// The container that owns the component. + /// + /// is . + /// + public KioskModeManager(IContainer container) + { + ArgumentNullException.ThrowIfNull(container); + container.Add(this); + } + + /// + /// Gets or sets the associated with the component. + /// + /// + /// + /// When the component is sited at design time and + /// has not been set explicitly, the component resolves the root component of the + /// designer host (typically the owning or ) + /// and assigns it as the . An explicitly assigned + /// is never overwritten. + /// + /// + public override ISite? Site + { + get => base.Site; + set + { + base.Site = value; + + if (value is not null + && !_containerControlExplicitlySet + && value.GetService(typeof(IDesignerHost)) is IDesignerHost host + && host.RootComponent is ContainerControl root) + { + SetContainerControl(root, isExplicitAssignment: false); + } + } + } + + /// + /// Gets or sets the container whose containing form is controlled by this + /// component. + /// + /// + /// A that is either a or + /// a child container that can resolve a parent form; otherwise, + /// . + /// + /// + /// + /// This property intentionally uses rather + /// than . A can be placed + /// on a at design time, and that user control + /// can later be hosted by a form. Restricting the property to + /// would make that design-time scenario inconsistent. + /// + /// + [SRCategory(nameof(SR.CatBehavior))] + [SRDescription(nameof(SR.KioskModeManagerContainerControlDescr))] + [DefaultValue(null)] + public ContainerControl? ContainerControl + { + get => _containerControl; + set => SetContainerControl(value, isExplicitAssignment: true); + } + + /// + /// Occurs when the value of changes. + /// + [SRCategory(nameof(SR.CatPropertyChanged))] + [SRDescription(nameof(SR.KioskModeManagerContainerControlChangedDescr))] + public event EventHandler? ContainerControlChanged + { + add => Events.AddHandler(s_containerControlChangedEvent, value); + remove => Events.RemoveHandler(s_containerControlChangedEvent, value); + } + + /// + /// Raises the event. + /// + /// An that contains the event data. + [EditorBrowsable(EditorBrowsableState.Advanced)] + protected virtual void OnContainerControlChanged(EventArgs e) + { + if (Events[s_containerControlChangedEvent] is EventHandler handler) + { + handler(this, e); + } + } + + /// + /// Gets or sets a value indicating whether fullscreen mode covers the + /// Windows taskbar. + /// + /// + /// to size the form to the complete screen bounds; + /// to use normal maximized form behavior. + /// The default is . + /// + /// + /// + /// When this property is , the form is maximized + /// and Windows keeps the taskbar available according to the user's shell + /// settings. When this property is , the form is + /// sized to the screen bounds so the kiosk surface covers the taskbar. + /// + /// + [SRCategory(nameof(SR.CatBehavior))] + [SRDescription(nameof(SR.KioskModeManagerHideTaskbarDescr))] + [DefaultValue(false)] + public bool HideTaskbar + { + get => _hideTaskbar; + set + { + if (_hideTaskbar == value) + { + return; + } + + _hideTaskbar = value; + + if (_isFullScreen && _targetForm is not null) + { + ApplyFullScreen(_targetForm); + } + } + } + + /// + /// Gets or sets a value indicating whether the form is topmost while + /// fullscreen. + /// + /// + /// to set while + /// fullscreen; otherwise, . The default is + /// . + /// + [SRCategory(nameof(SR.CatBehavior))] + [SRDescription(nameof(SR.KioskModeManagerTopMostInFullScreenDescr))] + [DefaultValue(false)] + public bool TopMostInFullScreen + { + get => _topMostInFullScreen; + set + { + if (_topMostInFullScreen == value) + { + return; + } + + _topMostInFullScreen = value; + + if (_isFullScreen && _targetForm is not null) + { + _targetForm.TopMost = value; + } + } + } + + /// + /// Gets or sets the key that toggles fullscreen mode. + /// + /// + /// A value that is handled without modifiers. The + /// default is . + /// + [SRCategory(nameof(SR.CatBehavior))] + [SRDescription(nameof(SR.KioskModeManagerToggleFullScreenKeyDescr))] + [DefaultValue(Keys.F11)] + public Keys ToggleFullScreenKey + { + get => _toggleFullScreenKey; + set + { + if (_toggleFullScreenKey == value) + { + return; + } + + _toggleFullScreenKey = value; + } + } + + /// + /// Gets or sets a value indicating whether pressing Escape exits + /// fullscreen mode. + /// + /// + /// to exit fullscreen mode when Escape is pressed + /// without modifiers; otherwise, . The default is + /// . + /// + [SRCategory(nameof(SR.CatBehavior))] + [SRDescription(nameof(SR.KioskModeManagerEscapeExitsFullScreenDescr))] + [DefaultValue(true)] + public bool EscapeExitsFullScreen + { + get => _escapeExitsFullScreen; + set + { + if (_escapeExitsFullScreen == value) + { + return; + } + + _escapeExitsFullScreen = value; + } + } + + /// + /// Gets or sets a value indicating whether the component requests that + /// Windows keep the display and system awake. + /// + /// + /// to request that Windows suppress display sleep + /// and system sleep; otherwise, . The default is + /// . + /// + /// + /// + /// This property uses the Windows power request APIs. It prevents ordinary + /// display and system idle sleep while enabled, but it does not override + /// every system policy and does not configure wake timers, Wake-on-LAN, or + /// voice activation. + /// + /// + /// + /// Windows could not create or activate the power request. + /// + [SRCategory(nameof(SR.CatBehavior))] + [SRDescription(nameof(SR.KioskModeManagerSuppressPowerSavingDescr))] + [DefaultValue(false)] + public bool SuppressPowerSaving + { + get => _suppressPowerSaving; + set + { + if (_suppressPowerSaving == value) + { + return; + } + + bool previousValue = _suppressPowerSaving; + _suppressPowerSaving = value; + + try + { + UpdatePowerRequest(); + } + catch + { + _suppressPowerSaving = previousValue; + throw; + } + } + } + + /// + /// Gets or sets the amount of time, in milliseconds, before the mouse + /// pointer is hidden while fullscreen. + /// + /// + /// The inactivity delay in milliseconds. A value of 0 disables automatic + /// pointer hiding. The default is 0. + /// + /// + /// + /// The timer is active only while the component is in fullscreen mode. + /// When the user moves the mouse after the component hid the pointer, the + /// pointer is shown immediately and the timer starts again. + /// + /// + /// + /// is less than 0. + /// + [SRCategory(nameof(SR.CatBehavior))] + [SRDescription(nameof(SR.KioskModeManagerMousePointerAutoHideDelayDescr))] + [DefaultValue(0)] + public int MousePointerAutoHideDelay + { + get => _mousePointerAutoHideDelay; + set + { + ArgumentOutOfRangeException.ThrowIfNegative(value); + + if (_mousePointerAutoHideDelay == value) + { + return; + } + + _mousePointerAutoHideDelay = value; + + if (value == 0) + { + StopMousePointerAutoHideTimer(); + ShowMousePointerIfHidden(); + } + else if (_isFullScreen) + { + RestartMousePointerAutoHideTimer(); + } + } + } + + /// + /// Gets or sets a value indicating whether the resolved form is currently + /// fullscreen. + /// + /// + /// if the component has placed the form in + /// fullscreen mode; otherwise, . + /// + /// + /// + /// Setting this property to enters fullscreen mode and + /// setting it to restores the previously saved form + /// state. The property is bindable so it can participate in two-way data + /// binding. + /// + /// + [SRCategory(nameof(SR.CatBehavior))] + [SRDescription(nameof(SR.KioskModeManagerFullScreenDescr))] + [Bindable(true)] + [DefaultValue(false)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public bool FullScreen + { + get => _pendingFullScreen || _isFullScreen; + set + { + if (!value) + { + _pendingFullScreen = false; + ExitFullScreen(); + } + else if (_initializing || DesignMode || !EnterFullScreen()) + { + _pendingFullScreen = true; + } + else + { + _pendingFullScreen = false; + } + } + } + + /// + /// Occurs when the fullscreen state changes. + /// + [SRCategory(nameof(SR.CatPropertyChanged))] + [SRDescription(nameof(SR.KioskModeManagerFullScreenChangedDescr))] + public event EventHandler? FullScreenChanged + { + add => Events.AddHandler(s_fullScreenChangedEvent, value); + remove => Events.RemoveHandler(s_fullScreenChangedEvent, value); + } + + /// + /// Raises the event. + /// + /// An that contains the event data. + [EditorBrowsable(EditorBrowsableState.Advanced)] + protected virtual void OnFullScreenChanged(EventArgs e) + { + if (Events[s_fullScreenChangedEvent] is EventHandler handler) + { + handler(this, e); + } + } + + /// + /// Occurs when keyboard, mouse, power resume, or session activity wakes + /// the kiosk experience. + /// + /// + /// + /// The event reports activity that the component can observe on the UI + /// thread or through Windows power/session notifications. It does not mean + /// that the component caused the computer to wake from sleep, and it does + /// not configure voice wake, network wake, or wake timers. + /// + /// + [SRCategory(nameof(SR.CatAction))] + [SRDescription(nameof(SR.KioskModeManagerWakeupDescr))] + public event KioskModeWakeupEventHandler? Wakeup + { + add => Events.AddHandler(s_wakeupEvent, value); + remove => Events.RemoveHandler(s_wakeupEvent, value); + } + + /// + /// Raises the event. + /// + /// A that contains the event data. + [EditorBrowsable(EditorBrowsableState.Advanced)] + protected virtual void OnWakeup(KioskModeWakeupEventArgs e) + { + if (Events[s_wakeupEvent] is KioskModeWakeupEventHandler handler) + { + handler(this, e); + } + + if (_wakeUpCommand is not null) + { + // The wakeup source is passed as its string name so the bound command stays + // independent of the WinForms-specific enum type. + string parameter = e.Source.ToString(); + if (_wakeUpCommand.CanExecute(parameter)) + { + _wakeUpCommand.Execute(parameter); + } + } + } + + /// + /// Gets or sets the command that is executed whenever the + /// event is raised. + /// + /// + /// An to execute on every wakeup, or + /// to execute no command. The default is . + /// + /// + /// + /// The command is executed with the wakeup source name (the result of + /// .) as the command + /// parameter, keeping the command independent of the WinForms-specific enum type. + /// + /// + [SRCategory(nameof(SR.CatAction))] + [SRDescription(nameof(SR.KioskModeManagerWakeUpCommandDescr))] + [Bindable(true)] + [DefaultValue(null)] + public ICommand? WakeUpCommand + { + get => _wakeUpCommand; + set => _wakeUpCommand = value; + } + + /// + /// Places the resolved form in fullscreen mode. + /// + /// + /// +/// The component saves the form's border style, window state, bounds, and +/// topmost state before applying fullscreen mode. Call +/// again to restore the saved state. + /// + /// + private bool EnterFullScreen() + { + if (_isFullScreen || _initializing || DesignMode) + { + return _isFullScreen; + } + + Form? targetForm = ResolveTargetForm(); + if (targetForm is null) + { + return false; + } + + _savedBorderStyle = targetForm.FormBorderStyle; + _savedWindowState = targetForm.WindowState; + _savedBounds = targetForm.WindowState == FormWindowState.Normal + ? targetForm.Bounds + : targetForm.RestoreBounds; + _savedTopMost = targetForm.TopMost; + + ApplyFullScreen(targetForm); + SetFullScreenState(true); + _pendingFullScreen = false; + RestartMousePointerAutoHideTimer(); + return true; + } + + /// + /// Restores the form state that was saved when fullscreen mode was + /// entered. + /// + private void ExitFullScreen() + { + if (!_isFullScreen || _targetForm is null) + { + return; + } + + StopMousePointerAutoHideTimer(); + ShowMousePointerIfHidden(); + + _targetForm.FormBorderStyle = _savedBorderStyle; + _targetForm.TopMost = _savedTopMost; + _targetForm.WindowState = FormWindowState.Normal; + _targetForm.Bounds = _savedBounds; + _targetForm.WindowState = _savedWindowState; + + SetFullScreenState(false); + } + + /// + /// Updates the fullscreen state and raises + /// when the value changes. + /// + /// The new fullscreen state. + private void SetFullScreenState(bool value) + { + if (_isFullScreen == value) + { + return; + } + + _isFullScreen = value; + OnFullScreenChanged(EventArgs.Empty); + } + + /// + /// Toggles the resolved form between fullscreen and restored mode. + /// + /// + /// + /// Use to determine the current state before + /// calling this method when the application needs an explicit target + /// state. + /// + /// + public void ToggleFullScreen() + => FullScreen = !FullScreen; + + void ISupportInitialize.BeginInit() + { + _initializing = true; + } + + void ISupportInitialize.EndInit() + { + _initializing = false; + + AttachToForm(); + UpdatePowerRequest(); + + if (_pendingFullScreen) + { + FullScreen = true; + } + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + _pendingFullScreen = false; + ExitFullScreen(); + DetachFromForm(); + _mousePointerAutoHideTimer?.Dispose(); + _mousePointerAutoHideTimer = null; + ReleasePowerRequest(); + } + + base.Dispose(disposing); + } + + private void SetContainerControl(ContainerControl? value, bool isExplicitAssignment) + { + if (_containerControl == value) + { + if (isExplicitAssignment) + { + _containerControlExplicitlySet = true; + } + + return; + } + + if (_isFullScreen) + { + _pendingFullScreen = false; + ExitFullScreen(); + } + + if (value is null) + { + _pendingFullScreen = false; + } + + DetachFromForm(); + _containerControl = value; + if (isExplicitAssignment) + { + _containerControlExplicitlySet = true; + } + + AttachToForm(); + OnContainerControlChanged(EventArgs.Empty); + } + + private void AttachToForm() + { + if (_initializing || DesignMode || _containerControl is null) + { + return; + } + + UpdateParentChangedSubscriptions(); + EnsureMessageMonitoring(); + ResolveTargetForm(); + if (_pendingFullScreen && _targetForm is not null) + { + EnterFullScreen(); + } + } + + private void DetachFromForm() + { + ClearParentChangedSubscriptions(); + + if (_messageFilter is not null) + { + Application.RemoveMessageFilter(_messageFilter); + _messageFilter = null; + } + + _formObserver?.Detach(); + StopMousePointerAutoHideTimer(); + ShowMousePointerIfHidden(); + _targetForm = null; + } + + private void OnContainerControlParentChanged(object? sender, EventArgs e) + { + UpdateParentChangedSubscriptions(); + + Form? newTarget = _containerControl as Form ?? _containerControl?.FindForm(); + if (ReferenceEquals(_targetForm, newTarget)) + { + return; + } + + bool restoreFullScreen = _pendingFullScreen || _isFullScreen; + + if (_isFullScreen) + { + ExitFullScreen(); + } + + ResolveTargetForm(); + + if (restoreFullScreen) + { + FullScreen = true; + } + } + + private void UpdateParentChangedSubscriptions() + { + ClearParentChangedSubscriptions(); + + for (Control? current = _containerControl; current is not null; current = current.Parent) + { + current.ParentChanged += OnContainerControlParentChanged; + _parentChain.Add(current); + } + } + + private void ClearParentChangedSubscriptions() + { + foreach (Control control in _parentChain) + { + control.ParentChanged -= OnContainerControlParentChanged; + } + + _parentChain.Clear(); + } + + private Form? ResolveTargetForm() + { + // ContainerControl intentionally supports both Form and UserControl + // design-time placement. FindForm handles the UserControl case. + Form? targetForm = _containerControl as Form ?? _containerControl?.FindForm(); + if (!ReferenceEquals(_targetForm, targetForm)) + { + _targetForm = targetForm; + UpdateFormObserver(); + } + + return _targetForm; + } + + private void EnsureMessageMonitoring() + { + // IMessageFilter observes input for child controls without changing + // Form.KeyPreview or installing global keyboard/mouse hooks. + if (_messageFilter is null) + { + _messageFilter = new KioskModeMessageFilter(this); + Application.AddMessageFilter(_messageFilter); + } + } + + private void ApplyFullScreen(Form form) + { + Rectangle screenReferenceBounds = form.WindowState == FormWindowState.Normal + ? form.Bounds + : form.RestoreBounds; + Screen screen = Screen.FromRectangle(screenReferenceBounds); + Rectangle fullScreenBounds = _hideTaskbar + ? screen.Bounds + : screen.WorkingArea; + + // Apply fullscreen from a normal state so bounds and border changes do + // not operate on Windows' maximized window rectangle. + form.WindowState = FormWindowState.Normal; + form.FormBorderStyle = FormBorderStyle.None; + form.TopMost = _topMostInFullScreen; + form.Bounds = fullScreenBounds; + } + + private void RefreshFullScreenBounds() + { + if (_isFullScreen + && _targetForm is { IsDisposed: false } form) + { + ApplyFullScreen(form); + } + } + + private void UpdateFormObserver() + { + if (DesignMode) + { + return; + } + + _formObserver ??= new KioskModeFormObserver(this); + _formObserver.Attach(_targetForm); + } + + private void UpdatePowerRequest() + { + if (_initializing || DesignMode) + { + return; + } + + if (_suppressPowerSaving) + { + CreatePowerRequest(); + } + else + { + ReleasePowerRequest(); + } + } + + private void ProcessPowerBroadcast(WPARAM powerEvent) + { + if ((nint)powerEvent.Value == PbtApmResumeAutomatic) + { + RaiseWakeup(KioskModeWakeupSource.PowerResume); + } + } + + private void ProcessSessionChange(WPARAM sessionEvent) + { + nint value = (nint)sessionEvent.Value; + if (value is WtsConsoleConnect or WtsRemoteConnect or WtsSessionLogon or WtsSessionUnlock) + { + RaiseWakeup(KioskModeWakeupSource.Session); + } + } + + private void RestartMousePointerAutoHideTimer() + { + // Use a WinForms timer so cursor changes happen on the UI thread that + // receives the input messages restarting this timer. + if (!_isFullScreen || _mousePointerAutoHideDelay == 0) + { + return; + } + + _mousePointerAutoHideTimer ??= new Timer(); + _mousePointerAutoHideTimer.Stop(); + _mousePointerAutoHideTimer.Interval = _mousePointerAutoHideDelay; + _mousePointerAutoHideTimer.Tick -= OnMousePointerAutoHideTimerTick; + _mousePointerAutoHideTimer.Tick += OnMousePointerAutoHideTimerTick; + _mousePointerAutoHideTimer.Start(); + } + + private void StopMousePointerAutoHideTimer() + { + _mousePointerAutoHideTimer?.Stop(); + } + + private void OnMousePointerAutoHideTimerTick(object? sender, EventArgs e) + { + StopMousePointerAutoHideTimer(); + + if (!_isCursorHidden) + { + Cursor.Hide(); + _isCursorHidden = true; + } + } + + private void ProcessKeyboardActivity(Keys keyData, Keys modifiers, bool isRepeat) + { + // Any keyboard input wakes the kiosk experience, even when it does not + // match one of the configured fullscreen control keys. + RaiseWakeup(KioskModeWakeupSource.Keyboard); + + if (isRepeat) + { + return; + } + + Keys keyCode = keyData & Keys.KeyCode; + + if (keyCode == _toggleFullScreenKey && modifiers == Keys.None) + { + ToggleFullScreen(); + } + else if (keyCode == Keys.Escape && modifiers == Keys.None + && _escapeExitsFullScreen && _isFullScreen) + { + ExitFullScreen(); + } + } + + private void ProcessMouseActivity() + { + ShowMousePointerIfHidden(); + RestartMousePointerAutoHideTimer(); + RaiseWakeup(KioskModeWakeupSource.Mouse); + } + + private void ShowMousePointerIfHidden() + { + // Cursor.Hide and Cursor.Show are counter-based. Only balance hides + // performed by this component. + if (!_isCursorHidden) + { + return; + } + + Cursor.Show(); + _isCursorHidden = false; + } + + private void RaiseWakeup(KioskModeWakeupSource source) + => OnWakeup(new KioskModeWakeupEventArgs(source)); + + private unsafe void CreatePowerRequest() + { + // PowerClearRequest clears the active request, but only CloseHandle + // releases the native HANDLE returned by PowerCreateRequest. + if (!_powerRequestHandle.IsNull) + { + return; + } + + REASON_CONTEXT context = new() + { + Version = PowerRequestContextVersion, + Flags = PowerRequestContextSimpleString, + }; + + nint reasonString = Marshal.StringToHGlobalUni(PowerRequestReason); + + try + { + context.Reason.SimpleReasonString = (PWSTR)(char*)reasonString; + _powerRequestHandle = PInvoke.PowerCreateRequest(in context); + } + finally + { + Marshal.FreeHGlobal(reasonString); + } + + if (_powerRequestHandle.IsNull) + { + throw new Win32Exception(); + } + + try + { + if (!PInvoke.PowerSetRequest(_powerRequestHandle, POWER_REQUEST_TYPE.PowerRequestDisplayRequired) + || !PInvoke.PowerSetRequest(_powerRequestHandle, POWER_REQUEST_TYPE.PowerRequestSystemRequired)) + { + throw new Win32Exception(); + } + } + catch + { + ReleasePowerRequest(); + throw; + } + } + + private void ReleasePowerRequest() + { + if (_powerRequestHandle.IsNull) + { + return; + } + + PInvoke.PowerClearRequest(_powerRequestHandle, POWER_REQUEST_TYPE.PowerRequestDisplayRequired); + PInvoke.PowerClearRequest(_powerRequestHandle, POWER_REQUEST_TYPE.PowerRequestSystemRequired); + PInvoke.CloseHandle(_powerRequestHandle); + _powerRequestHandle = default; + } + + private bool IsMessageFromMonitoredControl(HWND hwnd) + { + if (_targetForm is not null + && !_targetForm.IsDisposed + && (hwnd == _targetForm.HWND || PInvoke.IsChild(_targetForm, hwnd))) + { + return true; + } + + return _containerControl is not null + && !_containerControl.IsDisposed + && _containerControl.IsHandleCreated + && (hwnd == _containerControl.HWND || PInvoke.IsChild(_containerControl, hwnd)); + } + + private bool ProcessMessage(Message message) + { + if (_containerControl is null || DesignMode) + { + return false; + } + + if (_targetForm is null || _targetForm.IsDisposed) + { + ResolveTargetForm(); + if (_pendingFullScreen && _targetForm is not null) + { + EnterFullScreen(); + } + } + + if (!IsMessageFromMonitoredControl(message.HWND)) + { + return false; + } + + if (message.MsgInternal == PInvokeCore.WM_KEYDOWN + || message.MsgInternal == PInvokeCore.WM_SYSKEYDOWN) + { + bool isRepeat = ((nuint)(nint)message.LParamInternal & (1u << 30)) != 0; + ProcessKeyboardActivity( + (Keys)(nint)message.WParamInternal, + Control.ModifierKeys & Keys.Modifiers, + isRepeat); + } + else if (message.MsgInternal >= PInvokeCore.WM_MOUSEFIRST + && message.MsgInternal <= PInvokeCore.WM_MOUSELAST) + { + ProcessMouseActivity(); + } + + return false; + } + + /// + /// Observes keyboard and mouse messages that belong to the target form. + /// + private sealed class KioskModeMessageFilter : IMessageFilter + { + private readonly KioskModeManager _owner; + + public KioskModeMessageFilter(KioskModeManager owner) + { + _owner = owner; + } + + public bool PreFilterMessage(ref Message m) + => _owner.ProcessMessage(m); + } + + /// + /// Observes power and session messages that belong to the target form. + /// + private sealed class KioskModeFormObserver : NativeWindow + { + private readonly KioskModeManager _owner; + private Form? _form; + private bool _sessionNotificationsRegistered; + + public KioskModeFormObserver(KioskModeManager owner) + { + _owner = owner; + } + + public void Attach(Form? form) + { + if (ReferenceEquals(_form, form)) + { + return; + } + + Detach(); + + if (form is null || form.IsDisposed) + { + return; + } + + _form = form; + form.HandleCreated += OnFormHandleCreated; + form.HandleDestroyed += OnFormHandleDestroyed; + + if (form.IsHandleCreated) + { + OnFormHandleCreated(form, EventArgs.Empty); + } + } + + public void Detach() + { + if (_form is not null) + { + _form.HandleCreated -= OnFormHandleCreated; + _form.HandleDestroyed -= OnFormHandleDestroyed; + _form = null; + } + + UnregisterSessionNotifications(); + if (!HWND.IsNull) + { + ReleaseHandle(); + } + } + + private void OnFormHandleCreated(object? sender, EventArgs e) + { + if (_form is null || _form.IsDisposed || !_form.IsHandleCreated) + { + return; + } + + AssignHandle(_form.HWND); + RegisterSessionNotifications(); + } + + private void OnFormHandleDestroyed(object? sender, EventArgs e) + { + UnregisterSessionNotifications(); + if (!HWND.IsNull) + { + ReleaseHandle(); + } + } + + private void RegisterSessionNotifications() + { + if (_sessionNotificationsRegistered || HWND.IsNull) + { + return; + } + + _sessionNotificationsRegistered = PInvoke.WTSRegisterSessionNotification(HWND, NotifyForThisSession); + } + + private void UnregisterSessionNotifications() + { + if (!_sessionNotificationsRegistered) + { + return; + } + + PInvoke.WTSUnRegisterSessionNotification(HWND); + _sessionNotificationsRegistered = false; + } + + protected override void WndProc(ref Message m) + { + if (m.MsgInternal == PInvokeCore.WM_POWERBROADCAST) + { + _owner.ProcessPowerBroadcast(m.WParamInternal); + } + else if (m.MsgInternal == PInvokeCore.WM_WTSSESSION_CHANGE) + { + _owner.ProcessSessionChange(m.WParamInternal); + } + + base.WndProc(ref m); + + if (m.MsgInternal == PInvokeCore.WM_DISPLAYCHANGE + || (m.MsgInternal == PInvokeCore.WM_SETTINGCHANGE + && (nint)m.WParamInternal == SpiSetWorkArea)) + { + _owner.RefreshFullScreenBounds(); + } + } + } +} +#endif diff --git a/src/System.Windows.Forms/System/Windows/Forms/Components/KioskModeManager/KioskModeWakeupEventArgs.cs b/src/System.Windows.Forms/System/Windows/Forms/Components/KioskModeManager/KioskModeWakeupEventArgs.cs new file mode 100644 index 00000000000..1ad14d4fcc2 --- /dev/null +++ b/src/System.Windows.Forms/System/Windows/Forms/Components/KioskModeManager/KioskModeWakeupEventArgs.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Windows.Forms; + +#if NET11_0_OR_GREATER +/// +/// Provides data for the event. +/// +/// +/// +/// The property identifies the kind of activity the +/// component observed. Applications can use this value to distinguish direct +/// user activity, such as mouse or keyboard input, from system activity such +/// as power resume or session unlock. +/// +/// +public class KioskModeWakeupEventArgs : EventArgs +{ + /// + /// Initializes a new instance of the + /// class. + /// + /// The source of the wakeup notification. + public KioskModeWakeupEventArgs(KioskModeWakeupSource source) + { + SourceGenerated.EnumValidator.Validate(source, nameof(source)); + Source = source; + } + + /// + /// Gets the source of the wakeup notification. + /// + public KioskModeWakeupSource Source { get; } +} +#endif diff --git a/src/System.Windows.Forms/System/Windows/Forms/Components/KioskModeManager/KioskModeWakeupEventHandler.cs b/src/System.Windows.Forms/System/Windows/Forms/Components/KioskModeManager/KioskModeWakeupEventHandler.cs new file mode 100644 index 00000000000..b2784bb3b3c --- /dev/null +++ b/src/System.Windows.Forms/System/Windows/Forms/Components/KioskModeManager/KioskModeWakeupEventHandler.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Windows.Forms; + +#if NET11_0_OR_GREATER +/// +/// Represents the method that will handle the +/// event. +/// +/// The source of the event. +/// A that contains the event data. +public delegate void KioskModeWakeupEventHandler(object? sender, KioskModeWakeupEventArgs e); +#endif diff --git a/src/System.Windows.Forms/System/Windows/Forms/Components/KioskModeManager/KioskModeWakeupSource.cs b/src/System.Windows.Forms/System/Windows/Forms/Components/KioskModeManager/KioskModeWakeupSource.cs new file mode 100644 index 00000000000..6c9a05784c4 --- /dev/null +++ b/src/System.Windows.Forms/System/Windows/Forms/Components/KioskModeManager/KioskModeWakeupSource.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Windows.Forms; + +#if NET11_0_OR_GREATER +/// +/// Specifies the source of a +/// notification. +/// +public enum KioskModeWakeupSource +{ + /// + /// The wakeup notification was caused by keyboard activity. + /// + Keyboard = 0, + + /// + /// The wakeup notification was caused by mouse activity. + /// + Mouse = 1, + + /// + /// The wakeup notification was caused by the system resuming from a low + /// power state. + /// + PowerResume = 2, + + /// + /// The wakeup notification was caused by a Windows session activity, such + /// as logon or unlock. + /// + Session = 3, +} +#endif diff --git a/src/System.Windows.Forms/System/Windows/Forms/Timer.cs b/src/System.Windows.Forms/System/Windows/Forms/Components/Timer.cs similarity index 100% rename from src/System.Windows.Forms/System/Windows/Forms/Timer.cs rename to src/System.Windows.Forms/System/Windows/Forms/Components/Timer.cs diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/KioskModeManagerTests.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/KioskModeManagerTests.cs new file mode 100644 index 00000000000..f2d349e6bc3 --- /dev/null +++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/KioskModeManagerTests.cs @@ -0,0 +1,923 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +using System.ComponentModel; +using System.ComponentModel.Design; +using System.Drawing; +using System.Windows.Input; +using Moq; + +namespace System.Windows.Forms.Tests; + +#if NET11_0_OR_GREATER +public class KioskModeManagerTests +{ + [WinFormsFact] + public void KioskModeManager_Ctor_Default() + { + using SubKioskModeManager manager = new(); + + Assert.Null(manager.Container); + Assert.Null(manager.ContainerControl); + Assert.False(manager.DesignMode); + Assert.True(manager.EscapeExitsFullScreen); + Assert.False(manager.HideTaskbar); + Assert.False(manager.FullScreen); + Assert.Equal(0, manager.MousePointerAutoHideDelay); + Assert.Null(manager.Site); + Assert.False(manager.SuppressPowerSaving); + Assert.Equal(Keys.F11, manager.ToggleFullScreenKey); + Assert.False(manager.TopMostInFullScreen); + } + + [WinFormsFact] + public void KioskModeManager_Ctor_IContainer() + { + using Container container = new(); + using KioskModeManager manager = new(container); + + Assert.Same(container, manager.Container); + Assert.NotNull(manager.Site); + } + + [WinFormsFact] + public void KioskModeManager_Ctor_NullContainer_ThrowsArgumentNullException() + { + Assert.Throws("container", () => new KioskModeManager(null)); + } + + [WinFormsFact] + public void KioskModeManager_ContainerControl_Set_GetReturnsExpected() + { + using Form form = new(); + using KioskModeManager manager = new() + { + ContainerControl = form + }; + + Assert.Same(form, manager.ContainerControl); + + manager.ContainerControl = form; + Assert.Same(form, manager.ContainerControl); + + manager.ContainerControl = null; + Assert.Null(manager.ContainerControl); + } + + [WinFormsFact] + public void KioskModeManager_ContainerControl_SetWithHandler_CallsContainerControlChanged() + { + using Form form = new(); + using KioskModeManager manager = new(); + int callCount = 0; + EventHandler handler = (sender, e) => + { + Assert.Same(manager, sender); + Assert.Same(EventArgs.Empty, e); + callCount++; + }; + + manager.ContainerControlChanged += handler; + manager.ContainerControl = form; + Assert.Equal(1, callCount); + + manager.ContainerControl = form; + Assert.Equal(1, callCount); + + manager.ContainerControl = null; + Assert.Equal(2, callCount); + + manager.ContainerControlChanged -= handler; + manager.ContainerControl = form; + Assert.Equal(2, callCount); + } + + [WinFormsTheory] + [InlineData(0)] + [InlineData(1)] + [InlineData(3000)] + public void KioskModeManager_MousePointerAutoHideDelay_Set_GetReturnsExpected(int value) + { + using KioskModeManager manager = new() + { + MousePointerAutoHideDelay = value + }; + + Assert.Equal(value, manager.MousePointerAutoHideDelay); + + manager.MousePointerAutoHideDelay = value; + Assert.Equal(value, manager.MousePointerAutoHideDelay); + } + + [WinFormsFact] + public void KioskModeManager_MousePointerAutoHideDelay_SetNegative_ThrowsArgumentOutOfRangeException() + { + using KioskModeManager manager = new(); + + Assert.Throws("value", () => manager.MousePointerAutoHideDelay = -1); + } + + [WinFormsTheory] + [EnumData] + public void KioskModeWakeupEventArgs_Ctor_Source(KioskModeWakeupSource source) + { + KioskModeWakeupEventArgs eventArgs = new(source); + + Assert.Equal(source, eventArgs.Source); + } + + [WinFormsTheory] + [InvalidEnumData] + public void KioskModeWakeupEventArgs_Ctor_InvalidSource_ThrowsInvalidEnumArgumentException(KioskModeWakeupSource source) + { + Assert.Throws("source", () => new KioskModeWakeupEventArgs(source)); + } + + [WinFormsTheory] + [EnumData] + public void KioskModeManager_OnWakeup_Invoke_CallsWakeup(KioskModeWakeupSource source) + { + using SubKioskModeManager manager = new(); + KioskModeWakeupEventArgs eventArgs = new(source); + int callCount = 0; + KioskModeWakeupEventHandler handler = (sender, e) => + { + Assert.Same(manager, sender); + Assert.Same(eventArgs, e); + Assert.Equal(source, e.Source); + callCount++; + }; + + manager.Wakeup += handler; + manager.OnWakeup(eventArgs); + Assert.Equal(1, callCount); + + manager.Wakeup -= handler; + manager.OnWakeup(eventArgs); + Assert.Equal(1, callCount); + } + + [WinFormsFact] + public void KioskModeManager_ToggleFullScreen_Form_RestoresExpected() + { + using Form form = new() + { + Bounds = new Rectangle(10, 20, 300, 200), + FormBorderStyle = FormBorderStyle.FixedDialog, + WindowState = FormWindowState.Normal, + TopMost = false + }; + + using KioskModeManager manager = new() + { + ContainerControl = form, + HideTaskbar = false, + TopMostInFullScreen = true + }; + Rectangle workingArea = Screen.FromRectangle(form.Bounds).WorkingArea; + + manager.ToggleFullScreen(); + + Assert.True(manager.FullScreen); + Assert.Equal(FormBorderStyle.None, form.FormBorderStyle); + Assert.True(form.TopMost); + Assert.Equal(FormWindowState.Normal, form.WindowState); + Assert.Equal(workingArea, form.Bounds); + + manager.ToggleFullScreen(); + + Assert.False(manager.FullScreen); + Assert.Equal(FormBorderStyle.FixedDialog, form.FormBorderStyle); + Assert.False(form.TopMost); + Assert.Equal(FormWindowState.Normal, form.WindowState); + Assert.Equal(new Rectangle(10, 20, 300, 200), form.Bounds); + } + + [WinFormsFact] + public void KioskModeManager_FullScreen_HideTaskbar_UsesScreenBounds() + { + using Form form = new() + { + Bounds = new Rectangle(10, 20, 300, 200) + }; + using KioskModeManager manager = new() + { + ContainerControl = form, + HideTaskbar = true + }; + Rectangle screenBounds = Screen.FromRectangle(form.Bounds).Bounds; + + manager.FullScreen = true; + + Assert.Equal(FormWindowState.Normal, form.WindowState); + Assert.Equal(screenBounds, form.Bounds); + } + + [WinFormsFact] + public void KioskModeManager_FullScreen_MinimizedForm_UsesRestoreMonitor() + { + using Form form = new() + { + Bounds = new Rectangle(10, 20, 300, 200), + StartPosition = FormStartPosition.Manual + }; + form.Show(); + form.WindowState = FormWindowState.Minimized; + Rectangle restoreBounds = form.RestoreBounds; + Rectangle workingArea = Screen.FromRectangle( + restoreBounds).WorkingArea; + using KioskModeManager manager = new() + { + ContainerControl = form + }; + + manager.FullScreen = true; + + Assert.Equal(workingArea, form.Bounds); + } + + [WinFormsFact] + public void KioskModeManager_FullScreen_RefreshBounds_ReappliesWorkingArea() + { + using Form form = new() + { + Bounds = new Rectangle(10, 20, 300, 200) + }; + Rectangle workingArea = Screen.FromRectangle( + form.Bounds).WorkingArea; + using KioskModeManager manager = new() + { + ContainerControl = form, + FullScreen = true + }; + form.Bounds = new Rectangle( + workingArea.X + 10, + workingArea.Y + 10, + 300, + 200); + + manager.TestAccessor.Dynamic.RefreshFullScreenBounds(); + + Assert.Equal(workingArea, form.Bounds); + } + + [WinFormsFact] + public void KioskModeManager_FullScreen_StatusStripDropDown_RemainsAttached() + { + using Form form = new() + { + Bounds = new Rectangle(10, 20, 300, 200) + }; + using StatusStrip statusStrip = new(); + using ToolStripDropDownButton dropDownButton = new("Options"); + dropDownButton.DropDownItems.Add("First"); + statusStrip.Items.Add(dropDownButton); + form.Controls.Add(statusStrip); + using KioskModeManager manager = new() + { + ContainerControl = form + }; + + form.Show(); + manager.FullScreen = true; + dropDownButton.ShowDropDown(); + + Rectangle itemScreenBounds = statusStrip.RectangleToScreen(dropDownButton.Bounds); + Assert.InRange( + Math.Abs(dropDownButton.DropDown.Bounds.Bottom - itemScreenBounds.Top), + 0, + 1); + } + + [WinFormsFact] + public void KioskModeManager_ToggleFullScreen_UserControlContainer_UsesParentForm() + { + using Form form = new(); + using UserControl userControl = new(); + form.Controls.Add(userControl); + using KioskModeManager manager = new() + { + ContainerControl = userControl + }; + + manager.ToggleFullScreen(); + + Assert.True(manager.FullScreen); + Assert.Equal(FormBorderStyle.None, form.FormBorderStyle); + } + + [WinFormsFact] + public void KioskModeManager_ToggleFullScreen_UserControlContainerParentedAfterSet_UsesParentForm() + { + using Form form = new(); + using UserControl userControl = new(); + using KioskModeManager manager = new() + { + ContainerControl = userControl + }; + + form.Controls.Add(userControl); + manager.ToggleFullScreen(); + + Assert.True(manager.FullScreen); + Assert.Equal(FormBorderStyle.None, form.FormBorderStyle); + } + + [WinFormsFact] + public void KioskModeManager_FullScreen_SetBeforeUserControlIsParented_EntersWhenParented() + { + using Form form = new(); + using UserControl userControl = new(); + using KioskModeManager manager = new() + { + ContainerControl = userControl, + FullScreen = true + }; + + form.Controls.Add(userControl); + + Assert.True(manager.FullScreen); + Assert.Equal(FormBorderStyle.None, form.FormBorderStyle); + } + + [WinFormsFact] + public void KioskModeManager_FullScreen_UserControlReparented_TransfersToNewForm() + { + using Form firstForm = new() + { + FormBorderStyle = FormBorderStyle.FixedDialog + }; + using Form secondForm = new(); + using UserControl userControl = new(); + firstForm.Controls.Add(userControl); + using KioskModeManager manager = new() + { + ContainerControl = userControl, + FullScreen = true + }; + + secondForm.Controls.Add(userControl); + + Assert.True(manager.FullScreen); + Assert.Equal(FormBorderStyle.FixedDialog, firstForm.FormBorderStyle); + Assert.Equal(FormBorderStyle.None, secondForm.FormBorderStyle); + } + + [WinFormsFact] + public void KioskModeManager_FullScreen_AncestorReparented_TransfersToNewForm() + { + using Form firstForm = new() + { + FormBorderStyle = FormBorderStyle.FixedDialog + }; + using Form secondForm = new(); + using Panel panel = new(); + using UserControl userControl = new(); + panel.Controls.Add(userControl); + firstForm.Controls.Add(panel); + using KioskModeManager manager = new() + { + ContainerControl = userControl, + FullScreen = true + }; + + secondForm.Controls.Add(panel); + + Assert.True(manager.FullScreen); + Assert.Equal(FormBorderStyle.FixedDialog, firstForm.FormBorderStyle); + Assert.Equal(FormBorderStyle.None, secondForm.FormBorderStyle); + } + + [WinFormsFact] + public void KioskModeManager_FullScreen_SetBeforeAncestorIsParented_EntersWhenParented() + { + using Form form = new(); + using Panel panel = new(); + using UserControl userControl = new(); + panel.Controls.Add(userControl); + using KioskModeManager manager = new() + { + ContainerControl = userControl, + FullScreen = true + }; + + form.Controls.Add(panel); + + Assert.True(manager.FullScreen); + Assert.Equal(FormBorderStyle.None, form.FormBorderStyle); + } + + [WinFormsFact] + public void KioskModeManager_ContainerControl_SetWhileFullScreen_RestoresPreviousForm() + { + using Form form = new() + { + Bounds = new Rectangle(10, 20, 300, 200), + FormBorderStyle = FormBorderStyle.FixedDialog, + WindowState = FormWindowState.Normal + }; + + using KioskModeManager manager = new() + { + ContainerControl = form + }; + + manager.ToggleFullScreen(); + Assert.True(manager.FullScreen); + + manager.ContainerControl = null; + + Assert.False(manager.FullScreen); + Assert.Equal(FormBorderStyle.FixedDialog, form.FormBorderStyle); + Assert.Equal(FormWindowState.Normal, form.WindowState); + Assert.Equal(new Rectangle(10, 20, 300, 200), form.Bounds); + } + + [WinFormsFact] + public void KioskModeManager_ContainerControl_Set_DoesNotChangeFormKeyPreview() + { + using Form form = new() + { + KeyPreview = false + }; + + using KioskModeManager manager = new() + { + ContainerControl = form + }; + + Assert.False(form.KeyPreview); + } + + [WinFormsFact] + public void KioskModeManager_FullScreen_SetTrueThenFalse_EntersAndExitsFullScreen() + { + using Form form = new() + { + Bounds = new Rectangle(10, 20, 300, 200), + FormBorderStyle = FormBorderStyle.FixedDialog, + WindowState = FormWindowState.Normal + }; + + using KioskModeManager manager = new() + { + ContainerControl = form + }; + + manager.FullScreen = true; + + Assert.True(manager.FullScreen); + Assert.Equal(FormBorderStyle.None, form.FormBorderStyle); + + manager.FullScreen = false; + + Assert.False(manager.FullScreen); + Assert.Equal(FormBorderStyle.FixedDialog, form.FormBorderStyle); + Assert.Equal(FormWindowState.Normal, form.WindowState); + Assert.Equal(new Rectangle(10, 20, 300, 200), form.Bounds); + } + + [WinFormsFact] + public void KioskModeManager_FullScreen_TopMostDisabled_OverridesAndRestoresFormTopMost() + { + using Form form = new() + { + TopMost = true + }; + using KioskModeManager manager = new() + { + ContainerControl = form, + TopMostInFullScreen = false + }; + + manager.FullScreen = true; + + Assert.False(form.TopMost); + + manager.FullScreen = false; + + Assert.True(form.TopMost); + } + + [WinFormsFact] + public void KioskModeManager_FullScreen_SetWithHandler_CallsFullScreenChanged() + { + using Form form = new(); + using KioskModeManager manager = new() + { + ContainerControl = form + }; + + int callCount = 0; + EventHandler handler = (sender, e) => + { + Assert.Same(manager, sender); + Assert.Same(EventArgs.Empty, e); + callCount++; + }; + + manager.FullScreenChanged += handler; + + manager.FullScreen = true; + Assert.Equal(1, callCount); + + manager.FullScreen = true; + Assert.Equal(1, callCount); + + manager.FullScreen = false; + Assert.Equal(2, callCount); + + manager.FullScreenChanged -= handler; + manager.FullScreen = true; + Assert.Equal(2, callCount); + } + + [WinFormsTheory] + [NewAndDefaultData] + public void KioskModeManager_OnContainerControlChanged_Invoke_CallsContainerControlChanged(EventArgs eventArgs) + { + using SubKioskModeManager manager = new(); + int callCount = 0; + EventHandler handler = (sender, e) => + { + Assert.Same(manager, sender); + Assert.Same(eventArgs, e); + callCount++; + }; + + manager.ContainerControlChanged += handler; + manager.OnContainerControlChanged(eventArgs); + Assert.Equal(1, callCount); + + manager.ContainerControlChanged -= handler; + manager.OnContainerControlChanged(eventArgs); + Assert.Equal(1, callCount); + } + + [WinFormsTheory] + [NewAndDefaultData] + public void KioskModeManager_OnFullScreenChanged_Invoke_CallsFullScreenChanged(EventArgs eventArgs) + { + using SubKioskModeManager manager = new(); + int callCount = 0; + EventHandler handler = (sender, e) => + { + Assert.Same(manager, sender); + Assert.Same(eventArgs, e); + callCount++; + }; + + manager.FullScreenChanged += handler; + manager.OnFullScreenChanged(eventArgs); + Assert.Equal(1, callCount); + + manager.FullScreenChanged -= handler; + manager.OnFullScreenChanged(eventArgs); + Assert.Equal(1, callCount); + } + + [WinFormsTheory] + [EnumData] + public void KioskModeManager_WakeUpCommand_ExecutedOnWakeup_WithSourceName(KioskModeWakeupSource source) + { + using SubKioskModeManager manager = new(); + TestCommand command = new(); + manager.WakeUpCommand = command; + + Assert.Same(command, manager.WakeUpCommand); + + manager.OnWakeup(new KioskModeWakeupEventArgs(source)); + + Assert.Equal(1, command.CanExecuteCount); + Assert.Equal(source.ToString(), command.LastCanExecuteParameter); + Assert.Equal(1, command.ExecuteCount); + Assert.Equal(source.ToString(), command.LastParameter); + } + + [WinFormsFact] + public void KioskModeManager_WakeUpCommand_Null_DoesNotThrowOnWakeup() + { + using SubKioskModeManager manager = new(); + + Assert.Null(manager.WakeUpCommand); + + manager.OnWakeup(new KioskModeWakeupEventArgs(KioskModeWakeupSource.Keyboard)); + } + + [WinFormsFact] + public void KioskModeManager_WakeUpCommand_CanExecuteFalse_DoesNotExecute() + { + using SubKioskModeManager manager = new(); + TestCommand command = new() + { + CanExecuteResult = false + }; + + manager.WakeUpCommand = command; + manager.OnWakeup(new KioskModeWakeupEventArgs(KioskModeWakeupSource.Mouse)); + + Assert.Equal(1, command.CanExecuteCount); + Assert.Equal(KioskModeWakeupSource.Mouse.ToString(), command.LastCanExecuteParameter); + Assert.Equal(0, command.ExecuteCount); + Assert.Null(command.LastParameter); + } + + [WinFormsFact] + public void KioskModeManager_Site_Set_AssignsRootComponentAsContainerControl() + { + using Form form = new(); + Mock host = new(); + host.Setup(h => h.RootComponent).Returns(form); + + Mock site = new(); + site.Setup(s => s.GetService(typeof(IDesignerHost))).Returns(host.Object); + + using KioskModeManager manager = new(); + manager.Site = site.Object; + + Assert.Same(form, manager.ContainerControl); + } + + [WinFormsFact] + public void KioskModeManager_Site_Set_DoesNotOverwriteExplicitContainerControl() + { + using Form explicitForm = new(); + using Form rootForm = new(); + Mock host = new(); + host.Setup(h => h.RootComponent).Returns(rootForm); + + Mock site = new(); + site.Setup(s => s.GetService(typeof(IDesignerHost))).Returns(host.Object); + + using KioskModeManager manager = new() + { + ContainerControl = explicitForm + }; + + manager.Site = site.Object; + + Assert.Same(explicitForm, manager.ContainerControl); + } + + [WinFormsFact] + public void KioskModeManager_Site_SetWithExplicitNull_DoesNotAssignRootComponent() + { + using Form form = new(); + Mock host = new(); + host.Setup(h => h.RootComponent).Returns(form); + + Mock site = new(); + site.Setup(s => s.GetService(typeof(IDesignerHost))).Returns(host.Object); + + using KioskModeManager manager = new() + { + ContainerControl = null + }; + + manager.Site = site.Object; + + Assert.Null(manager.ContainerControl); + } + + [WinFormsFact] + public void KioskModeManager_Site_SetWithoutExplicitContainerControl_UpdatesResolvedRootComponent() + { + using Form firstForm = new(); + using Form secondForm = new(); + + Mock firstHost = new(); + firstHost.Setup(h => h.RootComponent).Returns(firstForm); + Mock secondHost = new(); + secondHost.Setup(h => h.RootComponent).Returns(secondForm); + + Mock firstSite = new(); + firstSite.Setup(s => s.GetService(typeof(IDesignerHost))).Returns(firstHost.Object); + Mock secondSite = new(); + secondSite.Setup(s => s.GetService(typeof(IDesignerHost))).Returns(secondHost.Object); + + using KioskModeManager manager = new(); + manager.Site = firstSite.Object; + Assert.Same(firstForm, manager.ContainerControl); + + manager.Site = secondSite.Object; + Assert.Same(secondForm, manager.ContainerControl); + } + + [WinFormsFact] + public void KioskModeManager_BeginInit_FullScreenSet_DoesNotEnterUntilEndInit() + { + using Form form = new() + { + Bounds = new Rectangle(10, 20, 300, 200), + FormBorderStyle = FormBorderStyle.FixedDialog, + WindowState = FormWindowState.Normal + }; + + using KioskModeManager manager = new(); + ISupportInitialize supportInitialize = manager; + Rectangle workingArea = Screen.FromRectangle(form.Bounds).WorkingArea; + + supportInitialize.BeginInit(); + manager.ContainerControl = form; + manager.FullScreen = true; + + Assert.Equal(FormBorderStyle.FixedDialog, form.FormBorderStyle); + Assert.Equal(FormWindowState.Normal, form.WindowState); + + supportInitialize.EndInit(); + + Assert.True(manager.FullScreen); + Assert.Equal(FormBorderStyle.None, form.FormBorderStyle); + Assert.Equal(FormWindowState.Normal, form.WindowState); + Assert.Equal(workingArea, form.Bounds); + } + + [WinFormsFact] + public void KioskModeManager_DisposeWhileFullScreen_RestoresExpected() + { + using Form form = new() + { + Bounds = new Rectangle(10, 20, 300, 200), + FormBorderStyle = FormBorderStyle.FixedDialog, + WindowState = FormWindowState.Normal, + TopMost = false + }; + + KioskModeManager manager = new() + { + ContainerControl = form, + TopMostInFullScreen = true, + FullScreen = true + }; + manager.Dispose(); + + Assert.Equal(FormBorderStyle.FixedDialog, form.FormBorderStyle); + Assert.Equal(FormWindowState.Normal, form.WindowState); + Assert.Equal(new Rectangle(10, 20, 300, 200), form.Bounds); + Assert.False(form.TopMost); + } + + [WinFormsFact] + public void KioskModeManager_ProcessMessage_KeyDownThenKeyUp_TogglesOnce() + { + using Form form = new(); + using KioskModeManager manager = new() + { + ContainerControl = form + }; + + Message keyDownMessage = Message.Create(form.Handle, (int)PInvokeCore.WM_KEYDOWN, (nint)Keys.F11, 0); + Message keyUpMessage = Message.Create(form.Handle, (int)PInvokeCore.WM_KEYUP, (nint)Keys.F11, 0); + + manager.TestAccessor.Dynamic.ProcessMessage(keyDownMessage); + Assert.True(manager.FullScreen); + + manager.TestAccessor.Dynamic.ProcessMessage(keyUpMessage); + Assert.True(manager.FullScreen); + } + + [WinFormsFact] + public void KioskModeManager_ProcessMessage_RepeatedKeyDown_DoesNotToggleAgain() + { + using Form form = new(); + using KioskModeManager manager = new() + { + ContainerControl = form + }; + + Message keyDownMessage = Message.Create(form.Handle, (int)PInvokeCore.WM_KEYDOWN, (nint)Keys.F11, 0); + Message repeatedKeyDownMessage = Message.Create( + form.Handle, + (int)PInvokeCore.WM_KEYDOWN, + (nint)Keys.F11, + 1 << 30); + + manager.TestAccessor.Dynamic.ProcessMessage(keyDownMessage); + manager.TestAccessor.Dynamic.ProcessMessage(repeatedKeyDownMessage); + + Assert.True(manager.FullScreen); + } + + [WinFormsFact] + public void KioskModeManager_ProcessMessage_MouseMove_CallsWakeup() + { + using Form form = new(); + using KioskModeManager manager = new() + { + ContainerControl = form + }; + + int callCount = 0; + KioskModeWakeupEventArgs lastEventArgs = null; + manager.Wakeup += (sender, e) => + { + Assert.Same(manager, sender); + callCount++; + lastEventArgs = e; + }; + + Message message = Message.Create(form.Handle, (int)PInvokeCore.WM_MOUSEMOVE, 0, 0); + manager.TestAccessor.Dynamic.ProcessMessage(message); + + Assert.Equal(1, callCount); + Assert.NotNull(lastEventArgs); + Assert.Equal(KioskModeWakeupSource.Mouse, lastEventArgs.Source); + } + + [WinFormsFact] + public void KioskModeManager_ProcessPowerBroadcast_InteractiveResume_CallsWakeupOnce() + { + using SubKioskModeManager manager = new(); + int callCount = 0; + KioskModeWakeupEventArgs lastEventArgs = null; + manager.Wakeup += (sender, e) => + { + Assert.Same(manager, sender); + callCount++; + lastEventArgs = e; + }; + + Message automaticResume = Message.Create(IntPtr.Zero, (int)PInvokeCore.WM_POWERBROADCAST, (nint)0x0012, 0); + Message interactiveResume = Message.Create(IntPtr.Zero, (int)PInvokeCore.WM_POWERBROADCAST, (nint)0x0007, 0); + manager.TestAccessor.Dynamic.ProcessPowerBroadcast(automaticResume.WParamInternal); + manager.TestAccessor.Dynamic.ProcessPowerBroadcast(interactiveResume.WParamInternal); + + Assert.Equal(1, callCount); + Assert.NotNull(lastEventArgs); + Assert.Equal(KioskModeWakeupSource.PowerResume, lastEventArgs.Source); + } + + [WinFormsTheory] + [InlineData(0x0001)] + [InlineData(0x0003)] + [InlineData(0x0005)] + [InlineData(0x0008)] + public void KioskModeManager_ProcessSessionChange_RecognizedReason_CallsWakeup(nint sessionReason) + { + using SubKioskModeManager manager = new(); + int callCount = 0; + KioskModeWakeupEventArgs lastEventArgs = null; + manager.Wakeup += (sender, e) => + { + Assert.Same(manager, sender); + callCount++; + lastEventArgs = e; + }; + + Message message = Message.Create(IntPtr.Zero, 0, sessionReason, 0); + manager.TestAccessor.Dynamic.ProcessSessionChange(message.WParamInternal); + + Assert.Equal(1, callCount); + Assert.NotNull(lastEventArgs); + Assert.Equal(KioskModeWakeupSource.Session, lastEventArgs.Source); + } + + private class TestCommand : ICommand + { + public bool CanExecuteResult { get; set; } = true; + + public int CanExecuteCount { get; private set; } + + public object LastCanExecuteParameter { get; private set; } + + public int ExecuteCount { get; private set; } + + public object LastParameter { get; private set; } + + public event EventHandler CanExecuteChanged + { + add { } + remove { } + } + + public bool CanExecute(object parameter) + { + CanExecuteCount++; + LastCanExecuteParameter = parameter; + return CanExecuteResult; + } + + public void Execute(object parameter) + { + ExecuteCount++; + LastParameter = parameter; + } + } + + private class SubKioskModeManager : KioskModeManager + { + public new bool DesignMode => base.DesignMode; + + public new void OnContainerControlChanged(EventArgs e) + => base.OnContainerControlChanged(e); + + public new void OnFullScreenChanged(EventArgs e) + => base.OnFullScreenChanged(e); + + public new void OnWakeup(KioskModeWakeupEventArgs e) + => base.OnWakeup(e); + } +} +#endif