Compare commits

..

47 Commits

Author SHA1 Message Date
Yu Leng
fbc1b8d4e8 [ColorPicker] Keep the zoom magnifier fixed in place while scrolling
The WinUI 3 magnifier re-centered its window on the cursor on every mouse-wheel step, so moving the mouse and scrolling again dragged the magnifier to the new cursor location instead of zooming the region it was first opened over. This regressed from the WPF version, which only positions the window on first appearance (while it is still transparent, Opacity < 0.5).

Move the window sizing and positioning into the first-appearance branch, so the magnifier is centered on the cursor once per zoom session and then stays put, only animating the card between zoom levels.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-07 17:13:05 +08:00
Yu Leng
56f09af496 Merge remote-tracking branch 'origin/main' into yuleng/m/cp/1 2026-07-07 15:52:51 +08:00
Yu Leng (from Dev Box)
3c159ec93a feat(colorpicker): 方案2 magnifier grid + drop dead code + overlay polish
方案2 magnifier grid (replaces the flat 方案1 grid):
- Grid line color now adapts to each cell's source-pixel brightness (dark over
  light areas, light over dark) so it stays visible on very light and very dark
  regions; grid alpha fades toward the magnifier edge (the shader's radius
  reveal); the center cursor-pixel highlight picks dark/light from its own
  brightness so it shows on a white pixel too. Source pixels are cached once per
  capture (ZoomView) instead of read back from the GPU on every draw.

Dead-code cleanup (all pre-existing, dead before the migration):
- ColorRepresentationHelper: drop 15 never-called ColorTo* methods (keep ColorToHex).
- Delete unused ColorToBrushConverter / ColorToStringConverter (+ their Styles.xaml
  registrations) and the duplicate converter keys in MainView.xaml.
- Remove ColorPickerControl.GetChildOfType<T>, HSVColor.GetSpectrum,
  ColorEditorViewModel.HideColorFormatCommand, the orphaned TitleBarCloseButtonStyle,
  the unused MainBorder x:Name, a stale using in SerializationHelper, and a duplicate
  IOException catch in UserSettings.
- Delete the orphaned Shaders/GridShader.fx (方案2 supersedes it as the reference).

Polish:
- Overlay GetMonitorBounds: when no monitors are enumerated, log a warning and fall
  back to the primary screen bounds instead of an unbounded rect, so edge-flipping
  still works.
- Overlay Show(): present the card transparent, then start the fade-in + initial
  positioning at Low priority after base.Show() so the 250ms fade begins when the
  window is actually on screen (no early/partial fade) and without a flash.

Builds 0/0 on ColorPickerUI + UnitTests. Magnifier look + fade verified on the real desktop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 16:31:24 +08:00
Yu Leng (from Dev Box)
b17e91276d fix(colorpicker): post-review correctness fixes + migration cleanup
A thorough review of the WinUI 3 migration (vs origin/main) surfaced
several issues; this applies the fixes. Builds 0/0 on ColorPickerUI and
ColorPickerUI.UnitTests.

Correctness:
- Editor Export was broken: ColorEditorViewModel.WindowHandle was never
  assigned, so the FileSavePicker was initialized with IntPtr.Zero. Add
  WindowHandle to IColorEditorViewModel and set it from AppStateHandler
  once the editor window exists.
- Installer harvested ColorPicker assets from the pre-WinUI3 root path;
  repoint generateAllFileComponents.ps1 + ColorPicker.wxs to WinUI3Apps
  so colorPicker.cur / icon.ico ship in the package.
- Restore the system cursors on process exit (AppDomain.ProcessExit):
  the Environment.Exit paths bypassed MouseInfoProvider.DisposeHook,
  leaking the crosshair as the system cursor (parity with WPF OnExit).
- Overlay tooltip re-queries the cursor monitor's DPI per move instead
  of caching it once, fixing sizing/offset on mixed-DPI multi-monitor.
- Dispose the Win2D CanvasBitmap per zoom session (ZoomWindowHelper +
  ZoomView.ClearBitmap) to stop leaking a GPU surface on every pick.
- Re-add the ETWTrace diagnostic session dropped during the migration
  (matches the WinUI 3 sibling modules).

Cleanup:
- Collapse the ~100-line csproj Compile-Remove + re-include quarantine
  (it excluded nothing) back to the default SDK glob; DefineConstants
  now appends so inherited DEBUG/TRACE survive.
- Delete dead code: ViewLocator (+test), ZoomViewModel/IZoomViewModel
  (+DI registration + ZoomWindowHelper wiring), AppStateHandler.SetTopMost,
  NativeMethods.SetToolWindowStyle (+orphaned interop), and App's
  ExitToken / RunnerPid + stale comments. App.Dispose now actually runs
  (via ProcessExit), so its IDisposable is no longer dead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 16:00:54 +08:00
Yu Leng (from Dev Box)
6d5b786fec feat(colorpicker): ease the WinUI3 magnifier resize between zoom levels
Ports the WPF ResizeBehavior feel (grow easeOutSine / shrink easeInQuad, 200ms) that the WinUI 3 magnifier lost.

ALT-1 (the WPF structural model): the zoom window is now a constant max-size host (50*8+30 = 430px) re-centered on the cursor each step, and only the inner card animates its Microsoft.UI.Composition Scale (CenterPoint pinned to card center). A fixed window that always exceeds the centered card means the briefly-larger card during a shrink never clips, so there is no per-step window resize and no shrink-trim bookkeeping. Win2D still draws at the final factor; the compositor scales that crisp texture during the tween.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 14:10:56 +08:00
Yu Leng (from Dev Box)
70f9ee13a5 fix(colorpicker): restore editor centering and overlay fade-in in WinUI3
Two small WPF behaviors were dropped during the WinUI 3 migration and are restored here:

- ColorEditorWindow now calls CenterOnScreen() (ports WindowStartupLocation="CenterScreen"); it previously opened at the OS default position.

- The picking overlay fades in over 250ms (CubicEase) on Show(), porting the WPF AppearAnimationBehavior that was lost when the merged TransparentWindow refactor removed ShowAnimations. Window has no Opacity, so the hosted MainView content is animated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 14:10:47 +08:00
Yu Leng (from Dev Box)
7526abe367 Merge remote-tracking branch 'origin/main' into yuleng/m/cp/1 2026-06-29 12:15:13 +08:00
Yu Leng (from Dev Box)
e6d0cf450e build(colorpicker): strip leaked unsigned ColorPickerUI apphost from installer root
The "Verify all binaries are signed and versioned" stage failed on
BaseApplicationsFiles_File_PowerToys.ColorPickerUI.exe (Not Signed).

A SelfContained Exe->WinExe ProjectReference (ColorPickerUI.UnitTests ->
ColorPickerUI) makes MSBuild copy the WinExe apphost (.exe/.deps.json/
.runtimeconfig.json) into the ROOT output dir as a side effect. The installer's
BaseApplications harvest picks up that leaked root PowerToys.ColorPickerUI.exe,
which ESRP never signs (it only signs the WinUI3Apps copy). Because the signed
and unsigned copies differ in hash, the dedup step kept both and the MSI shipped
the unsigned root one.

Mirror the existing ImageResizer leak workaround: strip the ColorPickerUI
apphost trio from BaseApplications. The dedup loop then keeps the signed
WinUI3Apps copy (which the native module launches anyway). The native root
PowerToys.ColorPicker.dll is unaffected (the UI suffix disambiguates).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 18:08:10 +08:00
Yu Leng (from Dev Box)
b532b8854f fix(colorpicker): move window XAML into ColorPickerXAML subfolder
The WinUI3Apps asset-conflict audit (.pipelines/verifyPossibleAssetConflicts.ps1)
rejects any *.xbf in the shared WinUI3Apps output root. ColorEditorWindow,
ColorPickerOverlayWindow and ZoomWindow lived at the project root, so their
compiled .xbf landed directly in WinUI3Apps\ and failed the audit.

Move all three .xaml + .xaml.cs into the existing ColorPickerXAML\ subfolder
(alongside App.xaml), matching every other WinUI3 app (AdvancedPaste/PowerDisplay/
Hosts/MeasureTool). No csproj change (default SDK globs) and no code change
(x:Class/namespace are folder-independent); the .xbf now lands under
WinUI3Apps\ColorPickerXAML\.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 14:24:01 +08:00
Yu Leng (from Dev Box)
ce8569bc0c style(colorpicker): apply XamlStyler formatting to fix CI XAML check
Run xstyler over the WinUI 3 ColorPicker XAML so it conforms to
src/Settings.XamlStyler, fixing the "Verify XAML formatting" CI step.
Formatting-only: add UTF-8 BOM, reorder x:FieldModifier, and split
multi-line comment delimiters. No semantic changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 12:58:13 +08:00
Yu Leng (from Dev Box)
8e7ccad936 Merge remote-tracking branch 'origin/main' into yuleng/m/cp/1 2026-06-28 12:29:42 +08:00
Yu Leng (from Dev Box)
6ba3b25a0b fix(colorpicker): polish the WinUI 3 color editor (icon, Mica, taller HSV sliders, clean history)
Desktop-verification polish for the ported editor (Approach A - native WinUI chrome):

- ColorEditorWindow: set the title-bar/taskbar icon to the ColorPicker app icon (was the generic
  WinUI placeholder), and add a Mica SystemBackdrop so the window's uncovered header no longer
  renders as an opaque black bar against the content card.
- ColorPickerControl: grow the H/S/V gradient bars from 8px to 16px (CornerRadius 8) so the native
  Slider thumb sits on them naturally instead of dwarfing a thin strip.
- ColorEditorView: hide the color-history horizontal scrollbar - WinUI's persistent Fluent scroll
  indicator read as a stray gray bar under the swatches; the mouse wheel still scrolls the history
  via the existing PointerWheelChanged handler.

Build: 0 warnings, 0 errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 12:24:46 +08:00
Yu Leng (from Dev Box)
adee12a08d fix(colorpicker): make the picking overlay a cursor-following tooltip (WinUI)
The migration showed the picking overlay as a default ~1920x1030 window: the WPF MainWindow's
SizeToContent + ChangeWindowPositionBehavior (cursor-follow) + re-asserted Topmost were never
ported (a stale 7d TODO shipped through 7e-1, and the behavior lived in the quarantined Behaviors\).

ColorPickerOverlayWindow now:
- sizes the window to the MainView tooltip (WinUI has no SizeToContent), measured only on show /
  content change and cached otherwise;
- follows the cursor via IMouseInfoProvider.MousePositionChanged, with the WPF behavior's
  monitor-edge dead-zone flip; it also positions on Show() since the event only fires on movement
  (a stationary-cursor show would otherwise leave the tooltip parked);
- stays HWND_TOPMOST so the tooltip sits above the (non-top-most) zoom magnifier, matching the WPF
  helper that re-asserted MainWindow.Topmost after showing the zoom;
- caches the DPI scale + monitor bounds per pick session and moves with a single SetWindowPos per
  cursor tick (no per-tick layout pass), keeping following light.

App.OnLaunched wires the shared IMouseInfoProvider singleton into the overlay.

Desktop-verified: the small tooltip follows the cursor, the color readout updates, pick-in-magnifier
works, and the card sits above the magnifier and returns after zoom-out. Build: 0 warnings, 0 errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 11:07:10 +08:00
Yu Leng (from Dev Box)
28d7a21d4c chore(colorpicker): remove dead WPF code left over from the WinUI 3 migration
The WPF->WinUI 3 migration is structurally complete, so delete the source that has no
remaining consumer:

- ColorPickerXAML\MainWindow.xaml(.cs): unused empty shell; App hosts ColorPickerOverlayWindow.
- Behaviors\*.cs (7 files): replaced by TransparentWindow built-ins, services, the native
  ListView reorder, and Win2D - none were re-included in the build.
- Shaders\GridShaderEffect.cs, Global.cs, GridShader.cso: the WPF pixel-grid shader, replaced by
  the Win2D ZoomView. GridShader.fx is kept as the reference spec for the alternative magnifier.
- Helpers\ControlHelper.cs: a WPF (System.Windows) attached-property helper with no consumer; its
  CornerRadius attached property is unnecessary in WinUI (see Styles.xaml). It was already
  excluded from the build by the Helpers\ quarantine glob.

Drop the now-empty Behaviors\ / Shaders\ Compile-Remove globs and refresh the csproj comment.
Build: 0 warnings, 0 errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:44:29 +08:00
Yu Leng (from Dev Box)
b89ecf5e51 build(colorpicker): fix installer satellite + signing paths for WinUI 3
- Remove the per-language ColorPicker satellite component from Resources.wxs: WinUI 3 localizes
  via the .pri compiled from Strings\*\Resources.resw, so PowerToys.ColorPickerUI.resources.dll
  is no longer produced and referencing it broke the WiX harvest.
- ESRPSigning: point PowerToys.ColorPickerUI.dll/.exe at the WinUI3Apps\ output (the WinUI 3 app
  now builds there), mirroring AdvancedPaste; the native PowerToys.ColorPicker.dll stays at root.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:31:49 +08:00
Yu Leng (from Dev Box)
e1c2a4bd01 feat(colorpicker): port the zoom magnifier with a Win2D pixel grid (7e-2)
Replace the no-op ZoomWindowHelper placeholder and the quarantined WPF ZoomWindow/ZoomView +
GridShaderEffect with a Win2D implementation (方案1):
- ZoomView hosts a single Win2D CanvasControl that draws the GDI-captured region scaled with
  nearest-neighbor filtering and overlays the pixel grid + center highlight (replacing the WPF
  ShaderEffect; the brightness-adaptive color + radius reveal from GridShader.fx are simplified
  for now and can be refined, or escalated to 方案2, after desktop verification).
- ZoomWindow derives from the shared TransparentWindow (transparent, frameless, no-activate).
- ZoomWindowHelper captures a 50x50 region around the cursor, converts the GDI bitmap to a
  CanvasBitmap, sizes + centers the magnifier on the cursor (physical pixels), and shows it;
  CopyFromScreen failures degrade gracefully. WindowCaptureExclusionHelper re-included as-is.
- Add the Microsoft.Graphics.Win2D package; un-quarantine ZoomView/ZoomWindow. The WPF
  Behaviors\ and Shaders\ stay quarantined (intentionally dead; GridShader.fx kept as the spec).

This was the last quarantined feature. Build is clean and the app starts; the magnifier's
on-screen behavior needs desktop verification (and the dev box's flaky GDI screen capture means
it can only be fully verified on a local interactive desktop).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:17:58 +08:00
Yu Leng (from Dev Box)
20256f3898 fix(colorpicker): don't crash at startup when CopyFromScreen fails
MouseInfoProvider samples the pixel under the cursor in its constructor (resolved during
App.OnLaunched, before the overlay is shown). On a non-interactive / disconnected session the
GDI CopyFromScreen can throw "the handle is invalid", which previously took down the whole app
at startup. Swallow the failure in GetPixelColor and return black; the next sample succeeds
once a real desktop is present. Found via the startup smoke test during 7e-1 verification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 16:47:50 +08:00
Yu Leng (from Dev Box)
cb9103a0a8 feat(colorpicker): wire the picking overlay + editor end-to-end (7e-1)
Port the mutually-dependent overlay/editor integration cluster off WPF + MEF and wire it into
the WinUI 3 startup path. The module now launches, resolves its DI graph, creates the overlay,
registers the named events, and stays alive (startup smoke test passes); the picking/editor
flow is ready for desktop verification.

- AppStateHandler: MEF -> DI; drives the picking overlay via the TransparentWindow-based
  ColorPickerOverlayWindow (App.Window) Show()/Hide() instead of Application.Current.MainWindow;
  hosts the editor via ColorEditorWindow; HwndSource -> IntPtr; reads EditorState for the
  Esc-block flag; cursor move via NativeMethodsHelper GetCursorPos/SetCursorPos.
- ColorEditorWindow: WPF WindowChrome/DWM/custom-title-bar -> native WinUI title bar + a fixed
  OverlappedPresenter; AppWindow.Closing cancels and ends the session (hides), not destroys.
- MouseInfoProvider: MEF -> DI; DispatcherTimer -> DispatcherQueueTimer; System.Windows.Point ->
  Windows.Foundation.Point; the wheel hook now carries a raw delta.
- KeyboardMonitor: MEF -> DI; System.Windows.Input.Key/KeyInterop -> Windows.System.VirtualKey.
- MainViewModel: MEF -> DI + ObservableObject; the three NativeEventWaiter loops drop the
  dispatcher arg (the primitive captures the DispatcherQueue itself); Application.Current.Shutdown
  -> cancel the exit token + Environment.Exit; Brush/Color -> Microsoft.UI.Xaml.Media /
  Windows.UI.Color; RegisterWindowHandle(IntPtr).
- ZoomWindowHelper is a no-op placeholder (the Win2D magnifier is 7e-2).
- App.OnLaunched builds ColorPickerOverlayWindow, resolves IMainViewModel, hosts it in MainView,
  and registers the HWND; AppServices registers the new singletons.

Build is clean and the app starts. Hotkey-show / pick / editor behavior needs desktop
verification; the zoom magnifier (7e-2) is not implemented yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 16:37:49 +08:00
Yu Leng (from Dev Box)
f247e021d3 feat(colorpicker): port ColorEditorView off WPF (ContextMenu->MenuFlyout, VSM)
- ListView.ContextMenu / MenuItem -> ListView.ContextFlyout / MenuFlyout(SubItem); the WPF
  FindAncestor PlacementTarget.SelectedItems command parameter -> {Binding SelectedItems,
  ElementName=HistoryColors}.
- The ListViewItem ControlTemplate triggers (custom selection ellipse) -> VisualStateManager
  CommonStates / SelectionStates.
- VirtualizingStackPanel -> ItemsStackPanel; MouseWheel -> PointerWheelChanged with
  ScrollViewer.ChangeView; DynamicResource -> ThemeResource (the SystemColors accent brush ->
  AccentFillColorDefaultBrush).
- {x:Static p:Resources.*} labels / automation names / menu headers resolved in code-behind
  via ResourceLoaderInstance.

Re-included past the Views quarantine (only ZoomView remains). Build is clean; the editor
list/menu interaction needs desktop verification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 15:52:25 +08:00
Yu Leng (from Dev Box)
1443f340d3 feat(colorpicker): port ColorPickerControl off WPF (native Slider restyle + Flyout)
The largest editor control, ported with approach A (lean on native WinUI):
- Slider: drop the WPF Track/TickBar/RepeatButton custom template; use native WinUI Sliders
  whose track theme brushes are overridden to Transparent so the gradient Borders behind
  (HueGradientBorder + the Saturation/Value LinearGradientBrush stops) show through.
- Adjust-color Popup -> a Button.Flyout; Popup.IsOpen toggling -> Flyout open/Hide().
- WPF DoubleAnimation/ThicknessAnimation via BeginAnimation -> a Storyboard (Height as a
  dependent animation; WinUI has no ThicknessAnimation, so the margin is set directly).
- Slider.ValueChanged RoutedPropertyChangedEventArgs<double> -> RangeBaseValueChangedEventArgs;
  PreviewTextInput -> TextBox.BeforeTextChanging; GotKeyboardFocus -> GotFocus.
- Color -> Windows.UI.Color; brushes/GradientStop/Point -> Microsoft.UI.Xaml.Media /
  Windows.Foundation; the custom automation peer derives from FrameworkElementAutomationPeer
  (no UserControlAutomationPeer in WinUI) and is partial (CsWinRT ABI requirement).
- {x:Static p:Resources.*} automation names/tooltips resolved in code via ResourceLoaderInstance.
- Extract AppStateHandler.BlockEscapeKeyClosingColorPickerEditor into a small EditorState static
  so the control no longer depends on the window-coupled AppStateHandler.

All Controls are now off the quarantine. Build is clean. The slider/flyout interaction needs
desktop verification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 15:39:00 +08:00
Yu Leng (from Dev Box)
bb81d9b408 feat(colorpicker): port ColorFormatControl off WPF (DP + Storyboard + DI)
- DependencyProperties on Microsoft.UI.Xaml; Color -> Windows.UI.Color.
- Bootstrapper MEF lookup -> App.GetService<IThrottledActionInvoker>().
- Copy-indicator WPF DoubleAnimation/BeginAnimation -> a Storyboard (the Height leg opts into
  EnableDependentAnimation); narrator MenuOpened -> LiveRegionChanged.
- {x:Static p:Resources.*} automation names / tooltips and the RelativeSource FindAncestor
  help-text binding (no WinUI equivalent) move to code-behind via ResourceLoaderInstance.
- ReadonlyTextBoxStyle simplified to BasedOn DefaultTextBoxStyle (approach A: lean on native).
- Re-included past the Controls quarantine (HSVColor + ColorFormatControl are default-glob now;
  only ColorPickerControl stays quarantined). Build is clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 15:16:43 +08:00
Yu Leng (from Dev Box)
cc639237e1 feat(colorpicker): port Styles.xaml (native WinUI styles) and merge into App.xaml
Port the shared resource dictionary off WPF (approach A: lean on native WinUI styling)
and merge it into App.xaml so the editor controls can resolve their styles/converters:
- Drop CustomAccentButtonStyle (consumers will use the built-in AccentButtonStyle).
- SubtleButtonStyle / TitleBarCloseButtonStyle now derive from the native DefaultButtonStyle
  via BasedOn, inheriting proper Fluent visual states (the WPF red-on-hover close fill is
  dropped pending editor visual polish).
- ColorShadeButtonStyle keeps a custom template, rewritten with the VisualStateManager and
  the built-in Control.CornerRadius -- the WPF-era ControlHelper.CornerRadius attached
  property is unnecessary in WinUI, so ControlHelper is not ported.
- Declare the six value converters and IconFontFamily.

App launches cleanly (startup smoke test passes; the merged dictionary loads at runtime
without error). Build is clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 15:07:17 +08:00
Yu Leng (from Dev Box)
7183772e47 feat(colorpicker): port ColorEditorViewModel off WPF (DI + RelayCommand + FileSavePicker)
- ObservableObject base (custom ViewModelBase removed); CommunityToolkit.Mvvm
  RelayCommand<object> / AsyncRelayCommand<object> replace the custom RelayCommand.
- System.Windows.Media.Color -> Windows.UI.Color throughout (history + selected color).
- Export now uses the WinRT FileSavePicker (async) instead of WPF SaveFileDialog; a new
  WindowHandle property lets the host editor window anchor the picker (desktop requirement).
- MEF export dropped (DI registration lands with the overlay/editor wiring).

Re-included past the ViewModels quarantine; the editor window/view/controls stay
quarantined pending the XAML port. Build is clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 14:53:17 +08:00
Yu Leng (from Dev Box)
39b576dae4 feat(colorpicker): port ZoomViewModel off WPF (ObservableObject + ImageSource)
ZoomViewModel now derives from CommunityToolkit.Mvvm ObservableObject (the custom
ViewModelBase was removed earlier) and exposes ZoomArea as
Microsoft.UI.Xaml.Media.ImageSource instead of WPF BitmapSource; the MEF export is
dropped (DI registration lands with the overlay wiring). Re-included past the ViewModels
quarantine; ZoomWindow / ZoomWindowHelper stay quarantined pending the Win2D magnifier.
Build is clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 14:40:24 +08:00
Yu Leng (from Dev Box)
3d32896542 feat(colorpicker): port the ViewModel contracts off WPF types
Port and un-quarantine IMainViewModel / IZoomViewModel / IColorEditorViewModel:
- IMainViewModel: System.Windows.Media.Brush -> Microsoft.UI.Xaml.Media.Brush;
  RegisterWindowHandle(HwndSource) -> RegisterWindowHandle(IntPtr).
- IZoomViewModel: System.Windows.Media.Imaging.BitmapSource -> Microsoft.UI.Xaml.Media.ImageSource.
- IColorEditorViewModel: System.Windows.Media.Color -> Windows.UI.Color (ICommand kept --
  System.Windows.Input.ICommand is the BCL type WinUI and CommunityToolkit.Mvvm already use).

These unblock the view models that implement them. Build is clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 14:34:46 +08:00
Yu Leng (from Dev Box)
79c1a82a2e feat(colorpicker): add TransparentWindow-based overlay shell + port MainView
Build the picking-overlay window on the shared Common.UI.Controls TransparentWindow
(introduced by PR #48176) instead of hand-rolling AppWindow transparency:
- ColorPickerOverlayWindow : TransparentWindow hosts the tooltip and inherits the
  transparent TransparentTintBackdrop, frameless tool-window chrome (out of taskbar/
  Alt-Tab, no Win11 phantom border), and a no-activate Show/Hide that never steals focus.
- Port MainView (the tooltip content): clr-namespace -> using, DynamicResource ->
  ThemeResource, ApplicationBackgroundBrush -> LayerFillColorDefaultBrush, keep loose
  {Binding} (no MainViewModel compile dependency yet), declare the two visibility
  converters locally, and replace the WPF AutomationEvents.MenuOpened narrator hack with
  LiveRegionChanged (paired with the existing LiveSetting=Assertive).
- Add a non-inverted BoolToVisibilityConverter.
- Reference Common.UI.Controls and CommunityToolkit.WinUI.Animations (the XAML compiler
  needs the latter to resolve TransparentWindow's ShowAnimations/HideAnimations).
- Narrow the Views compile/page quarantine to just the still-WPF editor and zoom views.

Not yet wired into the App launch path (waits on the MainViewModel / AppStateHandler
port). Build is clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 14:26:57 +08:00
Yu Leng (from Dev Box)
277efe593a feat(colorpicker): port Win32 input primitives (mouse/keyboard hooks) off WPF
Re-include the window-independent input layer past the Mouse/Keyboard quarantines:
- MouseHook: drop System.Windows.Input (MouseWheelEventArgs/InputManager/MouseDevice);
  the wheel event now carries the raw signed delta (high word of MSLLHOOKSTRUCT.mouseData),
  letting consumers derive zoom direction from its sign.
- IMouseInfoProvider: System.Windows.Point -> Windows.Foundation.Point.
- CursorManager, GlobalKeyboardHook, GlobalKeyboardHookEventArgs: re-included verbatim
  (already WPF-free Win32 / registry code).

The stateful consumers (MouseInfoProvider, KeyboardMonitor) stay quarantined because they
depend on AppStateHandler, which in turn depends on the not-yet-rebuilt overlay window.
Build is clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 14:03:37 +08:00
Yu Leng (from Dev Box)
679192d1ad refactor(colorpicker): delete legacy WPF root MainWindow to clear duplicate x:Class
The quarantined root MainWindow.xaml/.cs (the WPF picking-overlay shell:
AllowsTransparency, SizeToContent, Topmost, WindowStyle=None, MEF [Import]
IMainViewModel, HwndSource window-handle registration) declared the same
x:Class "ColorPicker.MainWindow" as the live WinUI 3 ColorPickerXAML\MainWindow.xaml,
coexisting only because the build quarantined the root file. Delete it -- git
history preserves the old WPF shell, and the sub-project C overlay port builds a
fresh AppWindow-based window rather than porting the WPF Window line-by-line -- and
drop the now-obsolete csproj quarantine entries, permanently removing the
duplicate-partial-class hazard before overlay work begins.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:52:24 +08:00
Yu Leng (from Dev Box)
b641f536dc feat(colorpicker): port color math + value converters off WPF to Windows.UI.Color
- Move the ~76-key string table from the dead Properties\Resources.resx to
  Strings\en-us\Resources.resw and delete the .resx; normalize the legacy
  "Lightest color" key (stray space) to "Lightest_color" for MRT.
- ColorRepresentationHelper: System.Windows.Media.Color -> Windows.UI.Color;
  collapse the 42-case color-name switch to ResourceLoaderInstance.GetString;
  make ReplaceName resolve the color name lazily (only when the format contains
  the name placeholder) so numeric formats skip the resource lookup and the
  pure-conversion unit tests don't require the MRT .pri in the test host.
- ColorHelper re-included as-is (no WPF deps).
- HSVColor / ColorFormatModel / SerializationHelper: System.Windows.Media.Color
  -> Windows.UI.Color.
- 5 value converters: IValueConverter(string language) signature +
  Microsoft.UI.Xaml namespaces.
- Re-include the ported files past the csproj folder quarantines; un-quarantine
  the three color unit tests (378 color-logic test cases pass; build is clean).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:45:54 +08:00
Yu Leng
3938ebd96f fix(colorpicker): align InternalsVisibleTo to the test assembly name
Sub-project D's NativeMethodsHelperTests is the first unit test to consume
an internal ColorPicker type (internal NativeMethodsHelper). The production
InternalsVisibleTo named "UnitTest-ColorPickerUI" but the test project's
assembly is "ColorPickerUI.UnitTests" (no AssemblyName override), so the
IVT did not apply and that test would fail to compile once the test project
builds. Align the IVT to the actual test assembly name. Found by the
whole-branch review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 18:09:09 +08:00
Yu Leng
230cbcfa03 feat(colorpicker): launch UI from WinUI3Apps\ + guard frozen named-event contract
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 17:59:37 +08:00
Yu Leng
3936ac7a78 feat(colorpicker): add DispatcherQueue NativeEventWaiter primitive (A §12a carry-forward)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 17:51:38 +08:00
Yu Leng
27462c49a1 feat(colorpicker): port Settings (UserSettings/IUserSettings/SettingItem) off MEF + DI singleton
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 17:46:36 +08:00
Yu Leng
de2e076b2f feat(colorpicker): re-include Telemetry + SessionEventHelper (verbatim, no WPF) 2026-06-25 17:41:17 +08:00
Yu Leng
905f9fc2d5 feat(colorpicker): port ClipboardHelper to WinRT DataPackage (SetContent+Flush) 2026-06-25 17:35:22 +08:00
Yu Leng
f317b6a027 feat(colorpicker): port ThrottledActionInvoker to DispatcherQueueTimer + DI singleton 2026-06-25 17:26:41 +08:00
Yu Leng
c5565f62a3 feat(colorpicker): port MonitorResolutionHelper to Windows.Foundation.Rect, drop WPF DpiScale 2026-06-25 17:20:57 +08:00
Yu Leng
6da9743f26 feat(colorpicker): port native interop (NativeMethods/NativeMethodsHelper) off System.Windows.Point
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 17:14:13 +08:00
Yu Leng
0569391c57 test(colorpicker): assert GetService<CancellationToken> resolves to the source token (A §12b carry-forward) 2026-06-25 17:09:00 +08:00
Yu Leng
5bfe386117 chore(colorpicker): post-review tidy (ExitToken cancel on runner exit, docs, manifest casing)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 15:37:31 +08:00
Yu Leng
7eac5f9890 feat(colorpicker): ResourceLoaderInstance + .resw scaffold; drop dead .resx designer
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 15:25:35 +08:00
Yu Leng
63008d264c feat(colorpicker): ViewLocator replaces implicit WPF DataTemplate mapping
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 15:20:10 +08:00
Yu Leng
8844f61ae8 refactor(colorpicker): drop custom MVVM base, keep RangeObservableCollection
- git rm Common/ViewModelBase.cs and Common/RelayCommand.cs (dead WPF MVVM helpers)
- Remove their now-obsolete <Compile Remove> lines from ColorPickerUI.csproj
- Add RangeObservableCollectionTests (1 test: AddRange raises single Reset)
- All 2 Foundation tests pass (ServiceProviderSmokeTests + new collection test)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 15:13:47 +08:00
Yu Leng
4e72726b0a feat(colorpicker): DI container via Microsoft.Extensions.DependencyInjection
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 15:07:02 +08:00
Yu Leng
a065e04124 test(colorpicker): quarantine legacy color tests so the test project builds
Exclude Helpers\ColorConverterTest.cs, Helpers\ColorFormatConversionTest.cs,
and Helpers\ColorRepresentationHelperTest.cs from compilation via
<Compile Remove> in ColorPickerUI.UnitTests.csproj. These tests reference
ColorPicker.Helpers types quarantined in the UI project during the WPF→WinUI 3
migration (Task 1). Files remain on disk for re-enablement in sub-project B.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 14:59:29 +08:00
Yu Leng
b1187982d5 feat(colorpicker): WinUI 3 entry point, App, and empty MainWindow
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 14:49:20 +08:00
Yu Leng
69057330fd build(colorpicker): flip ColorPickerUI to WinUI 3 + quarantine WPF sources
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 14:37:45 +08:00
120 changed files with 2550 additions and 4988 deletions

View File

@@ -38,8 +38,8 @@
"PowerToys.ColorPicker.dll",
"PowerToys.ColorPickerUI.dll",
"PowerToys.ColorPickerUI.exe",
"WinUI3Apps\\PowerToys.ColorPickerUI.dll",
"WinUI3Apps\\PowerToys.ColorPickerUI.exe",
"PowerToys.CropAndLockModuleInterface.dll",
"PowerToys.CropAndLock.exe",

View File

@@ -3,7 +3,7 @@
<?include $(sys.CURRENTDIR)\Common.wxi?>
<?define ColorPickerAssetsFiles=?>
<?define ColorPickerAssetsFilesPath=$(var.BinDir)Assets\ColorPicker\?>
<?define ColorPickerAssetsFilesPath=$(var.BinDir)WinUI3Apps\Assets\ColorPicker\?>
<Fragment>
<!-- Color Picker -->

View File

@@ -171,12 +171,9 @@
</RegistryKey>
<File Id="FancyZonesEditor_$(var.IdSafeLanguage)_File" Source="$(var.BinDir)\$(var.Language)\PowerToys.FancyZonesEditor.resources.dll" />
</Component>
<Component Id="ColorPicker_$(var.IdSafeLanguage)_Component" Directory="Resource$(var.IdSafeLanguage)INSTALLFOLDER" Guid="$(var.CompGUIDPrefix)03">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="ColorPicker_$(var.IdSafeLanguage)_Component" Value="" KeyPath="yes" />
</RegistryKey>
<File Id="ColorPicker_$(var.IdSafeLanguage)_File" Source="$(var.BinDir)\$(var.Language)\PowerToys.ColorPickerUI.resources.dll" />
</Component>
<!-- ColorPicker satellite resources removed during the WinUI 3 migration: the WinUI 3
ColorPicker localizes via the .pri compiled from Strings\*\Resources.resw, so no
PowerToys.ColorPickerUI.resources.dll is produced (referencing it broke the harvest). -->
<Component Id="MarkdownPreviewHandler_$(var.IdSafeLanguage)_Component" Directory="Resource$(var.IdSafeLanguage)INSTALLFOLDER" Guid="$(var.CompGUIDPrefix)04">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="MarkdownPreviewHandler_$(var.IdSafeLanguage)_Component" Value="" KeyPath="yes" />

View File

@@ -142,20 +142,27 @@ if ($platform -ceq "arm64") {
}
#BaseApplications
# WORKAROUND: Exclude ImageResizer files that leak into the root output directory.
# ImageResizerCLI (Exe, SelfContained) has a ProjectReference to ImageResizerUI (WinExe, SelfContained).
# MSBuild copies the referenced WinExe's apphost (.exe, .deps.json, .runtimeconfig.json) to the root
# output directory as a side effect. These files are incomplete (missing the managed .dll) and should
# not be included in the installer. The complete ImageResizer files are in WinUI3Apps/ and are handled
# by WinUI3ApplicationsFiles. TODO: Refactor ImageResizer to use a shared Library project instead.
# WORKAROUND: Exclude app-host files that leak into the root output directory.
# A SelfContained Exe with a ProjectReference to a SelfContained WinExe makes MSBuild copy the
# referenced WinExe's apphost (.exe, .deps.json, .runtimeconfig.json) to the root output directory
# as a side effect. These files are incomplete (missing the managed .dll) and should not be in the
# installer; the complete app is in WinUI3Apps/ and is handled by WinUI3ApplicationsFiles.
# - ImageResizer: ImageResizerCLI (Exe) references ImageResizerUI (WinExe).
# - ColorPicker: ColorPickerUI.UnitTests (Exe) references ColorPickerUI (WinExe). The leaked root
# PowerToys.ColorPickerUI.exe is also unsigned (ESRP only signs the WinUI3Apps copy), so it must
# be stripped or the "Verify all binaries are signed and versioned" stage fails.
# TODO: Refactor these to use a shared Library project instead.
Generate-FileList -fileDepsJson "" -fileListName BaseApplicationsFiles -wxsFilePath $PSScriptRoot\BaseApplications.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release"
# Remove leaked ImageResizer artifacts from BaseApplications
# Remove leaked app-host artifacts from BaseApplications
$baseAppWxsPath = "$PSScriptRoot\BaseApplications.wxs"
$baseAppWxs = Get-Content $baseAppWxsPath -Raw
$baseAppWxs = $baseAppWxs -replace 'PowerToys\.ImageResizer\.exe;?', ''
$baseAppWxs = $baseAppWxs -replace 'PowerToys\.ImageResizer\.deps\.json;?', ''
$baseAppWxs = $baseAppWxs -replace 'PowerToys\.ImageResizer\.runtimeconfig\.json;?', ''
$baseAppWxs = $baseAppWxs -replace 'PowerToys\.ColorPickerUI\.exe;?', ''
$baseAppWxs = $baseAppWxs -replace 'PowerToys\.ColorPickerUI\.deps\.json;?', ''
$baseAppWxs = $baseAppWxs -replace 'PowerToys\.ColorPickerUI\.runtimeconfig\.json;?', ''
# Clean up trailing/double semicolons left after removal
$baseAppWxs = $baseAppWxs -replace ';;+', ';'
$baseAppWxs = $baseAppWxs -replace '=;', '='
@@ -237,7 +244,7 @@ Generate-FileList -fileDepsJson "" -fileListName AwakeImagesFiles -wxsFilePath $
Generate-FileComponents -fileListName "AwakeImagesFiles" -wxsFilePath $PSScriptRoot\Awake.wxs
#ColorPicker
Generate-FileList -fileDepsJson "" -fileListName ColorPickerAssetsFiles -wxsFilePath $PSScriptRoot\ColorPicker.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\ColorPicker"
Generate-FileList -fileDepsJson "" -fileListName ColorPickerAssetsFiles -wxsFilePath $PSScriptRoot\ColorPicker.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\ColorPicker"
Generate-FileComponents -fileListName "ColorPickerAssetsFiles" -wxsFilePath $PSScriptRoot\ColorPicker.wxs
#Environment Variables

View File

@@ -48,7 +48,6 @@
<Style x:Key="DefaultDockItemControlStyle" TargetType="local:DockItemControl">
<Style.Setters>
<Setter Property="AutomationProperties.LiveSetting" Value="Off" />
<Setter Property="Background" Value="{ThemeResource DockItemBackground}" />
<Setter Property="BorderBrush" Value="{ThemeResource DockItemBorderBrush}" />
<Setter Property="Padding" Value="{StaticResource DockItemPadding}" />

View File

@@ -10,6 +10,7 @@ using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.Common.Helpers;
using Microsoft.CmdPal.Common.Messages;
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.UI.Controls;
using Microsoft.CmdPal.UI.Dock;
using Microsoft.CmdPal.UI.Events;
@@ -662,8 +663,6 @@ public sealed partial class MainWindow : WindowEx,
var borderColor = showFrame ? DWMWA_COLOR_DEFAULT : DWMWA_COLOR_NONE;
PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_BORDER_COLOR, &borderColor, sizeof(uint));
}
RedrawWindow(_hwnd);
}
private void InitializeBackdropSupport()
@@ -728,7 +727,7 @@ public sealed partial class MainWindow : WindowEx,
var positionWindowForAnchor = (HWND hwnd) =>
{
PInvoke.GetWindowRect(hwnd, out var bounds);
var swpFlags = SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE | SET_WINDOW_POS_FLAGS.SWP_NOZORDER | SET_WINDOW_POS_FLAGS.SWP_FRAMECHANGED;
var swpFlags = SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE | SET_WINDOW_POS_FLAGS.SWP_NOZORDER;
switch (anchorCorner)
{
case AnchorPoint.TopLeft:
@@ -828,33 +827,17 @@ public sealed partial class MainWindow : WindowEx,
// topmost status when we hide the window (because we cloak it instead
// of hiding it).
//
// SWP_FRAMECHANGED re-sends WM_NCCALCSIZE so the OS recomputes the
// (zero-width) frame. But on this cloak->show path it doesn't reliably
// *repaint* the non-client area, so the WS_THICKFRAME border that was
// painted earlier survives on the top/left/right edges (the bottom is
// clipped away by the card region). A real resize fixes it because it
// forces that NC repaint - so we do the same explicitly below.
// SWP_FRAMECHANGED is load-bearing for the borderless look on a cold
// start. Asking for SWP_FRAMECHANGED here re-sends WM_NCCALCSIZE and
// forces the NC repaint every time we show, so the frame is gone from
// the very first summon.
PInvoke.SetWindowPos(hwnd, HWND.HWND_TOPMOST, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_FRAMECHANGED);
// Force the non-client frame to actually redraw (RDW_FRAME) and the
// client to repaint over wherever it used to be. Without this the stale
// border lingers until the user resizes the window.
RedrawWindow(hwnd);
// Treat the overall show/hide lifecycle as the authoritative
// visibility transition, not the lower-level cloak/uncloak helpers.
SetIsVisibleToUser(true);
}
private static void RedrawWindow(HWND hwnd)
{
const uint RDW_INVALIDATE = 0x0001;
const uint RDW_UPDATENOW = 0x0100;
const uint RDW_ALLCHILDREN = 0x0080;
const uint RDW_FRAME = 0x0400;
_ = RedrawWindow(hwnd, IntPtr.Zero, IntPtr.Zero, RDW_INVALIDATE | RDW_UPDATENOW | RDW_ALLCHILDREN | RDW_FRAME);
}
private static DisplayArea GetScreen(HWND currentHwnd, MonitorBehavior target)
{
// Leaving a note here, in case we ever need it:
@@ -1319,10 +1302,6 @@ public sealed partial class MainWindow : WindowEx,
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DeleteObject(IntPtr hObject);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool RedrawWindow(IntPtr hWnd, IntPtr lprcUpdate, IntPtr hrgnUpdate, uint flags);
internal void MainWindow_Activated(object sender, WindowActivatedEventArgs args)
{
if (!_themeServiceInitialized && args.WindowActivationState != WindowActivationState.Deactivated)
@@ -1689,17 +1668,6 @@ public sealed partial class MainWindow : WindowEx,
case PInvoke.WM_NCCALCSIZE when wParam.Value != 0 && _hwndFrameVisible != true:
return (LRESULT)0;
// On every activation change the OS repaints the non-client frame to
// flip between the active / inactive caption. With WS_THICKFRAME still
// present that paints the OS border back over our borderless window each
// time we lose (or gain) focus - which is the frame that reappears when
// another window steals focus, and the one that shows up shortly after
// startup (the first activation flip). Passing -1 as the update-region
// (lParam) to DefWindowProc keeps the correct activation result while
// telling it to skip the non-client repaint entirely.
case PInvoke.WM_NCACTIVATE when _hwndFrameVisible != true:
return PInvoke.CallWindowProc(_originalWndProc, hwnd, uMsg, wParam, new LPARAM(-1));
case PInvoke.WM_HOTKEY:
{
var hotkeyIndex = (int)wParam.Value;

View File

@@ -105,7 +105,6 @@ WM_ACTIVATE
WM_ACTIVATEAPP
WA_INACTIVE
WM_DISPLAYCHANGE
WM_NCACTIVATE
WM_SYSCOMMAND
WM_SETTINGCHANGE
WM_WINDOWPOSCHANGING

View File

@@ -121,9 +121,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
public bool ExpandedMode { get; set; }
// Item keybindings act on the selected item, which is hidden while collapsed — only honor them when expanded.
private bool ItemActionsAllowed => !_compactMode || ExpandedMode;
public ShellPage()
{
_settingsService = App.Current.Services.GetRequiredService<ISettingsService>();
@@ -952,74 +949,47 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
case VirtualKey.F when modifiers.OnlyAlt: // Alt+F: toggle filter focus
((ShellPage)sender).ToggleFilterFocus();
e.Handled = true;
break;
case VirtualKey.Down when modifiers.None:
case VirtualKey.Tab when modifiers.None:
// In a collapsed compact palette, Down/Tab reveals the top-level items so the
// user can browse and discover them. Only swallow the key when we actually
// expand; otherwise let it fall through to normal list navigation / focus move.
if (((ShellPage)sender).TryExpandCollapsedCompact())
{
e.Handled = true;
}
break;
default:
{
// The CommandBar handles item keybindings; skip them while collapsed so a chord can't hit the hidden selection.
if (((ShellPage)sender).ItemActionsAllowed)
{
TryCommandKeybindingMessage msg = new(modifiers.Ctrl, modifiers.Alt, modifiers.Shift, modifiers.Win, e.Key);
WeakReferenceMessenger.Default.Send(msg);
e.Handled = msg.Handled;
}
// The CommandBar is responsible for handling all the item keybindings,
// since the bound context item may need to then show another
// context menu
TryCommandKeybindingMessage msg = new(modifiers.Ctrl, modifiers.Alt, modifiers.Shift, modifiers.Win, e.Key);
WeakReferenceMessenger.Default.Send(msg);
e.Handled = msg.Handled;
break;
}
}
}
private void ShellPage_OnKeyDown(object sender, KeyRoutedEventArgs e)
private static void ShellPage_OnKeyDown(object sender, KeyRoutedEventArgs e)
{
if (ItemActionsAllowed && TryHandleItemAction(e))
var mods = KeyModifiers.GetCurrent();
if (mods.Ctrl && e.Key == VirtualKey.Enter)
{
return;
// ctrl+enter
WeakReferenceMessenger.Default.Send<ActivateSecondaryCommandMessage>();
e.Handled = true;
}
if (e.Key == VirtualKey.Escape)
else if (e.Key == VirtualKey.Enter)
{
WeakReferenceMessenger.Default.Send<ActivateSelectedListItemMessage>();
e.Handled = true;
}
else if (mods.Ctrl && e.Key == VirtualKey.K)
{
// ctrl+k
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(new OpenContextMenuMessage(null, null, null, ContextMenuFilterLocation.Bottom));
e.Handled = true;
}
else if (e.Key == VirtualKey.Escape)
{
WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
e.Handled = true;
}
}
private static bool TryHandleItemAction(KeyRoutedEventArgs e)
{
var mods = KeyModifiers.GetCurrent();
switch (e.Key)
{
// Ctrl+Enter
case VirtualKey.Enter when mods.OnlyCtrl:
WeakReferenceMessenger.Default.Send<ActivateSecondaryCommandMessage>();
break;
// Enter
case VirtualKey.Enter when mods.None:
WeakReferenceMessenger.Default.Send<ActivateSelectedListItemMessage>();
break;
// Ctrl+K
case VirtualKey.K when mods.OnlyCtrl:
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(new(null, null, null, ContextMenuFilterLocation.Bottom));
break;
default:
return false;
}
e.Handled = true;
return true;
}
private void ShellPage_OnPointerPressed(object sender, PointerRoutedEventArgs e)
{
try
@@ -1085,23 +1055,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
PropertyChanged?.Invoke(this, new(nameof(ExpandedMode)));
}
/// <summary>
/// Expands a collapsed compact palette on demand (via Down/Tab) so the user can browse the
/// top-level items. Returns <see langword="false"/> and does nothing unless compact mode is
/// on and the palette is currently collapsed, letting the caller keep the key's normal
/// meaning (list navigation / focus traversal) in every other case.
/// </summary>
private bool TryExpandCollapsedCompact()
{
if (!_compactMode || ExpandedMode)
{
return false;
}
HandleExpandCompactOnUiThread(true);
return true;
}
/// <summary>
/// Forces the shell into its compact (collapsed) layout and flushes layout so the host can
/// read the resulting card height. Only has an effect when compact mode is enabled.

View File

@@ -13,8 +13,8 @@ namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests;
[TestClass]
public class AvailableResultsListTests
{
private CultureInfo originalCulture = null!;
private CultureInfo originalUiCulture = null!;
private CultureInfo originalCulture;
private CultureInfo originalUiCulture;
[TestInitialize]
public void Setup()

View File

@@ -12,8 +12,8 @@ namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests;
[TestClass]
public class FallbackTimeDateItemTests
{
private CultureInfo originalCulture = null!;
private CultureInfo originalUiCulture = null!;
private CultureInfo originalCulture;
private CultureInfo originalUiCulture;
[TestInitialize]
public void Setup()

View File

@@ -5,7 +5,6 @@
<PropertyGroup>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<Nullable>enable</Nullable>
<RootNamespace>Microsoft.CmdPal.Ext.TimeDate.UnitTests</RootNamespace>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>

View File

@@ -1,160 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using Microsoft.CmdPal.Ext.TimeDate;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests;
[TestClass]
[System.Diagnostics.CodeAnalysis.SuppressMessage("IDisposable", "CA1001:Types that own disposable fields should be disposable", Justification = "Disposed in TestCleanup")]
public class NowDockBandTests
{
private static readonly DateTime FixedTime = new DateTime(2025, 7, 1, 14, 5, 32);
private CultureInfo _originalCulture = null!;
private CultureInfo _originalUiCulture = null!;
private NowDockBand? _band;
[TestInitialize]
public void Setup()
{
_originalCulture = CultureInfo.CurrentCulture;
_originalUiCulture = CultureInfo.CurrentUICulture;
CultureInfo.CurrentCulture = new CultureInfo("en-US", false);
CultureInfo.CurrentUICulture = new CultureInfo("en-US", false);
}
[TestCleanup]
public void Cleanup()
{
_band?.Dispose();
_band = null;
CultureInfo.CurrentCulture = _originalCulture;
CultureInfo.CurrentUICulture = _originalUiCulture;
}
[TestMethod]
public void Constructor_TitleIsSetImmediately()
{
_band = new NowDockBand(clock: () => FixedTime);
Assert.AreEqual("2:05 PM", _band.Title);
Assert.IsFalse(string.IsNullOrEmpty(_band.Subtitle));
}
[TestMethod]
public void UpdateText_LongTimeFormat_TitleContainsSeconds()
{
_band = new NowDockBand(timeWithSeconds: true, clock: () => FixedTime);
_band.UpdateText();
Assert.AreEqual("2:05:32 PM", _band.Title);
}
[TestMethod]
public void UpdateText_ShortDateFormat_SubtitleIsShortDate()
{
_band = new NowDockBand(clock: () => FixedTime);
_band.UpdateText();
Assert.AreEqual("7/1/2025", _band.Subtitle);
}
[TestMethod]
public void UpdateText_FiresOnUpdatedCallback()
{
var callbackFired = false;
_band = new NowDockBand(onUpdated: () => callbackFired = true, clock: () => FixedTime);
callbackFired = false; // reset — constructor already fired it once during synchronous UpdateText()
_band.UpdateText();
Assert.IsTrue(callbackFired);
}
[TestMethod]
public void UpdateText_CallbackFiredAfterAssignments()
{
var titleAtCallback = string.Empty;
_band = new NowDockBand(
onUpdated: () => titleAtCallback = _band?.Title ?? string.Empty,
clock: () => FixedTime);
titleAtCallback = string.Empty; // reset after construction callback
_band.UpdateText();
Assert.IsFalse(string.IsNullOrEmpty(titleAtCallback), "Title should be assigned before callback fires");
}
[TestMethod]
public void UpdateText_CopyCommandsUpdated()
{
_band = new NowDockBand(clock: () => FixedTime);
_band.UpdateText();
Assert.AreEqual(_band.Title, _band.CopyTimeCommand.Text);
Assert.AreEqual(_band.Subtitle, _band.CopyDateCommand.Text);
}
[DataTestMethod]
[DataRow("de-DE")]
[DataRow("fr-FR")]
[DataRow("ar-SA")]
public void UpdateText_CultureSmoke_TitleNonEmpty(string cultureName)
{
// Culture MUST be set before construction — constructor calls UpdateText() synchronously
CultureInfo.CurrentCulture = new CultureInfo(cultureName, false);
CultureInfo.CurrentUICulture = new CultureInfo(cultureName, false);
_band = new NowDockBand(clock: () => FixedTime);
Assert.IsFalse(string.IsNullOrEmpty(_band.Title), $"Title should be non-empty for culture '{cultureName}'");
Assert.IsFalse(string.IsNullOrEmpty(_band.Subtitle), $"Subtitle should be non-empty for culture '{cultureName}'");
}
[TestMethod]
public void UpdateSettings_EnablingSeconds_TitleIncludesSeconds()
{
_band = new NowDockBand(timeWithSeconds: false, clock: () => FixedTime);
Assert.AreEqual("2:05 PM", _band.Title, "Precondition: seconds hidden by default");
_band.UpdateSettings(timeWithSeconds: true);
Assert.AreEqual("2:05:32 PM", _band.Title, "Title should update live to include seconds");
}
[TestMethod]
public void UpdateSettings_DisablingSeconds_TitleDropsSeconds()
{
_band = new NowDockBand(timeWithSeconds: true, clock: () => FixedTime);
Assert.AreEqual("2:05:32 PM", _band.Title, "Precondition: seconds shown");
_band.UpdateSettings(timeWithSeconds: false);
Assert.AreEqual("2:05 PM", _band.Title, "Title should update live to drop seconds");
}
[TestMethod]
public void UpdateSettings_NoChange_FiresNoCallback()
{
var callbackCount = 0;
_band = new NowDockBand(timeWithSeconds: false, onUpdated: () => callbackCount++, clock: () => FixedTime);
callbackCount = 0; // reset after construction callback
_band.UpdateSettings(timeWithSeconds: false);
Assert.AreEqual(0, callbackCount, "A no-op settings change should not refresh the band");
}
}

View File

@@ -15,8 +15,8 @@ namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests;
[TestClass]
public class QueryTests : CommandPaletteUnitTestBase
{
private CultureInfo originalCulture = null!;
private CultureInfo originalUiCulture = null!;
private CultureInfo originalCulture;
private CultureInfo originalUiCulture;
[TestInitialize]
public void Setup()
@@ -166,7 +166,7 @@ public class QueryTests : CommandPaletteUnitTestBase
Assert.IsTrue(results.Length > 0, $"Query '{query}' should return at least one result");
var firstItem = results.FirstOrDefault();
Assert.IsTrue(firstItem!.Title.StartsWith("Error: Invalid input", StringComparison.CurrentCulture), $"Query '{query}' should return an error result for invalid input");
Assert.IsTrue(firstItem.Title.StartsWith("Error: Invalid input", StringComparison.CurrentCulture), $"Query '{query}' should return an error result for invalid input");
}
[DataTestMethod]
@@ -201,7 +201,7 @@ public class QueryTests : CommandPaletteUnitTestBase
// Assert
Assert.IsNotNull(results);
var firstResult = results.FirstOrDefault();
Assert.IsTrue(firstResult!.Subtitle.StartsWith(expectedSubtitle, StringComparison.CurrentCulture), $"Could not find result with subtitle starting with '{expectedSubtitle}' for query '{query}'");
Assert.IsTrue(firstResult.Subtitle.StartsWith(expectedSubtitle, StringComparison.CurrentCulture), $"Could not find result with subtitle starting with '{expectedSubtitle}' for query '{query}'");
}
[DataTestMethod]
@@ -218,7 +218,7 @@ public class QueryTests : CommandPaletteUnitTestBase
// Assert
Assert.IsNotNull(resultsList);
var firstResult = resultsList.FirstOrDefault();
Assert.IsTrue(firstResult!.Title.Contains(expectedResult, StringComparison.CurrentCulture), $"Delimiter query '{query}' result not match {expectedResult} current result {firstResult.Title}");
Assert.IsTrue(firstResult.Title.Contains(expectedResult, StringComparison.CurrentCulture), $"Delimiter query '{query}' result not match {expectedResult} current result {firstResult.Title}");
}
[TestMethod]

View File

@@ -12,8 +12,8 @@ namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests;
[TestClass]
public class ResultHelperTests
{
private CultureInfo originalCulture = null!;
private CultureInfo originalUiCulture = null!;
private CultureInfo originalCulture;
private CultureInfo originalUiCulture;
[TestInitialize]
public void Setup()
@@ -41,8 +41,6 @@ public class ResultHelperTests
{
Label = "Test Label",
Value = "Test Value",
AlternativeSearchTag = string.Empty,
IconType = ResultIconType.Time,
};
// Act
@@ -57,10 +55,10 @@ public class ResultHelperTests
[TestMethod]
public void ResultHelper_CreateListItem_HandlesNullInput()
{
AvailableResult? availableResult = null;
AvailableResult availableResult = null;
// Act & Assert
Assert.ThrowsException<System.NullReferenceException>(() => availableResult!.ToListItem());
Assert.ThrowsException<System.NullReferenceException>(() => availableResult.ToListItem());
}
[TestMethod]
@@ -71,8 +69,6 @@ public class ResultHelperTests
{
Label = string.Empty,
Value = string.Empty,
AlternativeSearchTag = string.Empty,
IconType = ResultIconType.Time,
};
// Act
@@ -92,7 +88,6 @@ public class ResultHelperTests
{
Label = "Test Label",
Value = "Test Value",
AlternativeSearchTag = string.Empty,
IconType = ResultIconType.Date,
};
@@ -115,8 +110,6 @@ public class ResultHelperTests
{
Label = longText,
Value = longText,
AlternativeSearchTag = string.Empty,
IconType = ResultIconType.Time,
};
// Act
@@ -137,8 +130,6 @@ public class ResultHelperTests
{
Label = specialText,
Value = specialText,
AlternativeSearchTag = string.Empty,
IconType = ResultIconType.Time,
};
// Act

View File

@@ -13,7 +13,6 @@ public class Settings : ISettingsInterface
private readonly int firstDayOfWeek;
private readonly bool enableFallbackItems;
private readonly bool timeWithSecond;
private readonly bool dockClockWithSecond;
private readonly bool dateWithWeekday;
private readonly List<string> customFormats;
@@ -22,15 +21,13 @@ public class Settings : ISettingsInterface
int firstDayOfWeek = -1,
bool enableFallbackItems = true,
bool timeWithSecond = false,
bool dockClockWithSecond = false,
bool dateWithWeekday = false,
List<string>? customFormats = null)
List<string> customFormats = null)
{
this.firstWeekOfYear = firstWeekOfYear;
this.firstDayOfWeek = firstDayOfWeek;
this.enableFallbackItems = enableFallbackItems;
this.timeWithSecond = timeWithSecond;
this.dockClockWithSecond = dockClockWithSecond;
this.dateWithWeekday = dateWithWeekday;
this.customFormats = customFormats ?? new List<string>();
}
@@ -43,8 +40,6 @@ public class Settings : ISettingsInterface
public bool TimeWithSecond => timeWithSecond;
public bool DockClockWithSecond => dockClockWithSecond;
public bool DateWithWeekday => dateWithWeekday;
public List<string> CustomFormats => customFormats;

View File

@@ -12,8 +12,8 @@ namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests;
[TestClass]
public class StringParserTests
{
private CultureInfo originalCulture = null!;
private CultureInfo originalUiCulture = null!;
private CultureInfo originalCulture;
private CultureInfo originalUiCulture;
[TestInitialize]
public void Setup()

View File

@@ -12,8 +12,8 @@ namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests;
[TestClass]
public class TimeAndDateHelperTests
{
private CultureInfo originalCulture = null!;
private CultureInfo originalUiCulture = null!;
private CultureInfo originalCulture;
private CultureInfo originalUiCulture;
[TestInitialize]
public void Setup()

View File

@@ -15,8 +15,8 @@ namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests;
[TestClass]
public class TimeDateCalculatorTests
{
private CultureInfo originalCulture = null!;
private CultureInfo originalUiCulture = null!;
private CultureInfo originalCulture;
private CultureInfo originalUiCulture;
[TestInitialize]
public void Setup()
@@ -69,7 +69,7 @@ public class TimeDateCalculatorTests
var settings = new SettingsManager();
// Act
var results = TimeDateCalculator.ExecuteSearch(settings, null!);
var results = TimeDateCalculator.ExecuteSearch(settings, null);
// Assert
Assert.IsNotNull(results);

View File

@@ -11,8 +11,8 @@ namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests
[TestClass]
public class TimeDateCommandsProviderTests
{
private CultureInfo originalCulture = null!;
private CultureInfo originalUiCulture = null!;
private CultureInfo originalCulture;
private CultureInfo originalUiCulture;
[TestInitialize]
public void Setup()
@@ -90,27 +90,5 @@ namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests
// Assert
Assert.IsFalse(string.IsNullOrEmpty(displayName));
}
[TestMethod]
public void GetDockBands_ReturnsNonEmptyArray()
{
var provider = new TimeDateCommandsProvider();
var bands = provider.GetDockBands();
Assert.IsTrue(bands.Length > 0, "GetDockBands should return at least one item");
Assert.IsNotNull(bands[0], "First dock band should not be null");
}
[TestMethod]
public void GetDockBands_NotificationCenterBandDoesNotSetDockIcon()
{
var provider = new TimeDateCommandsProvider();
var bands = provider.GetDockBands();
Assert.IsTrue(bands.Length > 1, "Expected notification center band to be present");
Assert.IsNull(bands[1].Icon, "Notification center band should not set a dock icon");
}
}
}

View File

@@ -29,16 +29,16 @@ internal sealed partial class FallbackTimeDateItem : FallbackCommandItem
_validOptions = new(StringComparer.OrdinalIgnoreCase)
{
Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagDate", CultureInfo.CurrentCulture)!,
Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagDateNow", CultureInfo.CurrentCulture)!,
Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagDate", CultureInfo.CurrentCulture),
Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagDateNow", CultureInfo.CurrentCulture),
Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagTime", CultureInfo.CurrentCulture)!,
Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagTimeNow", CultureInfo.CurrentCulture)!,
Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagTime", CultureInfo.CurrentCulture),
Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagTimeNow", CultureInfo.CurrentCulture),
Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagFormat", CultureInfo.CurrentCulture)!,
Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagFormatNow", CultureInfo.CurrentCulture)!,
Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagFormat", CultureInfo.CurrentCulture),
Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagFormatNow", CultureInfo.CurrentCulture),
Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagWeek", CultureInfo.CurrentCulture)!,
Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagWeek", CultureInfo.CurrentCulture),
};
}
@@ -53,7 +53,7 @@ internal sealed partial class FallbackTimeDateItem : FallbackCommandItem
}
var availableResults = AvailableResultsList.GetList(false, _settingsManager, timestamp: _timestamp);
ListItem? result = null;
ListItem result = null;
var maxScore = 0;
foreach (var f in availableResults)

View File

@@ -10,22 +10,22 @@ internal sealed class AvailableResult
/// <summary>
/// Gets or sets the time/date value
/// </summary>
internal required string Value { get; set; }
internal string Value { get; set; }
/// <summary>
/// Gets or sets the text used for the subtitle and as search term
/// </summary>
internal required string Label { get; set; }
internal string Label { get; set; }
/// <summary>
/// Gets or sets an alternative search tag that will be evaluated if label doesn't match. For example we like to show the era on searches for 'year' too.
/// </summary>
internal required string AlternativeSearchTag { get; set; }
internal string AlternativeSearchTag { get; set; }
/// <summary>
/// Gets or sets a value indicating the type of result
/// </summary>
internal required ResultIconType IconType { get; set; }
internal ResultIconType IconType { get; set; }
/// <summary>
/// Gets or sets a value to show additional error details
@@ -37,7 +37,7 @@ internal sealed class AvailableResult
/// </summary>
/// <param name="theme">Theme</param>
/// <returns>Path</returns>
public IconInfo? GetIconInfo()
public IconInfo GetIconInfo()
{
return IconType switch
{

View File

@@ -20,8 +20,6 @@ public interface ISettingsInterface
public bool TimeWithSecond { get; }
public bool DockClockWithSecond { get; }
public bool DateWithWeekday { get; }
public List<string> CustomFormats { get; }

View File

@@ -17,7 +17,7 @@ internal static class ResultHelper
/// <param name="stringId">Id of the string. (Example: `MyString` for `MyString` and `MyStringNow`)</param>
/// <param name="stringIdNow">Optional string id for now case</param>
/// <returns>The string from the resource file, or <see cref="string.Empty"/> otherwise.</returns>
internal static string SelectStringFromResources(bool isSystemTimeDate, string stringId, string? stringIdNow = default)
internal static string SelectStringFromResources(bool isSystemTimeDate, string stringId, string stringIdNow = default)
{
return !isSystemTimeDate
? Resources.ResourceManager.GetString(stringId, CultureInfo.CurrentUICulture) ?? string.Empty

View File

@@ -87,12 +87,6 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
Resources.Microsoft_plugin_timedate_SettingTimeWithSeconds_Description,
false); // TODO -- double check default value
private readonly ToggleSetting _dockClockWithSeconds = new(
Namespaced(nameof(DockClockWithSecond)),
Resources.Microsoft_plugin_timedate_SettingDockClockWithSeconds,
Resources.Microsoft_plugin_timedate_SettingDockClockWithSeconds_Description,
false);
private readonly ToggleSetting _dateWithWeekday = new(
Namespaced(nameof(DateWithWeekday)),
Resources.Microsoft_plugin_timedate_SettingDateWithWeekday,
@@ -149,11 +143,9 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
public bool TimeWithSecond => _timeWithSeconds.Value;
public bool DockClockWithSecond => _dockClockWithSeconds.Value;
public bool DateWithWeekday => _dateWithWeekday.Value;
public List<string> CustomFormats => (_customFormats.Value ?? string.Empty).Split(TEXTBOXNEWLINE).ToList();
public List<string> CustomFormats => _customFormats.Value.Split(TEXTBOXNEWLINE).ToList();
internal static string SettingsJsonPath()
{
@@ -169,7 +161,6 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
Settings.Add(_enableFallbackItems);
Settings.Add(_timeWithSeconds);
Settings.Add(_dockClockWithSeconds);
Settings.Add(_dateWithWeekday);
Settings.Add(_firstWeekOfYear);
Settings.Add(_firstDayOfWeek);

View File

@@ -5,7 +5,6 @@
<PropertyGroup>
<RootNamespace>Microsoft.CmdPal.Ext.TimeDate</RootNamespace>
<Nullable>enable</Nullable>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>

View File

@@ -1,134 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using Microsoft.CmdPal.Ext.TimeDate.Helpers;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.TimeDate;
internal sealed partial class NowDockBand : ListItem, IDisposable
{
private static readonly TimeSpan PerSecondUpdateInterval = TimeSpan.FromSeconds(1);
private static readonly TimeSpan PerMinuteUpdateInterval = TimeSpan.FromMinutes(1);
private readonly System.Timers.Timer _timer;
private readonly Action? _onUpdated;
private readonly Func<DateTime> _clock;
private bool _timeWithSeconds;
private CopyTextCommand _copyTimeCommand;
private CopyTextCommand _copyDateCommand;
internal CopyTextCommand CopyTimeCommand => _copyTimeCommand;
internal CopyTextCommand CopyDateCommand => _copyDateCommand;
internal NowDockBand(bool timeWithSeconds = false, Action? onUpdated = null, Func<DateTime>? clock = null)
{
_timeWithSeconds = timeWithSeconds;
_onUpdated = onUpdated;
_clock = clock ?? (() => DateTime.Now);
Command = new OpenUrlCommand("ms-actioncenter:")
{
Id = "com.microsoft.cmdpal.timedate.dockBand",
Name = Resources.timedate_show_notification_center_command_name,
Result = CommandResult.Dismiss(),
};
_copyTimeCommand = new CopyTextCommand(string.Empty) { Name = Resources.timedate_copy_time_command_name };
_copyDateCommand = new CopyTextCommand(string.Empty) { Name = Resources.timedate_copy_date_command_name };
MoreCommands =
[
new CommandContextItem(_copyTimeCommand),
new CommandContextItem(_copyDateCommand),
];
UpdateText();
_timer = new System.Timers.Timer() { AutoReset = true };
ConfigureTimer();
}
// Reads the current "show seconds" preference and, if it changed, reconfigures
// the timer cadence and refreshes the displayed text. Safe to call at any time
// (e.g. from a settings-changed handler) so the dock clock stays in sync without
// requiring the app to restart.
internal void UpdateSettings(bool timeWithSeconds)
{
if (_timeWithSeconds == timeWithSeconds)
{
return;
}
_timeWithSeconds = timeWithSeconds;
ConfigureTimer();
UpdateText();
}
private void ConfigureTimer()
{
_timer.Stop();
_timer.Elapsed -= Timer_Elapsed;
_timer.Elapsed -= Timer_ElapsedFirstMinuteTick;
if (_timeWithSeconds)
{
_timer.Interval = PerSecondUpdateInterval.TotalMilliseconds;
_timer.Elapsed += Timer_Elapsed;
}
else
{
// Align the first tick to the next minute boundary so the clock flips
// exactly when the system clock does, then fall back to a per-minute cadence.
var now = _clock();
_timer.Interval = PerMinuteUpdateInterval.TotalMilliseconds - ((now.Second * 1000) + now.Millisecond);
_timer.Elapsed += Timer_ElapsedFirstMinuteTick;
}
_timer.Start();
}
private void Timer_ElapsedFirstMinuteTick(object? sender, System.Timers.ElapsedEventArgs e)
{
if (sender is System.Timers.Timer timer)
{
timer.Interval = PerMinuteUpdateInterval.TotalMilliseconds;
timer.Elapsed -= Timer_ElapsedFirstMinuteTick;
timer.Elapsed += Timer_Elapsed;
}
Timer_Elapsed(sender, e);
}
private void Timer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e)
{
UpdateText();
}
internal void UpdateText()
{
var now = _clock();
var timeString = now.ToString(
TimeAndDateHelper.GetStringFormat(FormatStringType.Time, _timeWithSeconds, false),
CultureInfo.CurrentCulture);
var dateString = now.ToString(
TimeAndDateHelper.GetStringFormat(FormatStringType.Date, false, false),
CultureInfo.CurrentCulture);
Title = timeString;
Subtitle = dateString;
_copyTimeCommand.Text = timeString;
_copyDateCommand.Text = dateString;
_onUpdated?.Invoke(); // Must remain last — ViewModel reads Title/Subtitle via GetItems() on callback
}
public void Dispose()
{
_timer.Stop();
_timer.Dispose();
}
}

View File

@@ -834,24 +834,6 @@ namespace Microsoft.CmdPal.Ext.TimeDate {
}
}
/// <summary>
/// Looks up a localized string similar to Show seconds on the dock clock.
/// </summary>
public static string Microsoft_plugin_timedate_SettingDockClockWithSeconds {
get {
return ResourceManager.GetString("Microsoft_plugin_timedate_SettingDockClockWithSeconds", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to This setting applies to the clock shown on the Command Palette dock..
/// </summary>
public static string Microsoft_plugin_timedate_SettingDockClockWithSeconds_Description {
get {
return ResourceManager.GetString("Microsoft_plugin_timedate_SettingDockClockWithSeconds_Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Select for more details..
/// </summary>

View File

@@ -267,12 +267,6 @@
<data name="Microsoft_plugin_timedate_SettingTimeWithSeconds_Description" xml:space="preserve">
<value>This setting applies to the 'Time' and 'Now' result.</value>
</data>
<data name="Microsoft_plugin_timedate_SettingDockClockWithSeconds" xml:space="preserve">
<value>Show seconds on the dock clock</value>
</data>
<data name="Microsoft_plugin_timedate_SettingDockClockWithSeconds_Description" xml:space="preserve">
<value>This setting applies to the clock shown on the Command Palette dock.</value>
</data>
<data name="Microsoft_plugin_timedate_SubTitleNote" xml:space="preserve">
<value>Select or press Ctrl+C to copy</value>
<comment>'Ctrl+C' is a shortcut</comment>

View File

@@ -20,11 +20,8 @@ public sealed partial class TimeDateCommandsProvider : CommandProvider
private static readonly TimeDateExtensionPage _timeDateExtensionPage = new(_settingsManager);
private readonly FallbackTimeDateItem _fallbackTimeDateItem = new(_settingsManager);
private readonly WrappedDockItem _bandItem;
private readonly WrappedDockItem _notificationCenterBandItem;
// Keep a reference to the band so we can dispose it when the provider is disposed.
private NowDockBand? _nowDockBand;
private readonly ListItem _bandItem;
private readonly ListItem _notificationCenterBandItem;
public TimeDateCommandsProvider()
{
@@ -40,40 +37,8 @@ public sealed partial class TimeDateCommandsProvider : CommandProvider
Icon = _timeDateExtensionPage.Icon;
Settings = _settingsManager.Settings;
WrappedDockItem? wrappedBand = null;
// During NowDockBand construction, UpdateText() runs synchronously.
// At that point wrappedBand is still null so the callback is a no-op.
// On subsequent timer ticks, wrappedBand is non-null and SetItems fires
// RaiseItemsChanged - the framework marshals to the UI thread in
// DockBandViewModel.InitializeFromList via DoOnUiThread.
_nowDockBand = new NowDockBand(_settingsManager.DockClockWithSecond, onUpdated: () =>
{
if (wrappedBand is not null)
{
wrappedBand.Items = [_nowDockBand!];
}
});
// Re-read the dock clock preference whenever settings change so the band updates
// live (no app restart required). The band ignores no-op changes internally.
_settingsManager.Settings.SettingsChanged += OnSettingsChanged;
wrappedBand = new WrappedDockItem(
[_nowDockBand],
"com.microsoft.cmdpal.timedate.dockBand",
Resources.Microsoft_plugin_timedate_dock_band_title)
{
Icon = Icons.TimeDateExtIcon,
};
_bandItem = wrappedBand;
var notificationCenterBand = new NotificationCenterDockBand();
_notificationCenterBandItem = new WrappedDockItem(
[notificationCenterBand],
"com.microsoft.cmdpal.timedate.notificationCenterBand",
Resources.timedate_notification_center_band_title);
_bandItem = new NowDockBand();
_notificationCenterBandItem = new NotificationCenterDockBand();
}
private string GetTranslatedPluginDescription()
@@ -91,26 +56,97 @@ public sealed partial class TimeDateCommandsProvider : CommandProvider
public override ICommandItem[] GetDockBands()
{
return [_bandItem, _notificationCenterBandItem];
}
var clockBand = new WrappedDockItem(
[_bandItem],
"com.microsoft.cmdpal.timedate.dockBand",
Resources.Microsoft_plugin_timedate_dock_band_title)
{
Icon = Icons.TimeDateExtIcon,
};
private void OnSettingsChanged(object sender, Settings args)
{
_nowDockBand?.UpdateSettings(_settingsManager.DockClockWithSecond);
}
var notificationBand = new WrappedDockItem(
[_notificationCenterBandItem],
"com.microsoft.cmdpal.timedate.notificationCenterBand",
Resources.timedate_notification_center_band_title)
{
Icon = Icons.NotificationCenterIcon,
};
public override void Dispose()
{
_settingsManager.Settings.SettingsChanged -= OnSettingsChanged;
_nowDockBand?.Dispose();
_nowDockBand = null;
GC.SuppressFinalize(this);
base.Dispose();
return new ICommandItem[] { clockBand, notificationBand };
}
}
#pragma warning disable SA1402 // File may only contain a single type
internal sealed partial class NowDockBand : ListItem
{
private static readonly TimeSpan UpdateInterval = TimeSpan.FromSeconds(60);
private CopyTextCommand _copyTimeCommand;
private CopyTextCommand _copyDateCommand;
public NowDockBand()
{
// Open Notification Center on click
Command = new OpenUrlCommand("ms-actioncenter:") { Id = "com.microsoft.cmdpal.timedate.dockBand", Name = Resources.timedate_show_notification_center_command_name, Result = CommandResult.Dismiss(), Icon = null };
_copyTimeCommand = new CopyTextCommand(string.Empty) { Name = Resources.timedate_copy_time_command_name };
_copyDateCommand = new CopyTextCommand(string.Empty) { Name = Resources.timedate_copy_date_command_name };
MoreCommands = [
new CommandContextItem(_copyTimeCommand),
new CommandContextItem(_copyDateCommand)
];
UpdateText();
// Create a timer to update the time every minute
System.Timers.Timer timer = new(UpdateInterval.TotalMilliseconds);
// but we want it to tick on the minute, so calculate the initial delay
var now = DateTime.Now;
timer.Interval = UpdateInterval.TotalMilliseconds - ((now.Second * 1000) + now.Millisecond);
// then after the first tick, set it to 60 seconds
timer.Elapsed += Timer_ElapsedFirst;
timer.Start();
}
private void Timer_ElapsedFirst(object sender, System.Timers.ElapsedEventArgs e)
{
// After the first tick, set the interval to 60 seconds
if (sender is System.Timers.Timer timer)
{
timer.Interval = UpdateInterval.TotalMilliseconds;
timer.Elapsed -= Timer_ElapsedFirst;
timer.Elapsed += Timer_Elapsed;
// Still call the callback, so that we update the clock
Timer_Elapsed(sender, e);
}
}
private void Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
UpdateText();
}
private void UpdateText()
{
var timeExtended = false; // timeLongFormat ?? settings.TimeWithSecond;
var dateExtended = false; // dateLongFormat ?? settings.DateWithWeekday;
var dateTimeNow = DateTime.Now;
var timeString = dateTimeNow.ToString(
TimeAndDateHelper.GetStringFormat(FormatStringType.Time, timeExtended, dateExtended),
CultureInfo.CurrentCulture);
var dateString = dateTimeNow.ToString(
TimeAndDateHelper.GetStringFormat(FormatStringType.Date, timeExtended, dateExtended),
CultureInfo.CurrentCulture);
Title = timeString;
Subtitle = dateString;
_copyDateCommand.Text = dateString;
_copyTimeCommand.Text = timeString;
}
}
internal sealed partial class NotificationCenterDockBand : ListItem
{
public NotificationCenterDockBand()

View File

@@ -119,7 +119,7 @@ private:
SHELLEXECUTEINFOW sei{ sizeof(sei) };
sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI };
sei.lpFile = L"PowerToys.ColorPickerUI.exe";
sei.lpFile = L"WinUI3Apps\\PowerToys.ColorPickerUI.exe";
sei.nShow = SW_SHOWNORMAL;
sei.lpParameters = executable_args.data();
if (ShellExecuteExW(&sei))

View File

@@ -19,6 +19,7 @@
<ItemGroup>
<PackageReference Include="MSTest" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Win32.SystemEvents">
<!-- This package is a dependency of System.Drawing.Common, but we need to set it here so we can exclude the assets, so it doesn't conflict with the 8.0.1 dll coming from .NET SDK. -->
<ExcludeAssets>runtime</ExcludeAssets> <!-- Should already be present on .net sdk runtime, so we avoid the conflicting runtime version from nuget -->
@@ -35,7 +36,7 @@
<ItemGroup>
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
<ProjectReference Include="..\ColorPickerUI\ColorPickerUI.csproj" />
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ColorPicker.UnitTests.Foundation
{
[TestClass]
public class FrozenContractTests
{
[TestMethod]
public void Show_event_name_is_frozen()
{
Assert.AreEqual(
@"Local\ShowColorPickerEvent-8c46be2a-3e05-4186-b56b-4ae986ef2525",
PowerToys.Interop.Constants.ShowColorPickerSharedEvent());
}
[TestMethod]
public void Terminate_event_name_is_frozen()
{
Assert.AreEqual(
@"Local\TerminateColorPickerEvent-3d676258-c4d5-424e-a87a-4be22020e813",
PowerToys.Interop.Constants.TerminateColorPickerSharedEvent());
}
[TestMethod]
public void SendSettingsTelemetry_event_name_is_frozen()
{
Assert.AreEqual(
@"Local\ColorPickerSettingsTelemetryEvent-6c7071d8-4014-46ec-b687-913bd8a422f1",
PowerToys.Interop.Constants.ColorPickerSendSettingsTelemetryEvent());
}
}
}

View File

@@ -0,0 +1,34 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Specialized;
using ColorPicker.Common;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ColorPicker.UnitTests.Foundation
{
[TestClass]
public class RangeObservableCollectionTests
{
private static readonly int[] _inputItems = new[] { 1, 2, 3 };
private static readonly int[] _expectedItems = new[] { 1, 2, 3 };
[TestMethod]
public void AddRange_adds_all_items_and_raises_a_single_reset()
{
var sut = new RangeObservableCollection<int>();
int notifications = 0;
sut.CollectionChanged += (_, e) =>
{
notifications++;
Assert.AreEqual(NotifyCollectionChangedAction.Reset, e.Action);
};
sut.AddRange(_inputItems);
CollectionAssert.AreEqual(_expectedItems, sut);
Assert.AreEqual(1, notifications);
}
}
}

View File

@@ -0,0 +1,50 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using ColorPicker.Foundation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ColorPicker.UnitTests.Foundation
{
[TestClass]
public class ServiceProviderSmokeTests
{
[TestMethod]
public void Container_resolves_the_exit_token_source_as_a_singleton()
{
var provider = AppServices.Configure();
var a = provider.GetRequiredService<System.Threading.CancellationTokenSource>();
var b = provider.GetRequiredService<System.Threading.CancellationTokenSource>();
Assert.IsNotNull(a);
Assert.AreSame(a, b);
}
[TestMethod]
public void Container_resolves_the_exit_token_as_the_sources_token()
{
var provider = AppServices.Configure();
var source = provider.GetRequiredService<System.Threading.CancellationTokenSource>();
var token = provider.GetRequiredService<System.Threading.CancellationToken>();
// CancellationToken is a struct; equality compares the underlying source.
Assert.AreEqual(source.Token, token);
}
[TestMethod]
public void Resolved_token_observes_source_cancellation()
{
var provider = AppServices.Configure();
var source = provider.GetRequiredService<System.Threading.CancellationTokenSource>();
source.Cancel();
var token = provider.GetRequiredService<System.Threading.CancellationToken>();
Assert.IsTrue(token.IsCancellationRequested);
}
}
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using ColorPicker.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ColorPicker.UnitTests.Helpers
{
[TestClass]
public class ClipboardHelperTests
{
[TestMethod]
public void CopyToClipboard_null_is_a_no_op()
{
ClipboardHelper.CopyToClipboard(null); // must not throw, must not touch the clipboard
}
[TestMethod]
public void CopyToClipboard_empty_is_a_no_op()
{
ClipboardHelper.CopyToClipboard(string.Empty); // must not throw
}
}
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Linq;
using ColorPicker.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ColorPicker.UnitTests.Helpers
{
[TestClass]
public class MonitorResolutionHelperTests
{
[TestMethod]
public void AllMonitors_has_at_least_one_named_monitor()
{
Assert.IsTrue(MonitorResolutionHelper.AllMonitors.Any());
Assert.IsFalse(string.IsNullOrEmpty(MonitorResolutionHelper.AllMonitors.First().Name));
}
[TestMethod]
public void Exactly_one_primary_monitor()
{
Assert.AreEqual(1, MonitorResolutionHelper.AllMonitors.Count(m => m.IsPrimary));
}
[TestMethod]
public void Primary_bounds_is_a_positive_windows_foundation_rect()
{
var b = MonitorResolutionHelper.AllMonitors.First(m => m.IsPrimary).Bounds;
Assert.IsInstanceOfType(b, typeof(Windows.Foundation.Rect));
Assert.IsTrue(b.Width > 0 && b.Height > 0);
}
[TestMethod]
public void HasMultipleMonitors_matches_count()
{
Assert.AreEqual(MonitorResolutionHelper.AllMonitors.Count() > 1, MonitorResolutionHelper.HasMultipleMonitors());
}
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using ColorPicker.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ColorPicker.UnitTests.Helpers
{
[TestClass]
public class NativeEventWaiterTests
{
[TestMethod]
public void WaitForEventLoop_without_a_dispatcher_queue_is_a_safe_no_op()
{
// MSTest worker thread has no DispatcherQueue: the guard logs + returns,
// and signalling the event must NOT invoke the callback.
var eventName = @"Local\PT_CP_Test_" + Guid.NewGuid().ToString("N");
var fired = new ManualResetEventSlim(false);
using var cts = new CancellationTokenSource();
NativeEventWaiter.WaitForEventLoop(eventName, () => fired.Set(), cts.Token);
using var signal = new EventWaitHandle(false, EventResetMode.AutoReset, eventName);
signal.Set();
Assert.IsFalse(fired.Wait(300), "callback must not fire when there is no DispatcherQueue to marshal onto");
}
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using ColorPicker.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ColorPicker.UnitTests.Helpers
{
[TestClass]
public class NativeMethodsHelperTests
{
[TestMethod]
public void GetCursorPos_after_SetCursorPos_round_trips()
{
// A safe coordinate inside any primary monitor.
Assert.IsTrue(NativeMethodsHelper.SetCursorPos(100, 120));
Assert.IsTrue(NativeMethodsHelper.GetCursorPos(out NativeMethodsHelper.POINT p));
Assert.AreEqual(100, p.X);
Assert.AreEqual(120, p.Y);
}
[TestMethod]
public void POINT_implicit_operator_yields_Windows_Foundation_Point()
{
var p = new NativeMethodsHelper.POINT { X = 7, Y = 9 };
Windows.Foundation.Point wf = p; // exercises the retargeted implicit operator
Assert.AreEqual(7.0, wf.X);
Assert.AreEqual(9.0, wf.Y);
}
}
}

View File

@@ -0,0 +1,34 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using ColorPicker.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Enumerations;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ColorPicker.UnitTests.Helpers
{
[TestClass]
public class SessionEventHelperTests
{
[TestMethod]
public void Start_sets_Event_with_StartedAs()
{
SessionEventHelper.Start(ColorPickerActivationAction.OpenColorPicker);
Assert.IsNotNull(SessionEventHelper.Event);
Assert.AreEqual(ColorPickerActivationAction.OpenColorPicker.ToString(), SessionEventHelper.Event.StartedAs);
}
[TestMethod]
public void End_after_Start_finalizes_a_nonnegative_duration()
{
SessionEventHelper.Start(ColorPickerActivationAction.OpenColorPicker);
SessionEventHelper.End();
// End() does not clear Event; it finalizes Duration and writes the telemetry event.
Assert.IsNotNull(SessionEventHelper.Event);
Assert.IsTrue(SessionEventHelper.Event.Duration >= 0);
}
}
}

View File

@@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using ColorPicker.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ColorPicker.UnitTests.Helpers
{
[TestClass]
public class ThrottledActionInvokerTests
{
[TestMethod]
public void Ctor_throws_without_a_dispatcher_queue()
{
// MSTest runs on a worker thread that has no DispatcherQueue, so the
// ctor's GetForCurrentThread() guard must throw. This also proves the
// timer is bound at construction time (not lazily).
Assert.ThrowsException<InvalidOperationException>(() => new ThrottledActionInvoker());
}
}
}

View File

@@ -0,0 +1,52 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Text.Json;
using ColorPicker.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ColorPicker.UnitTests.Settings
{
[TestClass]
public class UserSettingsTests
{
private sealed class SyncThrottledActionInvoker : IThrottledActionInvoker
{
public void ScheduleAction(Action action, int milliseconds) => action();
}
[TestMethod]
public void Default_settings_version_is_2_1()
{
Assert.AreEqual("2.1", new ColorPickerSettings().Version);
}
[TestMethod]
public void Properties_serialize_with_lowercase_keys()
{
var json = JsonSerializer.Serialize(new ColorPickerProperties());
StringAssert.Contains(json, "\"showcolorname\"");
StringAssert.Contains(json, "\"copiedcolorrepresentation\"");
StringAssert.Contains(json, "\"changecursor\"");
}
[TestMethod]
public void Color_history_entry_is_four_pipe_parts()
{
// The frozen A|R|G|B contract written by MainViewModel.GetColorString and
// parsed by ColorPickerService.GetSavedColorsAsync (CmdPal).
Assert.AreEqual(4, "255|16|32|48".Split('|').Length);
}
[TestMethod]
public void UserSettings_constructs_with_a_synchronous_invoker()
{
// Smoke: ctor loads defaults + registers the file watcher without a DispatcherQueue.
var settings = new ColorPicker.Settings.UserSettings(new SyncThrottledActionInvoker());
Assert.IsNotNull(settings.CopiedColorRepresentation);
}
}
}

View File

@@ -1,121 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.ComponentModel.Composition;
using System.Globalization;
using System.Threading;
using System.Windows;
using ColorPicker.Mouse;
using ManagedCommon;
using Microsoft.PowerToys.Telemetry;
namespace ColorPickerUI
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application, IDisposable
{
public ETWTrace EtwTrace { get; private set; } = new ETWTrace();
private Mutex _instanceMutex;
private static string[] _args;
private int _powerToysRunnerPid;
private bool disposedValue;
private CancellationTokenSource NativeThreadCTS { get; set; }
[Export]
private static CancellationToken ExitToken { get; set; }
protected override void OnStartup(StartupEventArgs e)
{
try
{
string appLanguage = LanguageHelper.LoadLanguage();
if (!string.IsNullOrEmpty(appLanguage))
{
System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(appLanguage);
}
}
catch (CultureNotFoundException ex)
{
Logger.LogError("CultureNotFoundException: " + ex.Message);
}
NativeThreadCTS = new CancellationTokenSource();
ExitToken = NativeThreadCTS.Token;
_args = e?.Args;
// allow only one instance of color picker
_instanceMutex = new Mutex(true, @"Local\PowerToys_ColorPicker_InstanceMutex", out bool createdNew);
if (!createdNew)
{
Logger.LogWarning("There is ColorPicker instance running. Exiting Color Picker");
_instanceMutex = null;
Shutdown(0);
return;
}
if (_args?.Length > 0)
{
_ = int.TryParse(_args[0], out _powerToysRunnerPid);
Logger.LogInfo($"Color Picker started from the PowerToys Runner. Runner pid={_powerToysRunnerPid}");
RunnerHelper.WaitForPowerToysRunner(_powerToysRunnerPid, () =>
{
Logger.LogInfo("PowerToys Runner exited. Exiting ColorPicker");
NativeThreadCTS.Cancel();
Dispatcher.Invoke(Shutdown);
});
}
else
{
_powerToysRunnerPid = -1;
}
base.OnStartup(e);
}
protected override void OnExit(ExitEventArgs e)
{
if (_instanceMutex != null)
{
_instanceMutex.ReleaseMutex();
}
CursorManager.RestoreOriginalCursors();
base.OnExit(e);
}
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
_instanceMutex?.Dispose();
EtwTrace?.Dispose();
}
disposedValue = true;
}
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
public bool IsRunningDetachedFromPowerToys()
{
return _powerToysRunnerPid == -1;
}
}
}

View File

@@ -1,63 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Windows;
using System.Windows.Media.Animation;
using Microsoft.Xaml.Behaviors;
namespace ColorPicker.Behaviors
{
public class AppearAnimationBehavior : Behavior<Window>
{
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.Loaded += AssociatedObject_Loaded;
}
private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
{
AssociatedObject.IsVisibleChanged += AssociatedObject_IsVisibleChanged;
}
private void AssociatedObject_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (AssociatedObject.IsVisible)
{
Appear();
}
else
{
Hide();
}
}
private void Appear()
{
var duration = new Duration(TimeSpan.FromMilliseconds(250));
var opacityAppear = new DoubleAnimation(0d, 1d, duration)
{
EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseInOut },
};
AssociatedObject.BeginAnimation(UIElement.OpacityProperty, opacityAppear);
// force re-evaluation of tooltip size
AssociatedObject.SizeToContent = SizeToContent.Manual;
AssociatedObject.SizeToContent = SizeToContent.WidthAndHeight;
}
private void Hide()
{
var duration = new Duration(TimeSpan.FromMilliseconds(1));
var opacityAppear = new DoubleAnimation(0d, duration);
AssociatedObject.BeginAnimation(UIElement.OpacityProperty, opacityAppear);
}
}
}

View File

@@ -1,92 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Windows;
using ColorPicker.Helpers;
using ColorPicker.Mouse;
using ManagedCommon;
using Microsoft.Xaml.Behaviors;
namespace ColorPicker.Behaviors
{
public class ChangeWindowPositionBehavior : Behavior<Window>
{
// color window should not get into these zones, only mouse to avoid window getting outsize of monitor
private const int MonitorRightSideDeadZone = 285;
private const int MonitorBottomSideDeadZone = 90;
private const int WindowOffsetWhenInDeadZone = 10;
private const int YOffset = 10;
private const int XOffset = 5;
private Point _lastMousePosition;
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.Loaded += AssociatedObject_Loaded;
}
private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
{
var mouseInfoProvider = Bootstrapper.Container.GetExportedValue<IMouseInfoProvider>();
SetWindowPosition(mouseInfoProvider.CurrentPosition);
mouseInfoProvider.MousePositionChanged += (s, mousePosition) =>
{
SetWindowPosition(mousePosition);
};
AssociatedObject.IsVisibleChanged += AssociatedObject_IsVisibleChanged;
}
private void AssociatedObject_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if ((bool)e.NewValue)
{
SetWindowPosition(_lastMousePosition);
}
}
private void SetWindowPosition(Point mousePosition)
{
_lastMousePosition = mousePosition;
var dpi = MonitorResolutionHelper.GetCurrentMonitorDpi();
var mousePositionScaled = new Point(mousePosition.X / dpi.DpiScaleX, mousePosition.Y / dpi.DpiScaleX);
var monitorBounds = GetBoundsOfMonitorWithMouseIn(mousePosition);
var windowLeft = mousePositionScaled.X + XOffset;
var windowTop = mousePositionScaled.Y + YOffset;
if ((windowLeft + MonitorRightSideDeadZone) > monitorBounds.Right / dpi.DpiScaleX)
{
windowLeft -= AssociatedObject.Width + WindowOffsetWhenInDeadZone;
}
if ((windowTop + MonitorBottomSideDeadZone) > monitorBounds.Bottom / dpi.DpiScaleX)
{
windowTop -= AssociatedObject.Height + WindowOffsetWhenInDeadZone;
}
AssociatedObject.Left = windowLeft;
AssociatedObject.Top = windowTop;
}
private static Rect GetBoundsOfMonitorWithMouseIn(Point mousePosition)
{
foreach (var monitor in MonitorResolutionHelper.AllMonitors)
{
if (monitor.Bounds.Contains(new Point(mousePosition.X, mousePosition.Y)))
{
return monitor.Bounds;
}
}
Logger.LogWarning("Failed to get monitor bounds for mouse position" + mousePosition.X + "," + mousePosition.Y);
return new Rect(0, 0, 0, 0);
}
}
}

View File

@@ -1,42 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Windows;
using ColorPicker.Helpers;
using Microsoft.Xaml.Behaviors;
namespace ColorPicker.Behaviors
{
public class CloseZoomWindowBehavior : Behavior<Window>
{
private ZoomWindowHelper _zoomWindowHelper;
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.Loaded += AssociatedObject_Loaded;
}
private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
{
AssociatedObject.PreviewKeyDown += AssociatedObject_PreviewKeyDown;
AssociatedObject.MouseLeftButtonDown += AssociatedObject_MouseLeftButtonDown;
_zoomWindowHelper = Bootstrapper.Container.GetExportedValue<ZoomWindowHelper>();
}
private void AssociatedObject_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
_zoomWindowHelper.CloseZoomWindow();
}
private void AssociatedObject_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
if (e.Key == System.Windows.Input.Key.Escape)
{
_zoomWindowHelper.CloseZoomWindow();
}
}
}
}

View File

@@ -1,72 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using ColorPicker.Models;
using Microsoft.Xaml.Behaviors;
namespace ColorPicker.Behaviors
{
public class DragAndDropReorderBehavior : Behavior<ItemsControl>
{
protected override void OnAttached()
{
base.OnAttached();
var style = new Style(typeof(ContentPresenter));
style.Setters.Add(
new EventSetter(
FrameworkElement.PreviewMouseMoveEvent,
new MouseEventHandler(ItemPreviewMouseMove)));
style.Setters.Add(
new EventSetter(
FrameworkElement.DropEvent,
new DragEventHandler(ItemDrop)));
AssociatedObject.ItemContainerStyle = style;
}
private void ItemPreviewMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
Task.Run(
new Action(() =>
{
Dispatcher.BeginInvoke(
new Action(() =>
{
if (e.LeftButton == MouseButtonState.Pressed)
{
var data = new DataObject();
data.SetData("Source", (sender as FrameworkElement).DataContext);
DragDrop.DoDragDrop(sender as DependencyObject, data, DragDropEffects.Move);
e.Handled = true;
}
}),
null);
}),
CancellationToken.None);
}
}
private void ItemDrop(object sender, DragEventArgs e)
{
var source = e.Data.GetData("Source") as ColorFormatModel;
if (source != null)
{
int newIndex = AssociatedObject.Items.IndexOf((sender as FrameworkElement).DataContext);
var list = AssociatedObject.ItemsSource as ObservableCollection<ColorFormatModel>;
list.RemoveAt(list.IndexOf(source));
list.Insert(newIndex, source);
}
}
}
}

View File

@@ -1,33 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Windows;
using Microsoft.Xaml.Behaviors;
namespace ColorPicker.Behaviors
{
public class DragWindowBehavior : Behavior<FrameworkElement>
{
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.Loaded += AssociatedObject_Loaded;
}
private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
{
AssociatedObject.MouseLeftButtonDown += AssociatedObject_MouseLeftButtonDown;
}
private void AssociatedObject_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
var parentWindow = Window.GetWindow(AssociatedObject);
if (parentWindow != null)
{
parentWindow.DragMove();
}
}
}
}

View File

@@ -1,65 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Windows;
using ColorPicker.Shaders;
using Microsoft.Xaml.Behaviors;
namespace ColorPicker.Behaviors
{
public class GridEffectBehavior : Behavior<FrameworkElement>
{
private static double _baseZoomImageSizeInPx = 50;
public static readonly DependencyProperty EffectProperty = DependencyProperty.Register("Effect", typeof(GridShaderEffect), typeof(GridEffectBehavior));
public static readonly DependencyProperty ZoomFactorProperty = DependencyProperty.Register("ZoomFactor", typeof(double), typeof(GridEffectBehavior));
public GridShaderEffect Effect
{
get { return (GridShaderEffect)GetValue(EffectProperty); }
set { SetValue(EffectProperty, value); }
}
public double ZoomFactor
{
get { return (double)GetValue(ZoomFactorProperty); }
set { SetValue(ZoomFactorProperty, value); }
}
protected override void OnAttached()
{
AssociatedObject.Loaded += AssociatedObject_Loaded;
}
private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
{
AssociatedObject.MouseMove += AssociatedObject_MouseMove;
}
private void AssociatedObject_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
{
var position = e.GetPosition(AssociatedObject);
var relativeX = position.X / AssociatedObject.ActualWidth;
var relativeY = position.Y / AssociatedObject.Height;
Effect.MousePosition = new Point(relativeX, relativeY);
if (ZoomFactor >= 4)
{
Effect.Radius = 0.04;
Effect.SquareSize = ZoomFactor;
Effect.TextureSize = _baseZoomImageSizeInPx * ZoomFactor;
}
else
{
// don't show grid, too small pixels
Effect.Radius = 0.0;
Effect.SquareSize = 0;
Effect.TextureSize = 0;
}
}
}
}

View File

@@ -1,106 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Windows;
using System.Windows.Media.Animation;
using Microsoft.Xaml.Behaviors;
namespace ColorPicker.Behaviors
{
public class ResizeBehavior : Behavior<FrameworkElement>
{
// animation behavior variables
// used when size is getting bigger
private static readonly TimeSpan _animationTime = TimeSpan.FromMilliseconds(200);
private static readonly IEasingFunction _easeFunction = new SineEase() { EasingMode = EasingMode.EaseOut };
// used when size is getting smaller
private static readonly TimeSpan _animationTimeSmaller = _animationTime;
private static readonly IEasingFunction _easeFunctionSmaller = new QuadraticEase() { EasingMode = EasingMode.EaseIn };
private static void CustomAnimation(DependencyProperty prop, FrameworkElement sender, double fromValue, double toValue)
{
// if the animation is to/from a value of 0, it will cancel the current animation
DoubleAnimation move = null;
if (toValue > 0 && fromValue > 0)
{
// if getting bigger
if (fromValue < toValue)
{
move = new DoubleAnimation(fromValue, toValue, new Duration(_animationTime), FillBehavior.Stop)
{
EasingFunction = _easeFunction,
};
}
else
{
move = new DoubleAnimation(fromValue, toValue, new Duration(_animationTimeSmaller), FillBehavior.Stop)
{
EasingFunction = _easeFunctionSmaller,
};
}
}
// HandoffBehavior must be SnapshotAndReplace
// Compose does not allow cancellation
sender.BeginAnimation(prop, move, HandoffBehavior.SnapshotAndReplace);
}
public static readonly DependencyProperty WidthProperty = DependencyProperty.Register("Width", typeof(double), typeof(ResizeBehavior), new PropertyMetadata(new PropertyChangedCallback(WidthPropertyChanged)));
private static void WidthPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var sender = ((ResizeBehavior)d).AssociatedObject;
var fromValue = sender.Width;
var toValue = (double)e.NewValue;
// setting Width before animation prevents jumping
sender.Width = toValue;
CustomAnimation(FrameworkElement.WidthProperty, sender, fromValue, toValue);
}
public static readonly DependencyProperty HeightProperty = DependencyProperty.Register("Height", typeof(double), typeof(ResizeBehavior), new PropertyMetadata(new PropertyChangedCallback(HeightPropertyChanged)));
private static void HeightPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var sender = ((ResizeBehavior)d).AssociatedObject;
var fromValue = sender.Height;
var toValue = (double)e.NewValue;
// setting Height before animation prevents jumping
sender.Height = toValue;
CustomAnimation(FrameworkElement.HeightProperty, sender, fromValue, toValue);
}
public double Width
{
get
{
return (double)GetValue(WidthProperty);
}
set
{
SetValue(WidthProperty, value);
}
}
public double Height
{
get
{
return (double)GetValue(HeightProperty);
}
set
{
SetValue(HeightProperty, value);
}
}
}
}

View File

@@ -1,28 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("UnitTest-ColorPickerUI")]
namespace ColorPicker
{
public static class Bootstrapper
{
public static CompositionContainer Container { get; private set; }
public static void InitializeContainer(object initPoint)
{
var catalog = new AssemblyCatalog(System.Reflection.Assembly.GetExecutingAssembly());
Container = new CompositionContainer(catalog);
Container.SatisfyImportsOnce(initPoint);
}
public static void Dispose()
=> Container.Dispose();
}
}

View File

@@ -1,57 +0,0 @@
<Window
x:Class="ColorPicker.ColorEditorWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:behaviors="clr-namespace:ColorPicker.Behaviors"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:e="http://schemas.microsoft.com/xaml/behaviors"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:p="clr-namespace:ColorPicker.Properties"
Width="440"
Height="380"
AutomationProperties.Name="{x:Static p:Resources.cp_editor}"
ResizeMode="NoResize"
Topmost="True"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<e:Interaction.Behaviors>
<behaviors:CloseZoomWindowBehavior />
</e:Interaction.Behaviors>
<Border x:Name="MainBorder" BorderBrush="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<DockPanel
Grid.Row="0"
Background="Transparent"
MouseLeftButtonDown="TitleBar_MouseDown"
MouseRightButtonUp="TitleBar_MouseRightClick">
<Image
Width="20"
Height="20"
Margin="10,5,5,8"
Source="pack://application:,,,/Assets/ColorPicker/icon.ico" />
<TextBlock
Margin="10"
VerticalAlignment="Center"
Text="{x:Static p:Resources.CP_Title}" />
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<Button
Width="50"
Height="30"
Margin="10,-10,-2,1"
Click="Close_Click"
Content="&#xE711;"
FontFamily="{StaticResource IconFontFamily}"
Style="{StaticResource TitleBarCloseButtonStyle}" />
</StackPanel>
</DockPanel>
<ContentPresenter
x:Name="contentPresenter"
Grid.Row="1"
Content="{Binding Content}" />
</Grid>
</Border>
</Window>

View File

@@ -1,183 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Drawing.Printing;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Shell;
using ColorPicker.Helpers;
using ControlzEx.Theming;
using ManagedCommon;
using Microsoft.Diagnostics.Tracing.Parsers.ClrPrivate;
using Microsoft.Win32;
using Windows.Graphics;
namespace ColorPicker
{
/// <summary>
/// Interaction logic for ColorEditorWindow.xaml
/// </summary>
public partial class ColorEditorWindow : Window
{
private readonly AppStateHandler _appStateHandler;
public ColorEditorWindow(AppStateHandler appStateHandler)
{
InitializeComponent();
_appStateHandler = appStateHandler;
Closing += ColorEditorWindow_Closing;
}
private void ColorEditorWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
e.Cancel = true;
_appStateHandler.EndUserSession();
}
protected override void OnSourceInitialized(EventArgs e)
{
WindowChrome.SetWindowChrome(
this,
new WindowChrome
{
CaptionHeight = 0,
CornerRadius = default,
GlassFrameThickness = new Thickness(-1),
ResizeBorderThickness = ResizeMode == ResizeMode.NoResize ? default : new Thickness(4),
UseAeroCaptionButtons = false,
});
if (OSVersionHelper.IsWindows11())
{
// ResizeMode="NoResize" removes rounded corners. So force them to rounded.
IntPtr hWnd = new WindowInteropHelper(GetWindow(this)).EnsureHandle();
DWMWINDOWATTRIBUTE attribute = DWMWINDOWATTRIBUTE.DWMWA_WINDOW_CORNER_PREFERENCE;
DWM_WINDOW_CORNER_PREFERENCE preference = DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUND;
DwmSetWindowAttribute(hWnd, attribute, ref preference, sizeof(uint));
}
else
{
// On Windows10 ResizeMode="NoResize" removes the border so we add a new one.
MainBorder.BorderThickness = new System.Windows.Thickness(0.5);
}
// Hide then Show with WindowStyle="None" will remove the Mica effect. So manually remove the titlebar.
RemoveWindowTitlebarContents();
base.OnSourceInitialized(e);
}
public void RemoveWindowTitlebarContents()
{
IntPtr handle = new WindowInteropHelper(GetWindow(this)).EnsureHandle();
if (handle == IntPtr.Zero)
{
return;
}
int windowStyleLong = GetWindowLong(handle, GWLSTYLE);
windowStyleLong &= ~(int)WindowStyles.WS_SYSMENU;
IntPtr result = SetWindowLong(handle, GWLSTYLE, windowStyleLong);
if (result.ToInt64() == 0)
{
int error = Marshal.GetLastWin32Error();
Logger.LogError($"SetWindowLong error {error}");
}
}
private void Close_Click(object sender, RoutedEventArgs e)
{
Close();
}
private void TitleBar_MouseDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left)
{
this.DragMove();
}
}
private void TitleBar_MouseRightClick(object sender, MouseButtonEventArgs e)
{
IntPtr hwnd = new WindowInteropHelper(this).Handle;
// Get the mouse position relative to the screen
Point mousePosition = e.GetPosition(this);
Point screenPoint = PointToScreen(mousePosition);
// Display the system menu at the current mouse position
IntPtr hMenu = GetSystemMenu(hwnd, false);
if (hMenu != IntPtr.Zero)
{
int command = TrackPopupMenu(
hMenu,
TPMLEFTALIGN | TPMRETURNCMD,
(int)screenPoint.X,
(int)screenPoint.Y,
0,
hwnd,
IntPtr.Zero);
if (command > 0)
{
SendMessage(hwnd, WMSYSCOMMAND, new IntPtr(command), IntPtr.Zero);
}
}
}
private const int WMSYSCOMMAND = 0x0112;
private const int TPMLEFTALIGN = 0x0000;
private const int TPMRETURNCMD = 0x0100;
// The enum flag for DwmSetWindowAttribute's second parameter, which tells the function what attribute to set.
public enum DWMWINDOWATTRIBUTE
{
DWMWA_WINDOW_CORNER_PREFERENCE = 33,
}
public enum DWM_WINDOW_CORNER_PREFERENCE
{
DWMWCP_DEFAULT = 0,
DWMWCP_DONOTROUND = 1,
DWMWCP_ROUND = 2,
DWMWCP_ROUNDSMALL = 3,
}
// Import dwmapi.dll and define DwmSetWindowAttribute in C# corresponding to the native function.
[DllImport("dwmapi.dll", CharSet = CharSet.Unicode, PreserveSig = false)]
internal static extern void DwmSetWindowAttribute(
IntPtr hwnd,
DWMWINDOWATTRIBUTE attribute,
ref DWM_WINDOW_CORNER_PREFERENCE pvAttribute,
uint cbAttribute);
[DllImport("user32.dll")]
private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);
[DllImport("user32.dll")]
private static extern int TrackPopupMenu(IntPtr hMenu, int uFlags, int x, int y, int nReserved, IntPtr hWnd, IntPtr prcRect);
[DllImport("user32.dll")]
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", SetLastError = true)]
private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll", SetLastError = true)]
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
public const int GWLSTYLE = -16;
[Flags]
public enum WindowStyles : uint
{
WS_SYSMENU = 0x00080000, // System menu (close/maximize/minimize button area)
}
}
}

View File

@@ -1,75 +1,90 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
<Import Project="$(RepoRoot)src\Common.SelfContained.props" />
<PropertyGroup>
<AssemblyTitle>PowerToys.ColorPickerUI</AssemblyTitle>
<AssemblyDescription>PowerToys ColorPicker</AssemblyDescription>
<OutputPath>$(RepoRoot)$(Platform)\$(Configuration)</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
<UseWPF>true</UseWPF>
<StartupObject>ColorPicker.Program</StartupObject>
<ProjectGuid>{BA58206B-1493-4C75-BFEA-A85768A1E156}</ProjectGuid>
<OutputType>WinExe</OutputType>
<RootNamespace>ColorPicker</RootNamespace>
<AssemblyName>PowerToys.ColorPickerUI</AssemblyName>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>Assets\ColorPicker\icon.ico</ApplicationIcon>
<NeutralLanguage>en-US</NeutralLanguage>
<ApplicationManifest>App.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
<None Remove="Assets\ColorPicker\icon.ico" />
</ItemGroup>
<ItemGroup>
<Resource Include="Assets\ColorPicker\icon.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>
<Resource Include="Shaders\GridShader.cso" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.ComponentModel.Composition" />
<PackageReference Include="System.IO.Abstractions" />
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" />
</ItemGroup>
<ItemGroup>
<None Update="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
<None Update="Assets\ColorPicker\colorPicker.cur">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\GPOWrapperProjection\GPOWrapperProjection.csproj" />
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="Shaders\GridShader.fx" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DependentUpon>Resources.resx</DependentUpon>
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<SubType>Designer</SubType>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
<Import Project="$(RepoRoot)src\Common.SelfContained.props" />
<PropertyGroup>
<AssemblyTitle>PowerToys.ColorPickerUI</AssemblyTitle>
<AssemblyDescription>PowerToys ColorPicker</AssemblyDescription>
<OutputType>WinExe</OutputType>
<OutputPath>$(RepoRoot)$(Platform)\$(Configuration)\WinUI3Apps</OutputPath>
<RootNamespace>ColorPicker</RootNamespace>
<AssemblyName>PowerToys.ColorPickerUI</AssemblyName>
<ApplicationManifest>App.manifest</ApplicationManifest>
<UseWinUI>true</UseWinUI>
<GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
<WindowsPackageType>None</WindowsPackageType>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<ApplicationIcon>Assets\ColorPicker\icon.ico</ApplicationIcon>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<!-- MRT searches for a pri file with the module name before defaulting to resources.pri -->
<ProjectPriFileName>PowerToys.ColorPickerUI.pri</ProjectPriFileName>
<!-- Custom Main entry point (append so inherited DEBUG/TRACE constants survive) -->
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_MAIN</DefineConstants>
</PropertyGroup>
<ItemGroup>
<Page Remove="ColorPickerXAML\App.xaml" />
</ItemGroup>
<ItemGroup>
<ApplicationDefinition Include="ColorPickerXAML\App.xaml" />
</ItemGroup>
<!-- See https://learn.microsoft.com/windows/apps/develop/platform/csharp-winrt/net-projection-from-cppwinrt-component -->
<PropertyGroup>
<CsWinRTIncludes>PowerToys.GPOWrapper</CsWinRTIncludes>
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
</PropertyGroup>
<!-- Allow the test project to access internal types (moved from Bootstrapper.cs) -->
<ItemGroup>
<InternalsVisibleTo Include="ColorPickerUI.UnitTests" />
</ItemGroup>
<ItemGroup>
<Resource Include="Assets\ColorPicker\icon.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>
<None Update="Assets\ColorPicker\colorPicker.cur">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" />
<!-- Required by the XAML compiler so it can resolve TransparentWindow's animation-set
properties (ShowAnimations/HideAnimations) when ColorPickerOverlayWindow.xaml derives
from the Common.UI.Controls TransparentWindow base. -->
<PackageReference Include="CommunityToolkit.WinUI.Animations" />
<!-- Win2D for the zoom magnifier's pixel-grid CanvasControl. -->
<PackageReference Include="Microsoft.Graphics.Win2D" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" />
<PackageReference Include="System.IO.Abstractions" />
<PackageReference Include="WinUIEx" />
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnablePreviewMsixTooling)'=='true'">
<ProjectCapability Include="Msix" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
<ProjectReference Include="..\..\..\common\Common.UI.Controls\Common.UI.Controls.csproj" />
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,14 +1,13 @@
<Application
x:Class="ColorPickerUI.App"
<Application
x:Class="ColorPicker.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml"
ThemeMode="System">
xmlns:local="using:ColorPicker">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles.xaml" />
<ResourceDictionary Source="Resources/ViewModelViewMappings.xaml" />
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<ResourceDictionary Source="ms-appx:///Resources/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>

View File

@@ -0,0 +1,152 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using System.Threading;
using ManagedCommon;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Xaml;
namespace ColorPicker
{
public partial class App : Application, IDisposable
{
private readonly string[] _args;
private Mutex _instanceMutex;
private int _powerToysRunnerPid;
private bool _disposed;
private static IServiceProvider _serviceProvider;
public static Window Window { get; private set; }
// ETW diagnostic trace session (matches the WPF original and the WinUI 3 sibling modules,
// e.g. EnvironmentVariables/AdvancedPaste). Constructed once; the session is torn down on
// process exit, so no explicit Dispose is required (siblings follow the same pattern).
public ETWTrace EtwTrace { get; } = new ETWTrace();
public App(string[] args)
{
_args = args ?? Array.Empty<string>();
try
{
string appLanguage = LanguageHelper.LoadLanguage();
if (!string.IsNullOrEmpty(appLanguage))
{
Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = appLanguage;
Thread.CurrentThread.CurrentUICulture = new CultureInfo(appLanguage);
}
}
catch (Exception ex)
{
Logger.LogError("Language initialization error: " + ex.Message);
}
_serviceProvider = ConfigureServices();
// Environment.Exit on the runner-exit / terminate / single-instance paths raises no
// Window.Closed, so the cursor-restore in MouseInfoProvider.DisposeHook never runs.
// Restore the system cursors on any process exit (idempotent) so an abnormal exit while
// a pick is active with ChangeCursor enabled does not leave the crosshair as the
// system-wide cursor — parity with the WPF App.OnExit.
AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
InitializeComponent();
UnhandledException += App_UnhandledException;
}
public bool IsRunningDetachedFromPowerToys() => _powerToysRunnerPid == -1;
public static T GetService<T>()
where T : class
=> _serviceProvider.GetService(typeof(T)) as T;
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
// Allow only one instance of Color Picker.
// Single-instance guard. The mutex is intentionally released by OS process
// exit (this is an unpackaged single-process app); no explicit ReleaseMutex.
_instanceMutex = new Mutex(true, @"Local\PowerToys_ColorPicker_InstanceMutex", out bool createdNew);
if (!createdNew)
{
Logger.LogWarning("There is a ColorPicker instance running. Exiting Color Picker");
_instanceMutex = null;
Environment.Exit(0);
return;
}
if (_args.Length > 0 && int.TryParse(_args[0], out _powerToysRunnerPid))
{
Logger.LogInfo($"Color Picker started from the PowerToys Runner. Runner pid={_powerToysRunnerPid}");
RunnerHelper.WaitForPowerToysRunner(_powerToysRunnerPid, () =>
{
Logger.LogInfo("PowerToys Runner exited. Exiting ColorPicker");
GetService<CancellationTokenSource>()?.Cancel();
Environment.Exit(0);
});
}
else
{
_powerToysRunnerPid = -1;
}
// Build the picking overlay first so the DI graph (AppStateHandler etc.) can read
// App.Window, then resolve the main view model (which wires the named events + input)
// and host it in the overlay's MainView. The overlay starts hidden (TransparentWindow)
// and is shown on the hotkey / ShowColorPicker event.
var overlay = new ColorPickerOverlayWindow();
Window = overlay;
var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(overlay);
var mainViewModel = GetService<ColorPicker.ViewModelContracts.IMainViewModel>();
overlay.MainViewControl.DataContext = mainViewModel;
mainViewModel.RegisterWindowHandle(hwnd);
// Make the overlay size to the tooltip and follow the cursor while shown (the
// IMouseInfoProvider singleton is the same one the MainViewModel reads).
overlay.InitializeCursorFollow(GetService<ColorPicker.Mouse.IMouseInfoProvider>());
}
private static IServiceProvider ConfigureServices()
=> ColorPicker.Foundation.AppServices.Configure();
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
Logger.LogError("Unhandled exception", e.Exception);
}
private void OnProcessExit(object sender, EventArgs e)
{
// Environment.Exit and normal shutdown both raise ProcessExit; run the deterministic
// cleanup here since the WinUI Application model never calls Dispose() on the App.
Mouse.CursorManager.RestoreOriginalCursors();
Dispose();
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
_instanceMutex?.Dispose();
_instanceMutex = null;
}
_disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8" ?>
<Window
x:Class="ColorPicker.ColorEditorWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<ContentControl
x:Name="ContentPresenter"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
x:FieldModifier="public" />
</Window>

View File

@@ -0,0 +1,73 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using ColorPicker.Helpers;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using WinUIEx;
namespace ColorPicker
{
/// <summary>
/// The color editor window. Approach A: lean on the native WinUI title bar + a fixed-size
/// OverlappedPresenter instead of the WPF WindowChrome / DWM / custom-title-bar machinery.
/// </summary>
public sealed partial class ColorEditorWindow : Window
{
private readonly AppStateHandler _appStateHandler;
public ColorEditorWindow(AppStateHandler appStateHandler)
{
InitializeComponent();
_appStateHandler = appStateHandler;
var title = ResourceLoaderInstance.GetString("CP_Title");
Title = title;
AppWindow.Title = title;
// The native title bar / taskbar otherwise shows the generic WinUI placeholder icon;
// point it at the ColorPicker app icon (copied next to the exe by the csproj).
AppWindow.SetIcon("Assets/ColorPicker/icon.ico");
// Without a backdrop the window's uncovered areas (the header above the content card)
// render black, which reads as a bold black bar at the header/content divider. Mica
// gives the whole window the standard WinUI material so the header is no longer black.
SystemBackdrop = new MicaBackdrop();
this.SetWindowSize(440, 380);
// Port of the WPF WindowStartupLocation="CenterScreen": WinUI windows open at the OS
// default (cascade near the top-left), so center the fixed-size editor on the display.
this.CenterOnScreen();
if (AppWindow.Presenter is OverlappedPresenter presenter)
{
presenter.IsResizable = false;
presenter.IsMaximizable = false;
presenter.IsMinimizable = false;
presenter.IsAlwaysOnTop = true;
}
AppWindow.Closing += AppWindow_Closing;
Activated += ColorEditorWindow_Activated;
}
public bool IsActiveWindow { get; private set; }
private void ColorEditorWindow_Activated(object sender, WindowActivatedEventArgs args)
{
IsActiveWindow = args.WindowActivationState != WindowActivationState.Deactivated;
}
// Match the WPF behavior: closing the editor ends the user session (which hides it) rather
// than destroying the window, so it can be reopened on the next activation.
private void AppWindow_Closing(AppWindow sender, AppWindowClosingEventArgs args)
{
args.Cancel = true;
_appStateHandler.EndUserSession();
}
}
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8" ?>
<common:TransparentWindow
x:Class="ColorPicker.ColorPickerOverlayWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:common="using:Microsoft.PowerToys.Common.UI.Controls.Window"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="using:ColorPicker.Views"
mc:Ignorable="d">
<views:MainView x:Name="MainViewControl" x:FieldModifier="public" />
</common:TransparentWindow>

View File

@@ -0,0 +1,284 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Linq;
using System.Runtime.InteropServices;
using ColorPicker.Helpers;
using ColorPicker.Mouse;
using ManagedCommon;
using Microsoft.PowerToys.Common.UI.Controls.Window;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media.Animation;
using Windows.Foundation;
namespace ColorPicker
{
/// <summary>
/// The picking-overlay tooltip window: hosts <see cref="Views.MainView"/> inside the
/// shared <see cref="TransparentWindow"/> (transparent <c>TransparentTintBackdrop</c>,
/// frameless, tool-window so it stays out of the taskbar/Alt-Tab, and a no-activate
/// <c>Show()</c> so it never steals foreground from the user's work).
/// </summary>
/// <remarks>
/// This replaces the deleted WPF root MainWindow (AllowsTransparency + WindowStyle=None +
/// SizeToContent + Topmost). The WPF window auto-sized to the small color tooltip, sat above
/// the zoom magnifier (Topmost, re-asserted by the zoom helper), and was re-positioned next to
/// the cursor by the <c>ChangeWindowPositionBehavior</c>. <see cref="InitializeCursorFollow"/>
/// ports all three: WinUI has no <c>SizeToContent</c>, the behavior was WPF-only, and the
/// window is kept top-most so the tooltip stays visible over the magnifier. Per-tick work is a
/// single top-most <c>SetWindowPos</c> move plus a cheap per-monitor DPI lookup (so a tooltip
/// dragged across mixed-DPI monitors re-sizes to the monitor under the cursor); the tooltip size
/// and monitor list are cached and only recomputed on first show / content change.
/// </remarks>
public sealed partial class ColorPickerOverlayWindow : TransparentWindow
{
// Cursor-follow offsets + monitor-edge handling (ported from the WPF
// ChangeWindowPositionBehavior; logical px, scaled to physical at the point of use).
private const double XOffset = 5;
private const double YOffset = 10;
private const int HwndTopmost = -1;
private const uint SwpNoSize = 0x0001;
private const uint SwpNoActivate = 0x0010;
private const uint MonitorDefaultToNearest = 0x00000002;
private const int MdtEffectiveDpi = 0;
private const int SmCxScreen = 0;
private const int SmCyScreen = 1;
private readonly nint _hwnd;
private IMouseInfoProvider _mouseInfoProvider;
private Storyboard _appearStoryboard;
private Point _lastCursor;
private double _scale = 1.0;
private Rect[] _monitors = Array.Empty<Rect>();
private int _width = -1;
private int _height = -1;
public ColorPickerOverlayWindow()
{
InitializeComponent();
_hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
}
/// <summary>
/// Makes the overlay size itself to the <see cref="Views.MainView"/> tooltip and follow
/// the cursor (kept above the zoom magnifier) while it is shown — the WinUI replacement for
/// the WPF <c>SizeToContent="WidthAndHeight"</c> + <c>ChangeWindowPositionBehavior</c> +
/// re-asserted <c>Topmost</c> that the migration had not yet ported.
/// </summary>
public void InitializeCursorFollow(IMouseInfoProvider mouseInfoProvider)
{
_mouseInfoProvider = mouseInfoProvider;
// The provider polls on the UI thread (its DispatcherQueueTimer), so this fires on the
// UI thread and can touch XAML/the window directly. It only fires when the cursor moves.
_mouseInfoProvider.MousePositionChanged += (s, position) => MoveToCursor(position, resize: false);
// The tooltip resizes only when its content changes (e.g. a longer color string or the
// color-name line). Re-cache the size then and re-apply it; the per-tick path stays a
// pure move so following is not throttled by a layout pass every cursor tick.
if (Content is FrameworkElement card)
{
card.SizeChanged += (s, e) => MoveToCursor(_lastCursor, resize: true);
}
}
/// <summary>
/// Hides <see cref="TransparentWindow.Show"/> so the overlay snaps to the cursor as it
/// appears. <see cref="IMouseInfoProvider.MousePositionChanged"/> only fires when the
/// cursor moves, so a stationary-cursor show would otherwise leave the tooltip parked at
/// the default window position. Positioning is queued after <c>base.Show()</c> so the card
/// is laid out (a collapsed element measures to 0) before sizing.
/// </summary>
public new void Show()
{
// Present the card transparent first (so there is no flash), then start the 250ms fade
// and the initial positioning together at Low priority — after base TransparentWindow.Show()
// has pumped its deferred SW_SHOWNA, so the fade clock begins when the window is actually
// on screen and the card is laid out before it is sized to the cursor.
MainViewControl.Opacity = 0;
base.Show();
DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
PlayAppearAnimation();
if (_mouseInfoProvider != null)
{
// Cache the monitor bounds for this pick session (the DPI scale is re-queried per
// move so a tooltip crossing into a different-DPI monitor re-sizes correctly).
RefreshEnvironment();
MoveToCursor(_mouseInfoProvider.CurrentPosition, resize: true);
}
});
}
/// <summary>
/// Port of the WPF <c>AppearAnimationBehavior</c>: fade the tooltip in over 250ms
/// (CubicEase) every time the overlay is shown. WPF animated <c>Window.Opacity</c>; a WinUI
/// <see cref="Window"/> has no Opacity, so the hosted content element is animated instead —
/// visually identical since it fills the whole tooltip. (WPF's 1ms hide fade was effectively
/// instant, so no hide animation is needed.)
/// </summary>
private void PlayAppearAnimation()
{
if (_appearStoryboard == null)
{
var fadeIn = new DoubleAnimation
{
From = 0,
To = 1,
Duration = new Duration(TimeSpan.FromMilliseconds(250)),
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseInOut },
};
Storyboard.SetTarget(fadeIn, MainViewControl);
Storyboard.SetTargetProperty(fadeIn, "Opacity");
_appearStoryboard = new Storyboard();
_appearStoryboard.Children.Add(fadeIn);
}
_appearStoryboard.Stop();
MainViewControl.Opacity = 0;
_appearStoryboard.Begin();
}
private void RefreshEnvironment()
{
_scale = GetDpiForWindow(_hwnd) / 96.0;
_monitors = MonitorResolutionHelper.AllMonitors.Select(m => m.Bounds).ToArray();
}
private void MoveToCursor(Point cursorPhysical, bool resize)
{
_lastCursor = cursorPhysical;
if (_monitors.Length == 0)
{
RefreshEnvironment();
}
// Re-query the DPI of the monitor under the cursor. On mixed-DPI multi-monitor setups
// the scale changes as the cursor crosses monitors; a value frozen at Show() would
// mis-size and mis-offset the tooltip (the WPF behavior re-queried DPI per move).
double scale = GetScaleForCursor(cursorPhysical);
if (scale > 0 && scale != _scale)
{
_scale = scale;
resize = true; // re-measure the tooltip card at the new monitor's scale
}
// WinUI has no SizeToContent: size the window to the tooltip card. Only measured when a
// resize is requested (first show / content change), so the cursor tick stays a move.
if (resize || _width <= 0 || _height <= 0)
{
if (Content is not FrameworkElement card)
{
return;
}
card.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
int width = (int)Math.Ceiling(card.DesiredSize.Width * _scale);
int height = (int)Math.Ceiling(card.DesiredSize.Height * _scale);
if (width <= 0 || height <= 0)
{
// Card still collapsed/unmeasured (fired before the show completes); a later
// tick or the post-Show() queue will size and position it.
return;
}
_width = width;
_height = height;
}
// Place next to the cursor, flipping away from the monitor's right/bottom edge so the
// tooltip never spills off-screen. Everything here is in physical pixels.
var bounds = GetMonitorBounds(cursorPhysical);
int xOffset = (int)(XOffset * _scale);
int yOffset = (int)(YOffset * _scale);
int left = (int)cursorPhysical.X + xOffset;
int top = (int)cursorPhysical.Y + yOffset;
if (left + _width > bounds.X + bounds.Width)
{
left = (int)cursorPhysical.X - _width - xOffset;
}
if (top + _height > bounds.Y + bounds.Height)
{
top = (int)cursorPhysical.Y - _height - yOffset;
}
// Insert at HWND_TOPMOST so the tooltip stays above the (non-top-most) zoom magnifier,
// matching the WPF helper that re-asserted MainWindow.Topmost after showing the zoom.
uint flags = SwpNoActivate | (resize ? 0u : SwpNoSize);
_ = SetWindowPos(_hwnd, HwndTopmost, left, top, _width, _height, flags);
}
private Rect GetMonitorBounds(Point cursorPhysical)
{
foreach (var bounds in _monitors)
{
if (bounds.Contains(cursorPhysical))
{
return bounds;
}
}
if (_monitors.Length > 0)
{
// Cursor not inside any enumerated monitor (rare; e.g. just outside the work area):
// fall back to the first monitor so the edge-flip math still applies.
return _monitors[0];
}
// No monitors enumerated at all (degenerate, e.g. a transient no-display state). Log it
// and fall back to the primary screen bounds so the tooltip still edge-flips instead of
// being placed against an unbounded rect.
Logger.LogWarning("ColorPicker overlay: no monitors enumerated; falling back to primary screen bounds.");
return new Rect(0, 0, GetSystemMetrics(SmCxScreen), GetSystemMetrics(SmCyScreen));
}
// Effective DPI scale of the monitor the cursor is on (1.0 == 96 DPI). Falls back to the
// last known scale if the lookup fails.
private double GetScaleForCursor(Point cursorPhysical)
{
var monitor = MonitorFromPoint(new POINT { X = (int)cursorPhysical.X, Y = (int)cursorPhysical.Y }, MonitorDefaultToNearest);
if (monitor != IntPtr.Zero && GetDpiForMonitor(monitor, MdtEffectiveDpi, out uint dpiX, out _) == 0)
{
return dpiX / 96.0;
}
return _scale;
}
[StructLayout(LayoutKind.Sequential)]
private struct POINT
{
public int X;
public int Y;
}
[DllImport("user32.dll")]
private static extern IntPtr MonitorFromPoint(POINT pt, uint dwFlags);
[DllImport("shcore.dll")]
private static extern int GetDpiForMonitor(IntPtr hmonitor, int dpiType, out uint dpiX, out uint dpiY);
[DllImport("user32.dll")]
private static extern int GetSystemMetrics(int nIndex);
[DllImport("user32.dll")]
private static extern uint GetDpiForWindow(nint hwnd);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetWindowPos(nint hWnd, nint hWndInsertAfter, int x, int y, int cx, int cy, uint flags);
}
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8" ?>
<common:TransparentWindow
x:Class="ColorPicker.ZoomWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:common="using:Microsoft.PowerToys.Common.UI.Controls.Window"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="using:ColorPicker.Views"
mc:Ignorable="d">
<views:ZoomView x:Name="ZoomViewControl" x:FieldModifier="public" />
</common:TransparentWindow>

View File

@@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.PowerToys.Common.UI.Controls.Window;
namespace ColorPicker
{
/// <summary>
/// The zoom magnifier window: a transparent, frameless, no-activate
/// <see cref="TransparentWindow"/> hosting the Win2D-backed <see cref="Views.ZoomView"/>.
/// </summary>
public sealed partial class ZoomWindow : TransparentWindow
{
public ZoomWindow()
{
InitializeComponent();
}
}
}

View File

@@ -1,42 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Windows.Input;
namespace ColorPicker.Common
{
public class RelayCommand : ICommand
{
private Predicate<object> _canExecute;
private Action<object> _execute;
public RelayCommand(Action execute)
{
_canExecute = x => true;
_execute = x => { execute.Invoke(); };
}
public RelayCommand(Action<object> execute)
: this(x => true, execute)
{
}
public RelayCommand(Predicate<object> canExecute, Action<object> execute)
{
_canExecute = canExecute;
_execute = execute;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public bool CanExecute(object parameter) => _canExecute(parameter);
public void Execute(object parameter) => _execute(parameter);
}
}

View File

@@ -1,22 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace ColorPicker.Common
{
/// <summary>
/// Base for view models to provide property changed notifications
/// </summary>
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@@ -3,134 +3,29 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:ColorPicker"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:p="clr-namespace:ColorPicker.Properties"
mc:Ignorable="d">
<UserControl.Resources>
<Style x:Key="ReadonlyTextBoxStyle" TargetType="{x:Type TextBox}">
<!-- Universal WPF UI focus -->
<Setter Property="FocusVisualStyle" Value="{DynamicResource DefaultControlFocusVisualStyle}" />
<!-- Universal WPF UI focus -->
<!-- Universal WPF UI ContextMenu -->
<Setter Property="ContextMenu" Value="{DynamicResource DefaultControlContextMenu}" />
<!-- Universal WPF UI ContextMenu -->
<Setter Property="Foreground" Value="{DynamicResource TextFillColorPrimaryBrush}" />
<Setter Property="CaretBrush" Value="{DynamicResource TextFillColorPrimaryBrush}" />
<Setter Property="Background">
<Setter.Value>
<SolidColorBrush Color="Transparent" />
</Setter.Value>
</Setter>
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="{StaticResource TextBoxBorderThemeThickness}" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="ScrollViewer.CanContentScroll" Value="False" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Hidden" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Hidden" />
<Setter Property="ScrollViewer.IsDeferredScrollingEnabled" Value="False" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Top" />
<Setter Property="MinHeight" Value="{DynamicResource TextControlThemeMinHeight}" />
<Setter Property="MinWidth" Value="{DynamicResource TextControlThemeMinWidth}" />
<Setter Property="Padding" Value="{DynamicResource TextControlThemePadding}" />
<Setter Property="Border.CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBox}">
<Grid HorizontalAlignment="{TemplateBinding HorizontalAlignment}" VerticalAlignment="{TemplateBinding VerticalAlignment}">
<Border
x:Name="ContentBorder"
MinWidth="{TemplateBinding MinWidth}"
MinHeight="{TemplateBinding MinHeight}"
Padding="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding Border.CornerRadius}">
<Grid
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
<ScrollViewer
x:Name="PART_ContentHost"
VerticalAlignment="Center"
CanContentScroll="{TemplateBinding ScrollViewer.CanContentScroll}"
HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
IsDeferredScrollingEnabled="{TemplateBinding ScrollViewer.IsDeferredScrollingEnabled}"
IsTabStop="{TemplateBinding ScrollViewer.IsTabStop}"
TextElement.Foreground="{TemplateBinding Foreground}"
VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}" />
</Grid>
</Border>
<!-- The Accent Border is a separate element so that changes to the border thickness do not affect the position of the element -->
<Border
x:Name="AccentBorder"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
BorderThickness="{StaticResource TextBoxAccentBorderThemeThickness}"
CornerRadius="{TemplateBinding Border.CornerRadius}">
<Border.BorderBrush>
<SolidColorBrush Color="Transparent" />
</Border.BorderBrush>
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsFocused" Value="True">
<Setter TargetName="AccentBorder" Property="BorderThickness" Value="0,0,0,0" />
<Setter TargetName="AccentBorder" Property="BorderBrush">
<Setter.Value>
<SolidColorBrush Color="Transparent" />
</Setter.Value>
</Setter>
<Setter TargetName="ContentBorder" Property="Background">
<Setter.Value>
<SolidColorBrush Color="Transparent" />
</Setter.Value>
</Setter>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsEnabled" Value="True" />
<Condition Property="IsMouseOver" Value="True" />
<Condition Property="IsFocused" Value="False" />
</MultiTrigger.Conditions>
<Setter TargetName="ContentBorder" Property="Background">
<Setter.Value>
<SolidColorBrush Color="Transparent" />
</Setter.Value>
</Setter>
</MultiTrigger>
<Trigger Property="IsEnabled" Value="True">
<Setter Property="Cursor" Value="IBeam" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="ContentBorder" Property="Background" Value="Transparent" />
<Setter TargetName="ContentBorder" Property="BorderBrush" Value="Transparent" />
<Setter TargetName="AccentBorder" Property="BorderBrush" Value="Transparent" />
<Setter Property="Foreground" Value="{DynamicResource TextFillColorDisabledBrush}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<!-- A label-like read-only text box: lean on the native template, just drop the chrome. -->
<Style
x:Key="ReadonlyTextBoxStyle"
BasedOn="{StaticResource DefaultTextBoxStyle}"
TargetType="TextBox">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="IsReadOnly" Value="True" />
<Setter Property="MinHeight" Value="0" />
<Setter Property="Padding" Value="0" />
</Style>
</UserControl.Resources>
<Border
x:Name="MainBorder"
Height="48"
Margin="0,0,0,8"
HorizontalAlignment="Stretch"
Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{DynamicResource CardStrokeColorDefaultBrush}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="{DynamicResource ControlCornerRadius}">
CornerRadius="{ThemeResource ControlCornerRadius}">
<Grid Margin="12,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="48" />
@@ -140,7 +35,7 @@
<TextBlock
x:Name="FormatNameTextBlock"
VerticalAlignment="Center"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
TextTrimming="CharacterEllipsis" />
@@ -149,10 +44,6 @@
Grid.Column="1"
Margin="0,-2,0,0"
VerticalAlignment="Center"
AutomationProperties.Name="{x:Static p:Resources.Color_Code}"
Background="Transparent"
BorderBrush="Transparent"
BorderThickness="0"
IsReadOnly="True"
IsTabStop="False"
Style="{StaticResource ReadonlyTextBoxStyle}" />
@@ -164,12 +55,9 @@
Height="36"
Margin="0,0,4,0"
Padding="0"
AutomationProperties.HelpText="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:Controls.ColorFormatControl}}, Path=SelectedColorCopyHelperText}"
AutomationProperties.Name="{x:Static p:Resources.Copy_to_clipboard}"
Background="Transparent"
BorderBrush="Transparent"
Style="{StaticResource SubtleButtonStyle}"
ToolTipService.ToolTip="{x:Static p:Resources.Copy_to_clipboard}">
Style="{StaticResource SubtleButtonStyle}">
<Button.Content>
<TextBlock FontFamily="{StaticResource IconFontFamily}" Text="&#xE8C8;" />
</Button.Content>

View File

@@ -1,67 +1,74 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Automation.Peers;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using ColorPicker.Helpers;
using ColorPicker.Models;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Automation.Peers;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Animation;
using Windows.UI;
namespace ColorPicker.Controls
{
/// <summary>
/// Interaction logic for ColorFormatControl.xaml
/// </summary>
public partial class ColorFormatControl : UserControl
public sealed partial class ColorFormatControl : UserControl
{
public static readonly DependencyProperty ColorFormatModelProperty = DependencyProperty.Register("ColorFormatModel", typeof(ColorFormatModel), typeof(ColorFormatControl), new PropertyMetadata(ColorFormatModelPropertyChanged));
public static readonly DependencyProperty ColorFormatModelProperty = DependencyProperty.Register(nameof(ColorFormatModel), typeof(ColorFormatModel), typeof(ColorFormatControl), new PropertyMetadata(null, ColorFormatModelPropertyChanged));
public static readonly DependencyProperty SelectedColorProperty = DependencyProperty.Register("SelectedColor", typeof(Color), typeof(ColorFormatControl), new PropertyMetadata(SelectedColorPropertyChanged));
public static readonly DependencyProperty SelectedColorProperty = DependencyProperty.Register(nameof(SelectedColor), typeof(Color), typeof(ColorFormatControl), new PropertyMetadata(default(Color), SelectedColorPropertyChanged));
public static readonly DependencyProperty SelectedColorCopyHelperTextProperty = DependencyProperty.Register("SelectedColorCopyHelperText", typeof(string), typeof(ColorFormatControl));
public static readonly DependencyProperty SelectedColorCopyHelperTextProperty = DependencyProperty.Register(nameof(SelectedColorCopyHelperText), typeof(string), typeof(ColorFormatControl), new PropertyMetadata(null));
public static readonly DependencyProperty ColorCopiedNotificationBorderProperty = DependencyProperty.Register("ColorCopiedNotificationBorder", typeof(FrameworkElement), typeof(ColorFormatControl), new PropertyMetadata(ColorCopiedBorderPropertyChanged));
public static readonly DependencyProperty ColorCopiedNotificationBorderProperty = DependencyProperty.Register(nameof(ColorCopiedNotificationBorder), typeof(FrameworkElement), typeof(ColorFormatControl), new PropertyMetadata(null));
private const int CopyIndicatorStayTimeInMs = 3000;
private IThrottledActionInvoker _actionInvoker;
private readonly IThrottledActionInvoker _actionInvoker;
private bool _copyIndicatorVisible;
public Color SelectedColor
{
get { return (Color)GetValue(SelectedColorProperty); }
set { SetValue(SelectedColorProperty, value); }
}
public ColorFormatModel ColorFormatModel
{
get { return (ColorFormatModel)GetValue(ColorFormatModelProperty); }
set { SetValue(ColorFormatModelProperty, value); }
}
public FrameworkElement ColorCopiedNotificationBorder
{
get { return (FrameworkElement)GetValue(ColorCopiedNotificationBorderProperty); }
set { SetValue(ColorCopiedNotificationBorderProperty, value); }
}
public string SelectedColorCopyHelperText
{
get { return (string)GetValue(SelectedColorCopyHelperTextProperty); }
set { SetValue(SelectedColorCopyHelperTextProperty, value); }
}
public ColorFormatControl()
{
InitializeComponent();
_actionInvoker = Bootstrapper.Container.GetExportedValue<IThrottledActionInvoker>();
_actionInvoker = App.GetService<IThrottledActionInvoker>();
CopyToClipboardButton.Click += CopyToClipboardButton_Click;
// AutomationProperties.Name/ToolTip used WPF's {x:Static p:Resources.*}; resolve the
// localized strings through the resource loader instead.
AutomationProperties.SetName(ColorTextRepresentationTextBlock, ResourceLoaderInstance.GetString("Color_Code"));
AutomationProperties.SetName(CopyToClipboardButton, ResourceLoaderInstance.GetString("Copy_to_clipboard"));
ToolTipService.SetToolTip(CopyToClipboardButton, ResourceLoaderInstance.GetString("Copy_to_clipboard"));
}
public Color SelectedColor
{
get => (Color)GetValue(SelectedColorProperty);
set => SetValue(SelectedColorProperty, value);
}
public ColorFormatModel ColorFormatModel
{
get => (ColorFormatModel)GetValue(ColorFormatModelProperty);
set => SetValue(ColorFormatModelProperty, value);
}
public FrameworkElement ColorCopiedNotificationBorder
{
get => (FrameworkElement)GetValue(ColorCopiedNotificationBorderProperty);
set => SetValue(ColorCopiedNotificationBorderProperty, value);
}
public string SelectedColorCopyHelperText
{
get => (string)GetValue(SelectedColorCopyHelperTextProperty);
set => SetValue(SelectedColorCopyHelperTextProperty, value);
}
private void CopyToClipboardButton_Click(object sender, RoutedEventArgs e)
@@ -79,60 +86,89 @@ namespace ColorPicker.Controls
private static void SelectedColorPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var self = (ColorFormatControl)d;
if (self.ColorFormatModel == null)
{
return;
}
var colorText = self.ColorFormatModel.GetColorText((Color)e.NewValue);
self.ColorTextRepresentationTextBlock.Text = colorText;
self.ColorTextRepresentationTextBlock.ToolTip = colorText;
ToolTipService.SetToolTip(self.ColorTextRepresentationTextBlock, colorText);
self.SelectedColorCopyHelperText = string.Format(CultureInfo.InvariantCulture, "{0} {1}", self.ColorFormatModel.FormatName, colorText);
// WPF used a RelativeSource FindAncestor binding for this; WinUI {Binding} has no
// FindAncestor, so push the help text onto the button directly.
AutomationProperties.SetHelpText(self.CopyToClipboardButton, self.SelectedColorCopyHelperText);
}
private static void ColorFormatModelPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((ColorFormatControl)d).FormatNameTextBlock.Text = ((ColorFormatModel)e.NewValue).FormatName;
((ColorFormatControl)d).FormatNameTextBlock.ToolTip = ((ColorFormatModel)e.NewValue).FormatName;
}
private static void ColorCopiedBorderPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((ColorFormatControl)d).ColorCopiedNotificationBorder = (FrameworkElement)e.NewValue;
var self = (ColorFormatControl)d;
var model = (ColorFormatModel)e.NewValue;
self.FormatNameTextBlock.Text = model.FormatName;
ToolTipService.SetToolTip(self.FormatNameTextBlock, model.FormatName);
}
private void AppearCopiedIndicator()
{
_copyIndicatorVisible = true;
var opacityAppear = new DoubleAnimation(1.0, new Duration(TimeSpan.FromMilliseconds(300)));
opacityAppear.EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseInOut };
var resize = new DoubleAnimation(56, new Duration(TimeSpan.FromMilliseconds(300)));
AnimateCopiedIndicator(1.0, 56);
resize.EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseInOut };
ColorCopiedNotificationBorder.BeginAnimation(Border.OpacityProperty, opacityAppear);
ColorCopiedNotificationBorder.BeginAnimation(Border.HeightProperty, resize);
var clipboardNotification = ((Decorator)ColorCopiedNotificationBorder).Child;
if (clipboardNotification == null)
if (ColorCopiedNotificationBorder is not Border border || border.Child is not StackPanel panel)
{
return;
}
var innerTextBlock = ((StackPanel)clipboardNotification).Children.OfType<TextBlock>().FirstOrDefault();
var peer = UIElementAutomationPeer.FromElement(innerTextBlock);
if (peer == null)
var innerTextBlock = panel.Children.OfType<TextBlock>().FirstOrDefault();
if (innerTextBlock == null)
{
peer = UIElementAutomationPeer.CreatePeerForElement(innerTextBlock);
return;
}
peer.RaiseAutomationEvent(AutomationEvents.MenuOpened);
var peer = FrameworkElementAutomationPeer.FromElement(innerTextBlock)
?? FrameworkElementAutomationPeer.CreatePeerForElement(innerTextBlock);
peer?.RaiseAutomationEvent(AutomationEvents.LiveRegionChanged);
}
private void HideCopiedIndicator()
{
_copyIndicatorVisible = false;
var opacityDisappear = new DoubleAnimation(0, new Duration(TimeSpan.FromMilliseconds(300)));
opacityDisappear.EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseInOut };
var resize = new DoubleAnimation(0, new Duration(TimeSpan.FromMilliseconds(300)));
AnimateCopiedIndicator(0, 0);
}
resize.EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseInOut };
ColorCopiedNotificationBorder.BeginAnimation(Border.OpacityProperty, opacityDisappear);
ColorCopiedNotificationBorder.BeginAnimation(Border.HeightProperty, resize);
private void AnimateCopiedIndicator(double opacity, double height)
{
if (ColorCopiedNotificationBorder == null)
{
return;
}
var storyboard = new Storyboard();
var opacityAnimation = new DoubleAnimation
{
To = opacity,
Duration = new Duration(TimeSpan.FromMilliseconds(300)),
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseInOut },
};
Storyboard.SetTarget(opacityAnimation, ColorCopiedNotificationBorder);
Storyboard.SetTargetProperty(opacityAnimation, "Opacity");
storyboard.Children.Add(opacityAnimation);
var heightAnimation = new DoubleAnimation
{
To = height,
Duration = new Duration(TimeSpan.FromMilliseconds(300)),
// Height is a layout property, so the animation must opt in to dependent animation.
EnableDependentAnimation = true,
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseInOut },
};
Storyboard.SetTarget(heightAnimation, ColorCopiedNotificationBorder);
Storyboard.SetTargetProperty(heightAnimation, "Height");
storyboard.Children.Add(heightAnimation);
storyboard.Begin();
}
}
}

View File

@@ -3,159 +3,31 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:ColorPicker.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:p="clr-namespace:ColorPicker.Properties"
AutomationProperties.Name="{x:Static p:Resources.Color_Palette}"
FocusManager.IsFocusScope="True"
KeyboardNavigation.TabNavigation="Once"
TabIndex="3"
mc:Ignorable="d">
<UserControl.Resources>
<Style TargetType="{x:Type Slider}">
<Setter Property="MinWidth" Value="104" />
<Setter Property="MinHeight" Value="21" />
<Setter Property="FocusVisualStyle" Value="{DynamicResource DefaultControlFocusVisualStyle}" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Slider}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" MinHeight="{TemplateBinding MinHeight}" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TickBar
x:Name="TopTick"
Grid.Row="0"
Height="6"
Fill="Transparent"
Placement="Top"
SnapsToDevicePixels="True"
Visibility="Collapsed" />
<Border
x:Name="TrackBackground"
Grid.Row="1"
Height="8"
Margin="0"
Background="{TemplateBinding Background}"
BorderThickness="0"
CornerRadius="4" />
<Track x:Name="PART_Track" Grid.Row="1">
<Track.DecreaseRepeatButton>
<RepeatButton Command="Slider.DecreaseLarge">
<RepeatButton.Style>
<Style TargetType="{x:Type RepeatButton}">
<Setter Property="IsTabStop" Value="False" />
<Setter Property="Focusable" Value="False" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type RepeatButton}">
<Border Background="Transparent" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</RepeatButton.Style>
</RepeatButton>
</Track.DecreaseRepeatButton>
<Track.Thumb>
<Thumb x:Name="Thumb">
<Thumb.Style>
<Style TargetType="{x:Type Thumb}">
<Setter Property="Height" Value="20" />
<Setter Property="Width" Value="20" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="BorderBrush" Value="{DynamicResource ControlElevationBorderBrush}" />
<Setter Property="Foreground" Value="Black" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Background" Value="{DynamicResource SliderOuterThumbBackground}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Thumb}">
<Border
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="16">
<Ellipse
x:Name="Ellipse"
Width="12"
Height="12"
Fill="{TemplateBinding Foreground}"
Stroke="Transparent"
StrokeThickness="0" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Thumb.Style>
</Thumb>
</Track.Thumb>
<Track.IncreaseRepeatButton>
<RepeatButton Command="Slider.IncreaseLarge">
<RepeatButton.Style>
<Style TargetType="{x:Type RepeatButton}">
<Setter Property="IsTabStop" Value="False" />
<Setter Property="Focusable" Value="False" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type RepeatButton}">
<Border Background="Transparent" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</RepeatButton.Style>
</RepeatButton>
</Track.IncreaseRepeatButton>
</Track>
<TickBar
x:Name="BottomTick"
Grid.Row="2"
Height="6"
Fill="Transparent"
Placement="Bottom"
SnapsToDevicePixels="True"
Visibility="Collapsed" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="TickPlacement" Value="TopLeft">
<Setter TargetName="TopTick" Property="Visibility" Value="Visible" />
</Trigger>
<Trigger Property="TickPlacement" Value="BottomRight">
<Setter TargetName="BottomTick" Property="Visibility" Value="Visible" />
</Trigger>
<Trigger Property="TickPlacement" Value="Both">
<Setter TargetName="TopTick" Property="Visibility" Value="Visible" />
<Setter TargetName="BottomTick" Property="Visibility" Value="Visible" />
</Trigger>
<Trigger Property="IsMouseOver" Value="True" />
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!--
Approach A: keep the native WinUI Slider, but make its track transparent so the
gradient Border behind each slider shows through. These keyed brushes override the
default slider track theme brushes for every Slider inside this control.
-->
<SolidColorBrush x:Key="SliderTrackFill" Color="Transparent" />
<SolidColorBrush x:Key="SliderTrackFillPointerOver" Color="Transparent" />
<SolidColorBrush x:Key="SliderTrackFillPressed" Color="Transparent" />
<SolidColorBrush x:Key="SliderTrackFillDisabled" Color="Transparent" />
<SolidColorBrush x:Key="SliderTrackValueFill" Color="Transparent" />
<SolidColorBrush x:Key="SliderTrackValueFillPointerOver" Color="Transparent" />
<SolidColorBrush x:Key="SliderTrackValueFillPressed" Color="Transparent" />
<SolidColorBrush x:Key="SliderTrackValueFillDisabled" Color="Transparent" />
</UserControl.Resources>
<Border
BorderBrush="{DynamicResource SurfaceStrokeColorDefaultBrush}"
BorderBrush="{ThemeResource SurfaceStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="{DynamicResource ControlCornerRadius}">
CornerRadius="{ThemeResource ControlCornerRadius}">
<Grid
x:Name="PickerPanel"
Height="248"
HorizontalAlignment="Stretch"
ClipToBounds="True"
Opacity="1">
HorizontalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="36" />
<RowDefinition Height="36" />
@@ -166,39 +38,30 @@
<Button
x:Name="colorVariation1Button"
Grid.Column="0"
helpers:ControlHelper.CornerRadius="4,4,0,0"
AutomationProperties.Name="{x:Static p:Resources.Lightest_color}"
Grid.Row="0"
Background="LightPink"
Click="ColorVariationButton_Click"
Style="{DynamicResource ColorShadeButtonStyle}"
TabIndex="3"
ToolTipService.ToolTip="{x:Static p:Resources.Select_color}" />
CornerRadius="4,4,0,0"
Style="{StaticResource ColorShadeButtonStyle}" />
<Button
x:Name="colorVariation2Button"
Grid.Row="1"
AutomationProperties.Name="{x:Static p:Resources.Lighter_color}"
Background="LightPink"
Click="ColorVariationButton_Click"
Style="{DynamicResource ColorShadeButtonStyle}"
ToolTipService.ToolTip="{x:Static p:Resources.Select_color}" />
Style="{StaticResource ColorShadeButtonStyle}" />
<Button
x:Name="colorVariation3Button"
Grid.Row="3"
AutomationProperties.Name="{x:Static p:Resources.Darker_color}"
Background="LightPink"
Click="ColorVariationButton_Click"
Style="{DynamicResource ColorShadeButtonStyle}"
ToolTipService.ToolTip="{x:Static p:Resources.Select_color}" />
Style="{StaticResource ColorShadeButtonStyle}" />
<Button
x:Name="colorVariation4Button"
Grid.Row="4"
helpers:ControlHelper.CornerRadius="0,0,4,4"
AutomationProperties.Name="{x:Static p:Resources.Darkest_color}"
Background="LightPink"
Click="ColorVariationButton_Click"
Style="{DynamicResource ColorShadeButtonStyle}"
ToolTipService.ToolTip="{x:Static p:Resources.Select_color}" />
CornerRadius="0,0,4,4"
Style="{StaticResource ColorShadeButtonStyle}" />
<Button
x:Name="CurrentColorButton"
@@ -206,27 +69,15 @@
Height="104"
Margin="0,72,0,72"
VerticalAlignment="Top"
AutomationProperties.HelpText="{x:Static p:Resources.Selected_color_helptext}"
AutomationProperties.Name="{x:Static p:Resources.Selected_color}"
Click="CurrentColorButton_Click"
Style="{DynamicResource ColorShadeButtonStyle}"
ToolTipService.ToolTip="{x:Static p:Resources.Selected_color_tooltip}">
<Popup
x:Name="DetailsFlyout"
Margin="24,0,0,0"
AllowsTransparency="True"
Closed="DetailsFlyout_Closed"
Opened="DetailsFlyout_Opened"
Placement="Top"
StaysOpen="False">
<Border
Padding="15"
Background="{DynamicResource ApplicationBackgroundBrush}"
BorderBrush="{DynamicResource SurfaceStrokeColorFlyoutBrush}"
BorderThickness="1"
CornerRadius="16">
<Grid x:Name="detailsGrid" KeyboardNavigation.TabNavigation="Contained">
Style="{StaticResource ColorShadeButtonStyle}">
<Button.Flyout>
<Flyout
x:Name="DetailsFlyout"
Closed="DetailsFlyout_Closed"
Opened="DetailsFlyout_Opened"
Placement="Top">
<Grid x:Name="detailsGrid" Width="316">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="44" />
<ColumnDefinition Width="86" />
@@ -248,111 +99,128 @@
<TextBlock
VerticalAlignment="Center"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="H"
TextAlignment="Center" />
<Slider
x:Name="HueGradientSlider"
<Grid
Grid.Column="1"
Grid.ColumnSpan="3"
VerticalAlignment="Center"
AutomationProperties.Name="{x:Static p:Resources.Hue_slider}"
IsMoveToPointEnabled="True"
Maximum="289"
Minimum="0"
ValueChanged="HueGradientSlider_ValueChanged" />
VerticalAlignment="Center">
<Border
x:Name="HueGradientBorder"
Height="16"
Margin="8,0"
VerticalAlignment="Center"
CornerRadius="8" />
<Slider
x:Name="HueGradientSlider"
VerticalAlignment="Center"
Maximum="289"
Minimum="0"
ValueChanged="HueGradientSlider_ValueChanged" />
</Grid>
<TextBlock
Grid.Row="2"
VerticalAlignment="Center"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="S"
TextAlignment="Center" />
<Slider
x:Name="SaturationGradientSlider"
<Grid
Grid.Row="2"
Grid.Column="1"
Grid.ColumnSpan="3"
AutomationProperties.Name="{x:Static p:Resources.Saturation_slider}"
IsMoveToPointEnabled="True"
Maximum="289"
Minimum="0"
ValueChanged="SaturationGradientSlider_ValueChanged">
<Slider.Background>
<LinearGradientBrush StartPoint="0, 0.5" EndPoint="1,0.5">
<GradientStop x:Name="SaturationStartColor" Color="Black" />
<GradientStop x:Name="SaturationStopColor" Offset="1" Color="Red" />
</LinearGradientBrush>
</Slider.Background>
</Slider>
VerticalAlignment="Center">
<Border
Height="16"
Margin="8,0"
VerticalAlignment="Center"
CornerRadius="8">
<Border.Background>
<LinearGradientBrush StartPoint="0,0.5" EndPoint="1,0.5">
<GradientStop x:Name="SaturationStartColor" Color="Black" />
<GradientStop x:Name="SaturationStopColor" Offset="1" Color="Red" />
</LinearGradientBrush>
</Border.Background>
</Border>
<Slider
x:Name="SaturationGradientSlider"
VerticalAlignment="Center"
Maximum="289"
Minimum="0"
ValueChanged="SaturationGradientSlider_ValueChanged" />
</Grid>
<TextBlock
Grid.Row="4"
VerticalAlignment="Center"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="V"
TextAlignment="Center" />
<Slider
x:Name="ValueGradientSlider"
<Grid
Grid.Row="4"
Grid.Column="1"
Grid.ColumnSpan="3"
AutomationProperties.Name="{x:Static p:Resources.Value_slider}"
IsMoveToPointEnabled="True"
Maximum="289"
Minimum="0"
ValueChanged="ValueGradientSlider_ValueChanged">
<Slider.Background>
<LinearGradientBrush StartPoint="0, 0.5" EndPoint="1,0.5">
<GradientStop x:Name="ValueStartColor" Color="Black" />
<GradientStop x:Name="ValueStopColor" Offset="1" Color="Red" />
</LinearGradientBrush>
</Slider.Background>
</Slider>
VerticalAlignment="Center">
<Border
Height="16"
Margin="8,0"
VerticalAlignment="Center"
CornerRadius="8">
<Border.Background>
<LinearGradientBrush StartPoint="0,0.5" EndPoint="1,0.5">
<GradientStop x:Name="ValueStartColor" Color="Black" />
<GradientStop x:Name="ValueStopColor" Offset="1" Color="Red" />
</LinearGradientBrush>
</Border.Background>
</Border>
<Slider
x:Name="ValueGradientSlider"
VerticalAlignment="Center"
Maximum="289"
Minimum="0"
ValueChanged="ValueGradientSlider_ValueChanged" />
</Grid>
<TextBlock
Grid.Row="6"
VerticalAlignment="Center"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="RGB"
TextAlignment="Center" />
<TextBox
x:Name="RNumberBox"
Grid.Row="6"
Grid.Column="1"
AutomationProperties.Name="{x:Static p:Resources.Red_value}"
PreviewTextInput="TextBox_PreviewTextInput"
TextBoxBase.TextChanged="RGBNumberBox_TextChanged" />
BeforeTextChanging="NumberBox_BeforeTextChanging"
MaxLength="3"
TextChanged="RGBNumberBox_TextChanged" />
<TextBox
x:Name="GNumberBox"
Grid.Row="6"
Grid.Column="2"
Margin="4,0,0,0"
AutomationProperties.Name="{x:Static p:Resources.Green_value}"
PreviewTextInput="TextBox_PreviewTextInput"
TextBoxBase.TextChanged="RGBNumberBox_TextChanged" />
BeforeTextChanging="NumberBox_BeforeTextChanging"
MaxLength="3"
TextChanged="RGBNumberBox_TextChanged" />
<TextBox
x:Name="BNumberBox"
Grid.Row="6"
Grid.Column="3"
Margin="4,0,0,0"
AutomationProperties.Name="{x:Static p:Resources.Blue_value}"
PreviewTextInput="TextBox_PreviewTextInput"
TextBoxBase.TextChanged="RGBNumberBox_TextChanged" />
BeforeTextChanging="NumberBox_BeforeTextChanging"
MaxLength="3"
TextChanged="RGBNumberBox_TextChanged" />
<TextBlock
Grid.Row="8"
Width="38"
VerticalAlignment="Center"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="HEX"
TextAlignment="Center" />
@@ -362,12 +230,9 @@
Grid.Column="1"
Grid.ColumnSpan="3"
HorizontalAlignment="Stretch"
AutomationProperties.Name="{x:Static p:Resources.Hex_value}"
CharacterCasing="Lower"
GotKeyboardFocus="HexCode_GotKeyboardFocus"
GotFocus="HexCode_GotFocus"
MaxLength="7"
TextChanged="HexCode_TextChanged"
TextWrapping="Wrap" />
TextChanged="HexCode_TextChanged" />
<Button
x:Name="OKButton"
@@ -375,20 +240,12 @@
Grid.ColumnSpan="4"
Margin="0,32,0,0"
HorizontalAlignment="Stretch"
AutomationProperties.Name="{x:Static p:Resources.Select}"
Click="OKButton_Click"
Content="{x:Static p:Resources.Select}"
Style="{StaticResource CustomAccentButtonStyle}" />
Style="{StaticResource AccentButtonStyle}" />
</Grid>
</Border>
</Popup>
</Flyout>
</Button.Flyout>
</Button>
</Grid>
</Border>
</UserControl>

View File

@@ -1,28 +1,30 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Automation.Peers;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using ColorPicker.Helpers;
using ManagedCommon;
using static System.Net.Mime.MediaTypeNames;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Automation.Peers;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Animation;
using Windows.Foundation;
using Windows.UI;
namespace ColorPicker.Controls
{
/// <summary>
/// Interaction logic for ColorPickerControl.xaml
/// </summary>
public partial class ColorPickerControl : UserControl
public sealed partial class ColorPickerControl : UserControl
{
private double _currH = 360;
private double _currS = 1;
@@ -34,15 +36,34 @@ namespace ColorPicker.Controls
private Color _originalColor;
private Color _currentColor;
public static readonly DependencyProperty SelectedColorProperty = DependencyProperty.Register("SelectedColor", typeof(Color), typeof(ColorPickerControl), new PropertyMetadata(SelectedColorPropertyChanged));
public static readonly DependencyProperty SelectedColorProperty = DependencyProperty.Register(nameof(SelectedColor), typeof(Color), typeof(ColorPickerControl), new PropertyMetadata(default(Color), SelectedColorPropertyChanged));
public static readonly DependencyProperty SelectedColorChangeCommandProperty = DependencyProperty.Register("SelectedColorChangedCommand", typeof(ICommand), typeof(ColorPickerControl));
public static readonly DependencyProperty SelectedColorChangeCommandProperty = DependencyProperty.Register(nameof(SelectedColorChangedCommand), typeof(ICommand), typeof(ColorPickerControl), new PropertyMetadata(null));
public ColorPickerControl()
{
InitializeComponent();
UpdateHueGradient(1, 1);
// WPF set these via {x:Static p:Resources.*}; resolve through the resource loader instead.
AutomationProperties.SetName(this, ResourceLoaderInstance.GetString("Color_Palette"));
AutomationProperties.SetName(HueGradientSlider, ResourceLoaderInstance.GetString("Hue_slider"));
AutomationProperties.SetName(SaturationGradientSlider, ResourceLoaderInstance.GetString("Saturation_slider"));
AutomationProperties.SetName(ValueGradientSlider, ResourceLoaderInstance.GetString("Value_slider"));
AutomationProperties.SetName(RNumberBox, ResourceLoaderInstance.GetString("Red_value"));
AutomationProperties.SetName(GNumberBox, ResourceLoaderInstance.GetString("Green_value"));
AutomationProperties.SetName(BNumberBox, ResourceLoaderInstance.GetString("Blue_value"));
AutomationProperties.SetName(HexCode, ResourceLoaderInstance.GetString("Hex_value"));
AutomationProperties.SetName(colorVariation1Button, ResourceLoaderInstance.GetString("Lightest_color"));
AutomationProperties.SetName(colorVariation2Button, ResourceLoaderInstance.GetString("Lighter_color"));
AutomationProperties.SetName(colorVariation3Button, ResourceLoaderInstance.GetString("Darker_color"));
AutomationProperties.SetName(colorVariation4Button, ResourceLoaderInstance.GetString("Darkest_color"));
AutomationProperties.SetName(CurrentColorButton, ResourceLoaderInstance.GetString("Selected_color"));
AutomationProperties.SetHelpText(CurrentColorButton, ResourceLoaderInstance.GetString("Selected_color_helptext"));
AutomationProperties.SetName(OKButton, ResourceLoaderInstance.GetString("Select"));
ToolTipService.SetToolTip(CurrentColorButton, ResourceLoaderInstance.GetString("Selected_color_tooltip"));
OKButton.Content = ResourceLoaderInstance.GetString("Select");
}
protected override AutomationPeer OnCreateAutomationPeer()
@@ -52,14 +73,14 @@ namespace ColorPicker.Controls
public Color SelectedColor
{
get { return (Color)GetValue(SelectedColorProperty); }
set { SetValue(SelectedColorProperty, value); }
get => (Color)GetValue(SelectedColorProperty);
set => SetValue(SelectedColorProperty, value);
}
public ICommand SelectedColorChangedCommand
{
get { return (ICommand)GetValue(SelectedColorChangeCommandProperty); }
set { SetValue(SelectedColorChangeCommandProperty, value); }
get => (ICommand)GetValue(SelectedColorChangeCommandProperty);
set => SetValue(SelectedColorChangeCommandProperty, value);
}
private static void SelectedColorPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
@@ -68,8 +89,7 @@ namespace ColorPicker.Controls
var newColor = (Color)e.NewValue;
control._originalColor = control._currentColor = newColor;
var newColorBackground = new SolidColorBrush(newColor);
control.CurrentColorButton.Background = newColorBackground;
control.CurrentColorButton.Background = new SolidColorBrush(newColor);
control._ignoreHexChanges = true;
control._ignoreRGBChanges = true;
@@ -92,16 +112,17 @@ namespace ColorPicker.Controls
{
var g6 = HSVColor.HueSpectrum(saturation, value);
var gradientBrush = new LinearGradientBrush();
gradientBrush.StartPoint = new Point(0, 0);
gradientBrush.EndPoint = new Point(1, 0);
var gradientBrush = new LinearGradientBrush
{
StartPoint = new Point(0, 0),
EndPoint = new Point(1, 0),
};
for (int i = 0; i < g6.Length; i++)
{
var stop = new GradientStop(g6[i], i * 0.16);
gradientBrush.GradientStops.Add(stop);
gradientBrush.GradientStops.Add(new GradientStop { Color = g6[i], Offset = i * 0.16 });
}
HueGradientSlider.Background = gradientBrush;
HueGradientBorder.Background = gradientBrush;
}
private static void SetColorVariationsForCurrentColor(DependencyObject d, (double Hue, double Saturation, double Value) hsv)
@@ -194,17 +215,11 @@ namespace ColorPicker.Controls
{
_isCollapsed = false;
var resizeColor = new DoubleAnimation(256, new Duration(TimeSpan.FromMilliseconds(250)));
resizeColor.EasingFunction = new ExponentialEase() { EasingMode = EasingMode.EaseInOut };
var moveColor = new ThicknessAnimation(new Thickness(0), new Duration(TimeSpan.FromMilliseconds(250)));
moveColor.EasingFunction = new ExponentialEase() { EasingMode = EasingMode.EaseInOut };
CurrentColorButton.BeginAnimation(System.Windows.Controls.Button.HeightProperty, resizeColor);
CurrentColorButton.BeginAnimation(System.Windows.Controls.Button.MarginProperty, moveColor);
// The flyout opens automatically via Button.Flyout; animate the swatch to its
// expanded pose alongside it.
AnimateCurrentColorButton(256, new Thickness(0), 250);
CurrentColorButton.IsEnabled = false;
SessionEventHelper.Event.EditorAdjustColorOpened = true;
DetailsFlyout.IsOpen = true;
}
}
@@ -214,50 +229,61 @@ namespace ColorPicker.Controls
{
_isCollapsed = true;
var resizeColor = new DoubleAnimation(165, new Duration(TimeSpan.FromMilliseconds(150)));
resizeColor.EasingFunction = new ExponentialEase() { EasingMode = EasingMode.EaseInOut };
var moveColor = new ThicknessAnimation(new Thickness(0, 72, 0, 72), new Duration(TimeSpan.FromMilliseconds(150)));
moveColor.EasingFunction = new ExponentialEase() { EasingMode = EasingMode.EaseInOut };
CurrentColorButton.BeginAnimation(System.Windows.Controls.Button.HeightProperty, resizeColor);
CurrentColorButton.BeginAnimation(System.Windows.Controls.Button.MarginProperty, moveColor);
AnimateCurrentColorButton(165, new Thickness(0, 72, 0, 72), 150);
CurrentColorButton.IsEnabled = true;
}
}
private void AnimateCurrentColorButton(double height, Thickness margin, int durationMs)
{
// WinUI 3 has no ThicknessAnimation, so set the margin directly and animate only the
// (dependent) Height; the swatch still grows/shrinks as the flyout opens/closes.
CurrentColorButton.Margin = margin;
var storyboard = new Storyboard();
var heightAnimation = new DoubleAnimation
{
To = height,
Duration = new Duration(TimeSpan.FromMilliseconds(durationMs)),
EnableDependentAnimation = true,
EasingFunction = new ExponentialEase { EasingMode = EasingMode.EaseInOut },
};
Storyboard.SetTarget(heightAnimation, CurrentColorButton);
Storyboard.SetTargetProperty(heightAnimation, "Height");
storyboard.Children.Add(heightAnimation);
storyboard.Begin();
}
private void OKButton_Click(object sender, RoutedEventArgs e)
{
SelectedColorChangedCommand.Execute(_currentColor);
SessionEventHelper.Event.EditorColorAdjusted = true;
DetailsFlyout.IsOpen = false;
DetailsFlyout.Hide();
}
private void DetailsFlyout_Closed(object sender, object e)
{
HideDetails();
AppStateHandler.BlockEscapeKeyClosingColorPickerEditor = false;
EditorState.BlockEscapeKeyClosingColorPickerEditor = false;
// Revert to original color
var originalColorBackground = new SolidColorBrush(_originalColor);
CurrentColorButton.Background = originalColorBackground;
CurrentColorButton.Background = new SolidColorBrush(_originalColor);
HexCode.Text = ColorToHex(_originalColor);
}
private void DetailsFlyout_Opened(object sender, object e)
{
AppStateHandler.BlockEscapeKeyClosingColorPickerEditor = true;
EditorState.BlockEscapeKeyClosingColorPickerEditor = true;
}
private void ColorVariationButton_Click(object sender, RoutedEventArgs e)
{
var selectedColor = ((SolidColorBrush)((System.Windows.Controls.Button)sender).Background).Color;
var selectedColor = ((SolidColorBrush)((Button)sender).Background).Color;
SelectedColorChangedCommand.Execute(selectedColor);
SessionEventHelper.Event.EditorSimilarColorPicked = true;
}
private void SaturationGradientSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
private void SaturationGradientSlider_ValueChanged(object sender, RangeBaseValueChangedEventArgs e)
{
UpdateSaturationColorGradient((sender as Slider).Value);
_ignoreGradientsChanges = true;
@@ -265,7 +291,7 @@ namespace ColorPicker.Controls
_ignoreGradientsChanges = false;
}
private void HueGradientSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
private void HueGradientSlider_ValueChanged(object sender, RangeBaseValueChangedEventArgs e)
{
UpdateHueColorGradient((sender as Slider).Value);
_ignoreGradientsChanges = true;
@@ -273,7 +299,7 @@ namespace ColorPicker.Controls
_ignoreGradientsChanges = false;
}
private void ValueGradientSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
private void ValueGradientSlider_ValueChanged(object sender, RangeBaseValueChangedEventArgs e)
{
UpdateValueColorGradient((sender as Slider).Value);
_ignoreGradientsChanges = true;
@@ -283,7 +309,7 @@ namespace ColorPicker.Controls
private void HexCode_TextChanged(object sender, TextChangedEventArgs e)
{
var newValue = (sender as System.Windows.Controls.TextBox).Text;
var newValue = (sender as TextBox).Text;
// support hex with 3 and 6 characters and optional with hashtag
var reg = new Regex("^#?([0-9A-Fa-f]{3}){1,2}$");
@@ -319,7 +345,7 @@ namespace ColorPicker.Controls
UpdateValueColorGradient(valuePosition);
}
UpdateTextBoxesAndCurrentColor(Color.FromRgb(color.R, color.G, color.B));
UpdateTextBoxesAndCurrentColor(new Color { A = 255, R = color.R, G = color.G, B = color.B });
}
private static string ColorToHex(Color color, string oldValue = "")
@@ -333,7 +359,8 @@ namespace ColorPicker.Controls
}
/// <summary>
/// Formats the hex code string to be accepted by <see cref="ConvertFromString()"/> of <see cref="ColorConverter.ColorConverter"/>. We are adding hashtag at the beginning if needed and convert from three characters to six characters code.
/// Formats the hex code string to be accepted by the <see cref="System.Drawing.ColorConverter"/>.
/// We add a hashtag at the beginning if needed and convert from three to six characters.
/// </summary>
/// <param name="hexCodeText">The string we read from the hex text box.</param>
/// <returns>Formatted string with hashtag and six characters of hex code.</returns>
@@ -351,14 +378,15 @@ namespace ColorPicker.Controls
}
}
private void HexCode_GotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
private void HexCode_GotFocus(object sender, RoutedEventArgs e)
{
(sender as System.Windows.Controls.TextBox).SelectAll();
(sender as TextBox).SelectAll();
}
private void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
private void NumberBox_BeforeTextChanging(TextBox sender, TextBoxBeforeTextChangingEventArgs args)
{
e.Handled = !System.Text.RegularExpressions.Regex.IsMatch(e.Text, "^[0-9]+$");
// WinUI has no PreviewTextInput; reject any non-digit edit before it is applied.
args.Cancel = !Regex.IsMatch(args.NewText, "^[0-9]*$");
}
private void RGBNumberBox_TextChanged(object sender, TextChangedEventArgs e)
@@ -376,12 +404,6 @@ namespace ColorPicker.Controls
}
}
/// <summary>
/// NumberBox provides value only after it has been validated - happens after pressing enter or leaving this control.
/// However, we need to get value immediately after the underlying textbox value changes
/// </summary>
/// <param name="numberBox">numberBox control which value we want to get</param>
/// <returns>Validated value as per numberbox conditions, if content is invalid it returns previous value</returns>
private static byte GetValueFromNumberBox(TextBox numberBox, byte previousValue)
{
int minimum = 0;
@@ -411,32 +433,10 @@ namespace ColorPicker.Controls
return null;
}
public static T GetChildOfType<T>(DependencyObject depObj)
where T : DependencyObject
{
if (depObj == null)
{
return null;
}
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
var child = VisualTreeHelper.GetChild(depObj, i);
var result = (child as T) ?? GetChildOfType<T>(child);
if (result != null)
{
return result;
}
}
return null;
}
}
#pragma warning disable SA1402 // File may only contain a single type
public class ColorPickerAutomationPeer : UserControlAutomationPeer
public sealed partial class ColorPickerAutomationPeer : FrameworkElementAutomationPeer
#pragma warning restore SA1402 // File may only contain a single type
{
public ColorPickerAutomationPeer(ColorPickerControl owner)
@@ -446,7 +446,7 @@ namespace ColorPicker.Controls
protected override string GetLocalizedControlTypeCore()
{
return ColorPicker.Properties.Resources.Color_Picker_Control;
return ResourceLoaderInstance.GetString("Color_Picker_Control");
}
}
}

View File

@@ -3,23 +3,14 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Windows.Media;
using Windows.UI;
namespace ColorPicker.Controls
{
public static class HSVColor
{
public static Color[] GetSpectrum()
{
var rgbs = new Color[360];
for (int h = 0; h < 360; h++)
{
rgbs[h] = RGBFromHSV(h, 1f, 1f);
}
return rgbs;
}
private static Color FromRgb(byte r, byte g, byte b) => new() { A = 255, R = r, G = g, B = b };
public static Color[] HueSpectrum(double saturation, double value)
{
@@ -37,7 +28,7 @@ namespace ColorPicker.Controls
{
if (h > 360 || h < 0 || s > 1 || s < 0 || v > 1 || v < 0)
{
return Color.FromRgb(0, 0, 0);
return FromRgb(0, 0, 0);
}
double c = v * s;
@@ -77,7 +68,7 @@ namespace ColorPicker.Controls
b = x;
}
return Color.FromRgb((byte)((r + m) * 255), (byte)((g + m) * 255), (byte)((b + m) * 255));
return FromRgb((byte)((r + m) * 255), (byte)((g + m) * 255), (byte)((b + m) * 255));
}
}
}

View File

@@ -1,17 +1,17 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
namespace ColorPicker.Converters
{
public class BoolToInvertedVisibilityConverter : IValueConverter
public partial class BoolToInvertedVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
public object Convert(object value, Type targetType, object parameter, string language)
{
if ((bool)value)
{
@@ -21,7 +21,7 @@ namespace ColorPicker.Converters
return Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}

View File

@@ -1,23 +1,27 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
namespace ColorPicker.Converters
{
public class ColorToStringConverter : IValueConverter
public partial class BoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
public object Convert(object value, Type targetType, object parameter, string language)
{
var color = (Color)value;
return "RGB(" + color.R + "," + color.G + "," + color.B + ")";
if ((bool)value)
{
return Visibility.Visible;
}
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}

View File

@@ -1,25 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;
namespace ColorPicker.Converters
{
public class ColorToBrushConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var color = (Color)value;
return new SolidColorBrush(color);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@@ -1,17 +1,17 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
namespace ColorPicker.Converters
{
public class NumberToInvertedVisibilityConverter : IValueConverter
public partial class NumberToInvertedVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
public object Convert(object value, Type targetType, object parameter, string language)
{
if ((int)value <= 0)
{
@@ -21,7 +21,7 @@ namespace ColorPicker.Converters
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}

View File

@@ -1,17 +1,17 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
namespace ColorPicker.Converters
{
public class NumberToVisibilityConverter : IValueConverter
public partial class NumberToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
public object Convert(object value, Type targetType, object parameter, string language)
{
if ((int)value <= 0)
{
@@ -21,7 +21,7 @@ namespace ColorPicker.Converters
return Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}

View File

@@ -0,0 +1,60 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using ColorPicker.Helpers;
using Microsoft.Extensions.DependencyInjection;
namespace ColorPicker.Foundation
{
/// <summary>
/// Builds the application's dependency-injection container.
/// Sub-projects B (editor), C (overlay), and D (infrastructure) add their
/// own registrations to <see cref="Register"/> as each service is migrated.
/// </summary>
/// <remarks>
/// Public (not internal) so the unit-test project can call <see cref="Configure"/>
/// without depending on an <c>InternalsVisibleTo</c> assembly-name match.
/// </remarks>
public static class AppServices
{
public static IServiceProvider Configure()
{
var services = new ServiceCollection();
Register(services);
return services.BuildServiceProvider();
}
private static void Register(IServiceCollection services)
{
// Replaces the WPF App's [Export] ExitToken: a single source whose
// token is cancelled when the runner exits / the app shuts down.
services.AddSingleton<CancellationTokenSource>();
services.AddSingleton(typeof(CancellationToken), sp =>
(object)sp.GetRequiredService<CancellationTokenSource>().Token);
// D6 infrastructure singletons.
// SINGLETON: replaces the WPF [Export(typeof(IThrottledActionInvoker))]; holds a single
// UI-thread DispatcherQueueTimer. Resolve only on the UI thread (ctor binds the timer).
services.AddSingleton<IThrottledActionInvoker, ThrottledActionInvoker>();
// D7 settings singleton: holds the live settings.json FileSystemWatcher + in-memory
// ColorHistory. SINGLETON, registered AFTER IThrottledActionInvoker (its ctor dep).
services.AddSingleton<ColorPicker.Settings.IUserSettings, ColorPicker.Settings.UserSettings>();
// Sub-project C overlay + editor wiring (7e-1). All singletons; the graph is acyclic:
// MainViewModel -> {MouseInfoProvider, ZoomWindowHelper, AppStateHandler, KeyboardMonitor},
// and AppStateHandler -> IColorEditorViewModel. AppStateHandler reads App.Window, which
// App.OnLaunched assigns before resolving IMainViewModel.
services.AddSingleton<ColorPicker.ViewModelContracts.IColorEditorViewModel, ColorPicker.ViewModels.ColorEditorViewModel>();
services.AddSingleton<AppStateHandler>();
services.AddSingleton<ZoomWindowHelper>();
services.AddSingleton<ColorPicker.Mouse.IMouseInfoProvider, ColorPicker.Mouse.MouseInfoProvider>();
services.AddSingleton<ColorPicker.Keyboard.KeyboardMonitor>();
services.AddSingleton<ColorPicker.ViewModelContracts.IMainViewModel, ColorPicker.ViewModels.MainViewModel>();
}
}
}

View File

@@ -1,41 +1,37 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.ComponentModel.Composition;
using System.Threading;
using System.Windows;
using System.Windows.Interop;
using ColorPicker.Settings;
using ColorPicker.ViewModelContracts;
using Common.UI;
using Microsoft.PowerToys.Settings.UI.Library.Enumerations;
using Microsoft.UI.Xaml;
using static ColorPicker.Helpers.NativeMethodsHelper;
namespace ColorPicker.Helpers
{
[Export(typeof(AppStateHandler))]
public class AppStateHandler
{
private readonly IColorEditorViewModel _colorEditorViewModel;
private readonly IUserSettings _userSettings;
private readonly object _colorPickerVisibilityLock = new object();
private ColorEditorWindow _colorEditorWindow;
private bool _colorPickerShown;
private Lock _colorPickerVisibilityLock = new Lock();
private IntPtr _mainWindowHandle;
private HwndSource _hwndSource;
private const int _globalHotKeyId = 0x0001;
// Blocks using the escape key to close the color picker editor when the adjust color flyout is open.
public static bool BlockEscapeKeyClosingColorPickerEditor { get; set; }
[ImportingConstructor]
public AppStateHandler(IColorEditorViewModel colorEditorViewModel, IUserSettings userSettings)
{
Application.Current.MainWindow.Closed += MainWindow_Closed;
// App.Window (the picking overlay) is created and assigned before the DI graph that
// resolves this handler, so it is available here.
if (App.Window != null)
{
App.Window.Closed += MainWindow_Closed;
}
_colorEditorViewModel = colorEditorViewModel;
_userSettings = userSettings;
}
@@ -71,7 +67,7 @@ namespace ColorPicker.Helpers
ShowColorPicker();
}
if (!(System.Windows.Application.Current as ColorPickerUI.App).IsRunningDetachedFromPowerToys())
if (!((App)Application.Current).IsRunningDetachedFromPowerToys())
{
UserSessionStarted?.Invoke(this, EventArgs.Empty);
}
@@ -93,7 +89,7 @@ namespace ColorPicker.Helpers
HideColorPicker();
}
if (!(System.Windows.Application.Current as ColorPickerUI.App).IsRunningDetachedFromPowerToys())
if (!((App)Application.Current).IsRunningDetachedFromPowerToys())
{
UserSessionEnded?.Invoke(this, EventArgs.Empty);
}
@@ -117,19 +113,12 @@ namespace ColorPicker.Helpers
ShowColorPickerEditor();
}
public static void SetTopMost()
{
Application.Current.MainWindow.Topmost = false;
Application.Current.MainWindow.Topmost = true;
}
private void ShowColorPicker()
{
if (!_colorPickerShown)
{
AppShown?.Invoke(this, EventArgs.Empty);
Application.Current.MainWindow.Opacity = 0;
Application.Current.MainWindow.Visibility = Visibility.Visible;
(App.Window as ColorPickerOverlayWindow)?.Show();
_colorPickerShown = true;
}
}
@@ -138,8 +127,7 @@ namespace ColorPicker.Helpers
{
if (_colorPickerShown)
{
Application.Current.MainWindow.Opacity = 0;
Application.Current.MainWindow.Visibility = Visibility.Collapsed;
(App.Window as ColorPickerOverlayWindow)?.Hide();
AppHidden?.Invoke(this, EventArgs.Empty);
_colorPickerShown = false;
}
@@ -150,7 +138,11 @@ namespace ColorPicker.Helpers
if (_colorEditorWindow == null)
{
_colorEditorWindow = new ColorEditorWindow(this);
_colorEditorWindow.contentPresenter.Content = _colorEditorViewModel;
// The export commands' FileSavePicker needs the editor window's HWND
// (InitializeWithWindow); assign it here, once the window exists.
_colorEditorViewModel.WindowHandle = WinRT.Interop.WindowNative.GetWindowHandle(_colorEditorWindow);
_colorEditorWindow.ContentPresenter.Content = new Views.ColorEditorView { DataContext = _colorEditorViewModel };
_colorEditorViewModel.OpenColorPickerRequested += ColorEditorViewModel_OpenColorPickerRequested;
_colorEditorViewModel.OpenSettingsRequested += ColorEditorViewModel_OpenSettingsRequested;
_colorEditorViewModel.OpenColorPickerRequested += (object sender, EventArgs e) =>
@@ -160,27 +152,19 @@ namespace ColorPicker.Helpers
}
_colorEditorViewModel.Initialize();
_colorEditorWindow.Show();
_colorEditorWindow.AppWindow.Show();
_colorEditorWindow.Activate();
SessionEventHelper.Event.EditorOpened = true;
}
private void HideColorPickerEditor()
{
if (_colorEditorWindow != null)
{
_colorEditorWindow.Hide();
}
_colorEditorWindow?.AppWindow.Hide();
}
public bool IsColorPickerEditorVisible()
{
if (_colorEditorWindow != null)
{
// Check if we are visible and on top. Using focus producing unreliable results the first time the picker is opened.
return _colorEditorWindow.Topmost && _colorEditorWindow.IsVisible;
}
return false;
return _colorEditorWindow != null && _colorEditorWindow.AppWindow.IsVisible;
}
public bool IsColorPickerVisible()
@@ -188,7 +172,7 @@ namespace ColorPicker.Helpers
return _colorPickerShown;
}
private void MainWindow_Closed(object sender, EventArgs e)
private void MainWindow_Closed(object sender, WindowEventArgs e)
{
AppClosed?.Invoke(this, EventArgs.Empty);
}
@@ -200,7 +184,7 @@ namespace ColorPicker.Helpers
ShowColorPicker();
}
_colorEditorWindow.Hide();
_colorEditorWindow?.AppWindow.Hide();
}
private void ColorEditorViewModel_OpenSettingsRequested(object sender, EventArgs e)
@@ -208,9 +192,9 @@ namespace ColorPicker.Helpers
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.ColorPicker);
}
internal void RegisterWindowHandle(System.Windows.Interop.HwndSource hwndSource)
internal void RegisterWindowHandle(IntPtr hwnd)
{
_hwndSource = hwndSource;
_mainWindowHandle = hwnd;
}
public bool HandleEnterPressed()
@@ -226,8 +210,8 @@ namespace ColorPicker.Helpers
public bool HandleEscPressed()
{
if (!BlockEscapeKeyClosingColorPickerEditor
&& (_colorPickerShown || (_colorEditorWindow != null && _colorEditorWindow.IsActive)))
if (!EditorState.BlockEscapeKeyClosingColorPickerEditor
&& (_colorPickerShown || (_colorEditorWindow != null && _colorEditorWindow.IsActiveWindow)))
{
return EndUserSession();
}
@@ -237,13 +221,12 @@ namespace ColorPicker.Helpers
internal void MoveCursor(int xOffset, int yOffset)
{
POINT lpPoint;
GetCursorPos(out lpPoint);
GetCursorPos(out POINT lpPoint);
lpPoint.X += xOffset;
lpPoint.Y += yOffset;
SetCursorPos(lpPoint.X, lpPoint.Y);
}
internal IntPtr GetMainWindowHandle() => _hwndSource?.Handle ?? IntPtr.Zero;
internal IntPtr GetMainWindowHandle() => _mainWindowHandle;
}
}

View File

@@ -4,9 +4,8 @@
using System.Runtime.InteropServices;
using System.Text;
using System.Windows;
using ManagedCommon;
using Windows.ApplicationModel.DataTransfer;
using static ColorPicker.NativeMethods;
@@ -21,35 +20,40 @@ namespace ColorPicker.Helpers
public static void CopyToClipboard(string colorRepresentationToCopy)
{
if (!string.IsNullOrEmpty(colorRepresentationToCopy))
if (string.IsNullOrEmpty(colorRepresentationToCopy))
{
// nasty hack - sometimes clipboard can be in use and it will raise and exception
for (int i = 0; i < 10; i++)
return;
}
// nasty hack - sometimes clipboard can be in use and it will raise and exception
for (int i = 0; i < 10; i++)
{
try
{
try
{
Clipboard.SetDataObject(colorRepresentationToCopy);
break;
}
catch (COMException ex)
{
var hwnd = GetOpenClipboardWindow();
var sb = new StringBuilder(501);
_ = GetWindowText(hwnd.ToInt32(), sb, 500);
var applicationUsingClipboard = sb.ToString();
if ((uint)ex.ErrorCode != ErrorCodeClipboardCantOpen)
{
Logger.LogError("Failed to set text into clipboard", ex);
}
else
{
Logger.LogError("Failed to set text into clipboard, application that is locking clipboard - " + applicationUsingClipboard, ex);
}
}
System.Threading.Thread.Sleep(10);
var data = new DataPackage();
data.SetText(colorRepresentationToCopy);
Clipboard.SetContent(data);
Clipboard.Flush(); // persist after this process exits (Color Picker may close immediately)
break;
}
catch (COMException ex)
{
var hwnd = GetOpenClipboardWindow();
var sb = new StringBuilder(501);
_ = GetWindowText(hwnd.ToInt32(), sb, 500);
var applicationUsingClipboard = sb.ToString();
if ((uint)ex.ErrorCode != ErrorCodeClipboardCantOpen)
{
Logger.LogError("Failed to set text into clipboard", ex);
}
else
{
Logger.LogError("Failed to set text into clipboard, application that is locking clipboard - " + applicationUsingClipboard, ex);
}
}
System.Threading.Thread.Sleep(10);
}
}
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
@@ -6,7 +6,6 @@ using System;
using System.Drawing;
using System.Globalization;
using ColorPicker.Properties;
using ManagedCommon;
namespace ColorPicker.Helpers
@@ -22,7 +21,7 @@ namespace ColorPicker.Helpers
/// <param name="color">The <see cref="Color"/> for the presentation</param>
/// <param name="colorRepresentationType">The type of the representation</param>
/// <returns>A <see cref="string"/> representation of a color</returns>
internal static string GetStringRepresentationFromMediaColor(System.Windows.Media.Color color, string colorRepresentationType)
internal static string GetStringRepresentationFromMediaColor(Windows.UI.Color color, string colorRepresentationType)
{
var drawingcolor = Color.FromArgb(color.A, color.R, color.G, color.B);
return GetStringRepresentation(drawingcolor, colorRepresentationType, string.Empty);
@@ -47,26 +46,6 @@ namespace ColorPicker.Helpers
}
}
/// <summary>
/// Return a <see cref="string"/> representation of a CMYK color
/// </summary>
/// <param name="color">The <see cref="Color"/> for the CMYK color presentation</param>
/// <returns>A <see cref="string"/> representation of a CMYK color</returns>
private static string ColorToCMYK(Color color)
{
var (cyan, magenta, yellow, blackKey) = ColorFormatHelper.ConvertToCMYKColor(color);
cyan = Math.Round(cyan * 100);
magenta = Math.Round(magenta * 100);
yellow = Math.Round(yellow * 100);
blackKey = Math.Round(blackKey * 100);
return $"cmyk({cyan.ToString(CultureInfo.InvariantCulture)}%"
+ $", {magenta.ToString(CultureInfo.InvariantCulture)}%"
+ $", {yellow.ToString(CultureInfo.InvariantCulture)}%"
+ $", {blackKey.ToString(CultureInfo.InvariantCulture)}%)";
}
/// <summary>
/// Return a hexadecimal <see cref="string"/> representation of a RGB color
/// </summary>
@@ -81,288 +60,30 @@ namespace ColorPicker.Helpers
+ $"{color.B.ToString(hexFormat, CultureInfo.InvariantCulture)}";
}
/// <summary>
/// Return a <see cref="string"/> representation of a HSB color
/// </summary>
/// <param name="color">The <see cref="Color"/> for the HSB color presentation</param>
/// <returns>A <see cref="string"/> representation of a HSB color</returns>
private static string ColorToHSB(Color color)
{
var (hue, saturation, brightness) = ColorFormatHelper.ConvertToHSBColor(color);
hue = Math.Round(hue);
saturation = Math.Round(saturation * 100);
brightness = Math.Round(brightness * 100);
return $"hsb({hue.ToString(CultureInfo.InvariantCulture)}"
+ $", {saturation.ToString(CultureInfo.InvariantCulture)}%"
+ $", {brightness.ToString(CultureInfo.InvariantCulture)}%)";
}
/// <summary>
/// Return a <see cref="string"/> representation float color styling(0.1f, 0.1f, 0.1f)
/// </summary>
/// <param name="color">The <see cref="Color"/> to convert</param>
/// <returns>a string value (0.1f, 0.1f, 0.1f)</returns>
private static string ColorToFloat(Color color)
{
var (red, green, blue) = ColorHelper.ConvertToDouble(color);
const int precision = 2;
const string floatFormat = "0.##";
return $"({Math.Round(red, precision).ToString(floatFormat, CultureInfo.InvariantCulture)}f"
+ $", {Math.Round(green, precision).ToString(floatFormat, CultureInfo.InvariantCulture)}f"
+ $", {Math.Round(blue, precision).ToString(floatFormat, CultureInfo.InvariantCulture)}f, 1f)";
}
/// <summary>
/// Return a <see cref="string"/> representation decimal color value
/// </summary>
/// <param name="color">The <see cref="Color"/> to convert</param>
/// <returns>a string value number</returns>
private static string ColorToDecimal(Color color)
{
return $"{(color.R * 65536) + (color.G * 256) + color.B}";
}
/// <summary>
/// Return a <see cref="string"/> representation of a HSI color
/// </summary>
/// <param name="color">The <see cref="Color"/> for the HSI color presentation</param>
/// <returns>A <see cref="string"/> representation of a HSI color</returns>
private static string ColorToHSI(Color color)
{
var (hue, saturation, intensity) = ColorFormatHelper.ConvertToHSIColor(color);
hue = Math.Round(hue);
saturation = Math.Round(saturation * 100);
intensity = Math.Round(intensity * 100);
return $"hsi({hue.ToString(CultureInfo.InvariantCulture)}"
+ $", {saturation.ToString(CultureInfo.InvariantCulture)}%"
+ $", {intensity.ToString(CultureInfo.InvariantCulture)}%)";
}
/// <summary>
/// Return a <see cref="string"/> representation of a HSL color
/// </summary>
/// <param name="color">The <see cref="Color"/> for the HSL color presentation</param>
/// <returns>A <see cref="string"/> representation of a HSL color</returns>
private static string ColorToHSL(Color color)
{
var (hue, saturation, lightness) = ColorFormatHelper.ConvertToHSLColor(color);
hue = Math.Round(hue);
saturation = Math.Round(saturation * 100);
lightness = Math.Round(lightness * 100);
// Using InvariantCulture since this is used for color representation
return $"hsl({hue.ToString(CultureInfo.InvariantCulture)}"
+ $", {saturation.ToString(CultureInfo.InvariantCulture)}%"
+ $", {lightness.ToString(CultureInfo.InvariantCulture)}%)";
}
/// <summary>
/// Return a <see cref="string"/> representation of a HSV color
/// </summary>
/// <param name="color">The <see cref="Color"/> for the HSV color presentation</param>
/// <returns>A <see cref="string"/> representation of a HSV color</returns>
private static string ColorToHSV(Color color)
{
var (hue, saturation, value) = ColorFormatHelper.ConvertToHSVColor(color);
hue = Math.Round(hue);
saturation = Math.Round(saturation * 100);
value = Math.Round(value * 100);
// Using InvariantCulture since this is used for color representation
return $"hsv({hue.ToString(CultureInfo.InvariantCulture)}"
+ $", {saturation.ToString(CultureInfo.InvariantCulture)}%"
+ $", {value.ToString(CultureInfo.InvariantCulture)}%)";
}
/// <summary>
/// Return a <see cref="string"/> representation of a HWB color
/// </summary>
/// <param name="color">The <see cref="Color"/> for the HWB color presentation</param>
/// <returns>A <see cref="string"/> representation of a HWB color</returns>
private static string ColorToHWB(Color color)
{
var (hue, whiteness, blackness) = ColorFormatHelper.ConvertToHWBColor(color);
hue = Math.Round(hue);
whiteness = Math.Round(whiteness * 100);
blackness = Math.Round(blackness * 100);
return $"hwb({hue.ToString(CultureInfo.InvariantCulture)}"
+ $", {whiteness.ToString(CultureInfo.InvariantCulture)}%"
+ $", {blackness.ToString(CultureInfo.InvariantCulture)}%)";
}
/// <summary>
/// Return a <see cref="string"/> representation of a natural color
/// </summary>
/// <param name="color">The <see cref="Color"/> for the natural color presentation</param>
/// <returns>A <see cref="string"/> representation of a natural color</returns>
private static string ColorToNCol(Color color)
{
var (hue, whiteness, blackness) = ColorFormatHelper.ConvertToNaturalColor(color);
whiteness = Math.Round(whiteness * 100);
blackness = Math.Round(blackness * 100);
return $"{hue}"
+ $", {whiteness.ToString(CultureInfo.InvariantCulture)}%"
+ $", {blackness.ToString(CultureInfo.InvariantCulture)}%";
}
/// <summary>
/// Return a <see cref="string"/> representation of a RGB color
/// </summary>
/// <param name="color">The <see cref="Color"/> for the RGB color presentation</param>
/// <returns>A <see cref="string"/> representation of a RGB color</returns>
private static string ColorToRGB(Color color)
=> $"rgb({color.R.ToString(CultureInfo.InvariantCulture)}"
+ $", {color.G.ToString(CultureInfo.InvariantCulture)}"
+ $", {color.B.ToString(CultureInfo.InvariantCulture)})";
/// <summary>
/// Returns a <see cref="string"/> representation of a CIE LAB color
/// </summary>
/// <param name="color">The <see cref="Color"/> for the CIE LAB color presentation</param>
/// <returns>A <see cref="string"/> representation of a CIE LAB color</returns>
private static string ColorToCIELAB(Color color)
{
var (lightness, chromaticityA, chromaticityB) = ColorFormatHelper.ConvertToCIELABColor(color);
lightness = Math.Round(lightness, 2);
chromaticityA = Math.Round(chromaticityA, 2);
chromaticityB = Math.Round(chromaticityB, 2);
return $"CIELab({lightness.ToString(CultureInfo.InvariantCulture)}" +
$", {chromaticityA.ToString(CultureInfo.InvariantCulture)}" +
$", {chromaticityB.ToString(CultureInfo.InvariantCulture)})";
}
/// <summary>
/// Returns a <see cref="string"/> representation of a Oklab color
/// </summary>
/// <param name="color">The <see cref="Color"/> for the Oklab color presentation</param>
/// <returns>A <see cref="string"/> representation of a Oklab color</returns>
private static string ColorToOklab(Color color)
{
var (lightness, chromaticityA, chromaticityB) = ColorFormatHelper.ConvertToOklabColor(color);
lightness = Math.Round(lightness, 2);
chromaticityA = Math.Round(chromaticityA, 2);
chromaticityB = Math.Round(chromaticityB, 2);
return $"oklab({lightness.ToString(CultureInfo.InvariantCulture)}" +
$", {chromaticityA.ToString(CultureInfo.InvariantCulture)}" +
$", {chromaticityB.ToString(CultureInfo.InvariantCulture)})";
}
/// <summary>
/// Returns a <see cref="string"/> representation of a CIE LCh color
/// </summary>
/// <param name="color">The <see cref="Color"/> for the CIE LCh color presentation</param>
/// <returns>A <see cref="string"/> representation of a CIE LCh color</returns>
private static string ColorToOklch(Color color)
{
var (lightness, chroma, hue) = ColorFormatHelper.ConvertToOklchColor(color);
lightness = Math.Round(lightness, 2);
chroma = Math.Round(chroma, 2);
hue = Math.Round(hue, 2);
return $"oklch({lightness.ToString(CultureInfo.InvariantCulture)}" +
$", {chroma.ToString(CultureInfo.InvariantCulture)}" +
$", {hue.ToString(CultureInfo.InvariantCulture)})";
}
/// <summary>
/// Returns a <see cref="string"/> representation of a CIE XYZ color
/// </summary>
/// <param name="color">The <see cref="Color"/> for the CIE XYZ color presentation</param>
/// <returns>A <see cref="string"/> representation of a CIE XYZ color</returns>
private static string ColorToCIEXYZ(Color color)
{
var (x, y, z) = ColorFormatHelper.ConvertToCIEXYZColor(color);
x = Math.Round(x * 100, 4);
y = Math.Round(y * 100, 4);
z = Math.Round(z * 100, 4);
return $"XYZ({x.ToString(CultureInfo.InvariantCulture)}" +
$", {y.ToString(CultureInfo.InvariantCulture)}" +
$", {z.ToString(CultureInfo.InvariantCulture)})";
}
/// <summary>
/// Return a hexadecimal integer <see cref="string"/> representation of a RGB color
/// </summary>
/// <param name="color">The <see cref="Color"/> for the hexadecimal integer presentation</param>
/// <returns>A hexadecimal integer <see cref="string"/> representation of a RGB color</returns>
private static string ColorToHexInteger(Color color)
{
const string hexFormat = "X2";
return "0xFF"
+ $"{color.R.ToString(hexFormat, CultureInfo.InvariantCulture)}"
+ $"{color.G.ToString(hexFormat, CultureInfo.InvariantCulture)}"
+ $"{color.B.ToString(hexFormat, CultureInfo.InvariantCulture)}";
}
public static string GetColorNameFromColorIdentifier(string colorIdentifier)
{
switch (colorIdentifier)
{
case "TEXT_COLOR_WHITE": return Resources.TEXT_COLOR_WHITE;
case "TEXT_COLOR_BLACK": return Resources.TEXT_COLOR_BLACK;
case "TEXT_COLOR_LIGHTGRAY": return Resources.TEXT_COLOR_LIGHTGRAY;
case "TEXT_COLOR_GRAY": return Resources.TEXT_COLOR_GRAY;
case "TEXT_COLOR_DARKGRAY": return Resources.TEXT_COLOR_DARKGRAY;
case "TEXT_COLOR_CORAL": return Resources.TEXT_COLOR_CORAL;
case "TEXT_COLOR_ROSE": return Resources.TEXT_COLOR_ROSE;
case "TEXT_COLOR_LIGHTORANGE": return Resources.TEXT_COLOR_LIGHTORANGE;
case "TEXT_COLOR_TAN": return Resources.TEXT_COLOR_TAN;
case "TEXT_COLOR_LIGHTYELLOW": return Resources.TEXT_COLOR_LIGHTYELLOW;
case "TEXT_COLOR_LIGHTGREEN": return Resources.TEXT_COLOR_LIGHTGREEN;
case "TEXT_COLOR_LIME": return Resources.TEXT_COLOR_LIME;
case "TEXT_COLOR_AQUA": return Resources.TEXT_COLOR_AQUA;
case "TEXT_COLOR_SKYBLUE": return Resources.TEXT_COLOR_SKYBLUE;
case "TEXT_COLOR_LIGHTTURQUOISE": return Resources.TEXT_COLOR_LIGHTTURQUOISE;
case "TEXT_COLOR_PALEBLUE": return Resources.TEXT_COLOR_PALEBLUE;
case "TEXT_COLOR_LIGHTBLUE": return Resources.TEXT_COLOR_LIGHTBLUE;
case "TEXT_COLOR_ICEBLUE": return Resources.TEXT_COLOR_ICEBLUE;
case "TEXT_COLOR_PERIWINKLE": return Resources.TEXT_COLOR_PERIWINKLE;
case "TEXT_COLOR_LAVENDER": return Resources.TEXT_COLOR_LAVENDER;
case "TEXT_COLOR_PINK": return Resources.TEXT_COLOR_PINK;
case "TEXT_COLOR_RED": return Resources.TEXT_COLOR_RED;
case "TEXT_COLOR_ORANGE": return Resources.TEXT_COLOR_ORANGE;
case "TEXT_COLOR_BROWN": return Resources.TEXT_COLOR_BROWN;
case "TEXT_COLOR_GOLD": return Resources.TEXT_COLOR_GOLD;
case "TEXT_COLOR_YELLOW": return Resources.TEXT_COLOR_YELLOW;
case "TEXT_COLOR_OLIVEGREEN": return Resources.TEXT_COLOR_OLIVEGREEN;
case "TEXT_COLOR_GREEN": return Resources.TEXT_COLOR_GREEN;
case "TEXT_COLOR_BRIGHTGREEN": return Resources.TEXT_COLOR_BRIGHTGREEN;
case "TEXT_COLOR_TEAL": return Resources.TEXT_COLOR_TEAL;
case "TEXT_COLOR_TURQUOISE": return Resources.TEXT_COLOR_TURQUOISE;
case "TEXT_COLOR_BLUE": return Resources.TEXT_COLOR_BLUE;
case "TEXT_COLOR_BLUEGRAY": return Resources.TEXT_COLOR_BLUEGRAY;
case "TEXT_COLOR_INDIGO": return Resources.TEXT_COLOR_INDIGO;
case "TEXT_COLOR_PURPLE": return Resources.TEXT_COLOR_PURPLE;
case "TEXT_COLOR_DARKRED": return Resources.TEXT_COLOR_DARKRED;
case "TEXT_COLOR_DARKYELLOW": return Resources.TEXT_COLOR_DARKYELLOW;
case "TEXT_COLOR_DARKGREEN": return Resources.TEXT_COLOR_DARKGREEN;
case "TEXT_COLOR_DARKTEAL": return Resources.TEXT_COLOR_DARKTEAL;
case "TEXT_COLOR_DARKBLUE": return Resources.TEXT_COLOR_DARKBLUE;
case "TEXT_COLOR_DARKPURPLE": return Resources.TEXT_COLOR_DARKPURPLE;
case "TEXT_COLOR_PLUM": return Resources.TEXT_COLOR_PLUM;
default: return string.Empty;
}
// The color-name identifiers (e.g. "TEXT_COLOR_BLACK") are the .resw resource keys
// verbatim, so resolve them directly through the resource loader. This replaces the
// WPF-era switch over the generated Resources designer, which no longer exists after
// the WinUI 3 migration moved the string table from Properties\Resources.resx to
// Strings\en-us\Resources.resw.
return ResourceLoaderInstance.GetString(colorIdentifier);
}
public static string ReplaceName(string colorFormat, Color color)
{
return colorFormat.Replace(ColorFormatHelper.GetColorNameParameter(), GetColorNameFromColorIdentifier(ColorNameHelper.GetColorNameIdentifier(color)));
var colorNameParameter = ColorFormatHelper.GetColorNameParameter();
// Only resolve the (localized, resource-backed) color name when the format string
// actually contains the name placeholder. This skips a resource lookup for the common
// numeric formats and keeps pure-conversion unit tests from requiring the MRT resource
// loader (and the module .pri) to be initialized in the test host.
if (!colorFormat.Contains(colorNameParameter, StringComparison.Ordinal))
{
return colorFormat;
}
return colorFormat.Replace(colorNameParameter, GetColorNameFromColorIdentifier(ColorNameHelper.GetColorNameIdentifier(color)));
}
}
}

View File

@@ -1,317 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace ColorPicker.Helpers
{
public static class ControlHelper
{
/// <summary>
/// Gets the radius for the corners of the control's border.
/// </summary>
/// <param name="control">The element from which to read the property value.</param>
/// <returns>
/// The degree to which the corners are rounded, expressed as values of the CornerRadius
/// structure.
/// </returns>
public static CornerRadius GetCornerRadius(Control control)
{
return (CornerRadius)control.GetValue(CornerRadiusProperty);
}
/// <summary>
/// Sets the radius for the corners of the control's border.
/// </summary>
/// <param name="control">The element on which to set the attached property.</param>
/// <param name="value">The property value to set.</param>
public static void SetCornerRadius(Control control, CornerRadius value)
{
control.SetValue(CornerRadiusProperty, value);
}
/// <summary>
/// Identifies the CornerRadius dependency property.
/// </summary>
public static readonly DependencyProperty CornerRadiusProperty =
DependencyProperty.RegisterAttached(
"CornerRadius",
typeof(CornerRadius),
typeof(ControlHelper),
null);
/// <summary>
/// Identifies the Header dependency property.
/// </summary>
public static readonly DependencyProperty HeaderProperty =
DependencyProperty.RegisterAttached(
"Header",
typeof(object),
typeof(ControlHelper),
new FrameworkPropertyMetadata(OnHeaderChanged));
/// <summary>
/// Gets the content for the control's header.
/// </summary>
/// <param name="control">The element from which to read the property value.</param>
/// <returns>The content of the control's header. The default is **null**.</returns>
public static object GetHeader(Control control)
{
return control.GetValue(HeaderProperty);
}
/// <summary>
/// Sets the content for the control's header.
/// </summary>
/// <param name="control">The element on which to set the attached property.</param>
/// <param name="value">The property value to set.</param>
public static void SetHeader(Control control, object value)
{
control.SetValue(HeaderProperty, value);
}
private static void OnHeaderChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
UpdateHeaderVisibility((Control)d);
}
/// <summary>
/// Identifies the HeaderTemplate dependency property.
/// </summary>
public static readonly DependencyProperty HeaderTemplateProperty =
DependencyProperty.RegisterAttached(
"HeaderTemplate",
typeof(DataTemplate),
typeof(ControlHelper),
new FrameworkPropertyMetadata(OnHeaderTemplateChanged));
/// <summary>
/// Gets the DataTemplate used to display the content of the control's header.
/// </summary>
/// <param name="control">The element from which to read the property value.</param>
/// <returns>
/// The template that specifies the visualization of the header object. The default
/// is **null**.
/// </returns>
public static DataTemplate GetHeaderTemplate(Control control)
{
return (DataTemplate)control.GetValue(HeaderTemplateProperty);
}
/// <summary>
/// Sets the DataTemplate used to display the content of the control's header.
/// </summary>
/// <param name="control">The element on which to set the attached property.</param>
/// <param name="value">The property value to set.</param>
public static void SetHeaderTemplate(Control control, DataTemplate value)
{
control.SetValue(HeaderTemplateProperty, value);
}
private static void OnHeaderTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
UpdateHeaderVisibility((Control)d);
}
private static readonly DependencyPropertyKey HeaderVisibilityPropertyKey =
DependencyProperty.RegisterAttachedReadOnly(
"HeaderVisibility",
typeof(Visibility),
typeof(ControlHelper),
new FrameworkPropertyMetadata(Visibility.Collapsed));
public static readonly DependencyProperty HeaderVisibilityProperty =
HeaderVisibilityPropertyKey.DependencyProperty;
public static Visibility GetHeaderVisibility(Control control)
{
return (Visibility)control.GetValue(HeaderVisibilityProperty);
}
private static void SetHeaderVisibility(Control control, Visibility value)
{
control.SetValue(HeaderVisibilityPropertyKey, value);
}
private static void UpdateHeaderVisibility(Control control)
{
Visibility visibility;
if (GetHeaderTemplate(control) != null)
{
visibility = Visibility.Visible;
}
else
{
visibility = IsNullOrEmptyString(GetHeader(control)) ? Visibility.Collapsed : Visibility.Visible;
}
SetHeaderVisibility(control, visibility);
}
/// <summary>
/// Gets the text that is displayed in the control until the value is changed
/// by a user action or some other operation.
/// </summary>
/// <param name="control">The element from which to read the property value.</param>
/// <returns>
/// The text that is displayed in the control when no value is entered. The default
/// is an empty string ("").
/// </returns>
public static string GetPlaceholderText(Control control)
{
return (string)control.GetValue(PlaceholderTextProperty);
}
/// <summary>
/// Sets the text that is displayed in the control until the value is changed
/// by a user action or some other operation.
/// </summary>
/// <param name="control">The element on which to set the attached property.</param>
/// <param name="value">The property value to set.</param>
public static void SetPlaceholderText(Control control, string value)
{
control.SetValue(PlaceholderTextProperty, value);
}
/// <summary>
/// Identifies the PlaceholderText dependency property.
/// </summary>
public static readonly DependencyProperty PlaceholderTextProperty =
DependencyProperty.RegisterAttached(
"PlaceholderText",
typeof(string),
typeof(ControlHelper),
new FrameworkPropertyMetadata(string.Empty, OnPlaceholderTextChanged));
private static void OnPlaceholderTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
UpdatePlaceholderTextVisibility((Control)d);
}
public static Visibility GetPlaceholderTextVisibility(Control control)
{
return (Visibility)control.GetValue(PlaceholderTextVisibilityProperty);
}
private static void SetPlaceholderTextVisibility(Control control, Visibility value)
{
control.SetValue(PlaceholderTextVisibilityPropertyKey, value);
}
private static readonly DependencyPropertyKey PlaceholderTextVisibilityPropertyKey =
DependencyProperty.RegisterAttachedReadOnly(
"PlaceholderTextVisibility",
typeof(Visibility),
typeof(ControlHelper),
new FrameworkPropertyMetadata(Visibility.Collapsed));
public static readonly DependencyProperty PlaceholderTextVisibilityProperty =
PlaceholderTextVisibilityPropertyKey.DependencyProperty;
private static void UpdatePlaceholderTextVisibility(Control control)
{
SetPlaceholderTextVisibility(control, string.IsNullOrEmpty(GetPlaceholderText(control)) ? Visibility.Collapsed : Visibility.Visible);
}
/// <summary>
/// Gets a brush that describes the color of placeholder text.
/// </summary>
/// <param name="control">The element from which to read the property value.</param>
/// <returns>The brush that describes the color of placeholder text.</returns>
public static Brush GetPlaceholderForeground(Control control)
{
return (Brush)control.GetValue(PlaceholderForegroundProperty);
}
/// <summary>
/// Sets a brush that describes the color of placeholder text.
/// </summary>
/// <param name="control">The element on which to set the attached property.</param>
/// <param name="value">The property value to set.</param>
public static void SetPlaceholderForeground(Control control, Brush value)
{
control.SetValue(PlaceholderForegroundProperty, value);
}
/// <summary>
/// Identifies the PlaceholderForeground dependency property.
/// </summary>
public static readonly DependencyProperty PlaceholderForegroundProperty =
DependencyProperty.RegisterAttached(
"PlaceholderForeground",
typeof(Brush),
typeof(ControlHelper),
null);
/// <summary>
/// Gets content that is shown below the control. The content should provide
/// guidance about the input expected by the control.
/// </summary>
/// <param name="control">The element from which to read the property value.</param>
/// <returns>The content to be displayed below the control. The default is **null**.</returns>
public static object GetDescription(Control control)
{
return control.GetValue(DescriptionProperty);
}
/// <summary>
/// Sets content that is shown below the control. The content should provide
/// guidance about the input expected by the control.
/// </summary>
/// <param name="control">The element on which to set the attached property.</param>
/// <param name="value">The property value to set.</param>
public static void SetDescription(Control control, object value)
{
control.SetValue(DescriptionProperty, value);
}
/// <summary>
/// Identifies the Description dependency property.
/// </summary>
public static readonly DependencyProperty DescriptionProperty =
DependencyProperty.RegisterAttached(
"Description",
typeof(object),
typeof(ControlHelper),
new FrameworkPropertyMetadata(OnDescriptionChanged));
private static void OnDescriptionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
UpdateDescriptionVisibility((Control)d);
}
private static readonly DependencyPropertyKey DescriptionVisibilityPropertyKey =
DependencyProperty.RegisterAttachedReadOnly(
"DescriptionVisibility",
typeof(Visibility),
typeof(ControlHelper),
new FrameworkPropertyMetadata(Visibility.Collapsed));
public static readonly DependencyProperty DescriptionVisibilityProperty =
DescriptionVisibilityPropertyKey.DependencyProperty;
public static Visibility GetDescriptionVisibility(Control control)
{
return (Visibility)control.GetValue(DescriptionVisibilityProperty);
}
private static void SetDescriptionVisibility(Control control, Visibility value)
{
control.SetValue(DescriptionVisibilityPropertyKey, value);
}
private static void UpdateDescriptionVisibility(Control control)
{
SetDescriptionVisibility(control, IsNullOrEmptyString(GetDescription(control)) ? Visibility.Collapsed : Visibility.Visible);
}
internal static bool IsNullOrEmptyString(object obj)
{
return obj == null || (obj is string s && string.IsNullOrEmpty(s));
}
}
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace ColorPicker.Helpers
{
/// <summary>
/// Cross-cutting color-editor UI state shared between the color picker control (which sets
/// the flag while the adjust-color flyout is open) and the global Esc handling in
/// AppStateHandler (which reads it). Kept in its own static holder so the control does not
/// depend on the window-coupled AppStateHandler.
/// </summary>
internal static class EditorState
{
/// <summary>
/// Gets or sets a value indicating whether the Escape key should be blocked from
/// closing the color picker editor (true while the adjust-color flyout is open).
/// </summary>
public static bool BlockEscapeKeyClosingColorPickerEditor { get; set; }
}
}

View File

@@ -7,8 +7,7 @@ using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Media;
using Windows.Foundation;
using static ColorPicker.NativeMethods;
@@ -22,12 +21,12 @@ namespace ColorPicker.Helpers
{
var info = new MonitorInfoEx();
GetMonitorInfo(new HandleRef(null, monitor), info);
Bounds = new System.Windows.Rect(
Bounds = new Windows.Foundation.Rect(
info.rcMonitor.left,
info.rcMonitor.top,
info.rcMonitor.right - info.rcMonitor.left,
info.rcMonitor.bottom - info.rcMonitor.top);
WorkingArea = new System.Windows.Rect(
WorkingArea = new Windows.Foundation.Rect(
info.rcWork.left,
info.rcWork.top,
info.rcWork.right - info.rcWork.left,
@@ -36,11 +35,6 @@ namespace ColorPicker.Helpers
Name = new string(info.szDevice).TrimEnd((char)0);
}
public static DpiScale GetCurrentMonitorDpi()
{
return VisualTreeHelper.GetDpi(Application.Current.MainWindow);
}
public static IEnumerable<MonitorResolutionHelper> AllMonitors
{
get
@@ -52,9 +46,9 @@ namespace ColorPicker.Helpers
}
}
public System.Windows.Rect Bounds { get; private set; }
public Windows.Foundation.Rect Bounds { get; private set; }
public System.Windows.Rect WorkingArea { get; private set; }
public Windows.Foundation.Rect WorkingArea { get; private set; }
public string Name { get; private set; }

View File

@@ -0,0 +1,85 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using ManagedCommon;
using Microsoft.UI.Dispatching;
namespace ColorPicker.Helpers
{
/// <summary>
/// WinUI 3 replacement for <c>Common.UI.NativeEventWaiter</c>. Waits on a Windows
/// named <see cref="EventWaitHandle"/> on a background thread and marshals the
/// callback onto the captured UI-thread <see cref="DispatcherQueue"/> via
/// <see cref="DispatcherQueue.TryEnqueue(DispatcherQueueHandler)"/>.
/// </summary>
/// <remarks>
/// The Common.UI original uses <c>System.Windows.Threading.Dispatcher.BeginInvoke</c>,
/// which has no WinUI 3 equivalent. This copy keeps the original
/// <see cref="WaitHandle.WaitAny(WaitHandle[])"/> shape so the worker wakes the instant
/// <paramref name="cancel"/> is signalled (ColorPicker's ExitToken is cancelled on runner
/// exit / shutdown), rather than polling.
/// </remarks>
public static class NativeEventWaiter
{
/// <summary>
/// Spawns a background worker that invokes <paramref name="callback"/> on the current
/// thread's <see cref="DispatcherQueue"/> whenever the named event fires, and exits when
/// <paramref name="cancel"/> is signalled. MUST be called from a thread that owns a
/// <see cref="DispatcherQueue"/> (the WinUI UI thread); otherwise the registration is skipped.
/// </summary>
public static void WaitForEventLoop(string eventName, Action callback, CancellationToken cancel)
{
var dispatcherQueue = DispatcherQueue.GetForCurrentThread();
if (dispatcherQueue == null)
{
Logger.LogError($"[NativeEventWaiter] No DispatcherQueue on the calling thread for event: {eventName}. Call from the UI thread.");
return;
}
var thread = new Thread(() =>
{
try
{
using var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, eventName);
var handles = new WaitHandle[] { cancel.WaitHandle, eventHandle };
while (true)
{
// Index 0 == cancel (exit immediately); index 1 == event signalled.
if (WaitHandle.WaitAny(handles) == 1)
{
dispatcherQueue.TryEnqueue(() =>
{
try
{
callback();
}
catch (Exception callbackEx)
{
Logger.LogError($"[NativeEventWaiter] Callback failed for event {eventName}: {callbackEx.Message}");
}
});
}
else
{
return;
}
}
}
catch (Exception ex)
{
Logger.LogError($"[NativeEventWaiter] Event loop failed for {eventName}: {ex.Message}");
}
})
{
IsBackground = true,
Name = $"NativeEventWaiter_{eventName}",
};
thread.Start();
}
}
}

View File

@@ -3,7 +3,7 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using System.Windows;
using Windows.Foundation;
namespace ColorPicker.Helpers
{

View File

@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.Windows.ApplicationModel.Resources;
namespace ColorPicker.Helpers
{
internal static class ResourceLoaderInstance
{
private static Func<string, string> _getString;
internal static Func<string, string> GetString
{
get => _getString ??= CreateDefault();
set => _getString = value;
}
private static Func<string, string> CreateDefault()
{
var loader = new ResourceLoader("PowerToys.ColorPickerUI.pri");
return loader.GetString;
}
}
}

View File

@@ -8,10 +8,9 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Windows.Media;
using ColorPicker.Models;
using Windows.UI;
namespace ColorPicker.Helpers
{

View File

@@ -1,25 +1,29 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.ComponentModel.Composition;
using System.Threading;
using System.Windows.Threading;
using Microsoft.UI.Dispatching;
namespace ColorPicker.Helpers
{
[Export(typeof(IThrottledActionInvoker))]
// MEF [Export] removed; registered in AppServices.Register.
public sealed class ThrottledActionInvoker : IThrottledActionInvoker
{
private readonly DispatcherQueueTimer _timer;
private Lock _invokerLock = new Lock();
private Action _actionToRun;
private DispatcherTimer _timer;
public ThrottledActionInvoker()
{
_timer = new DispatcherTimer();
// Must be constructed on the UI thread so it binds to the app's DispatcherQueue.
var queue = DispatcherQueue.GetForCurrentThread()
?? throw new InvalidOperationException("ThrottledActionInvoker must be created on a thread with a DispatcherQueue (the UI thread).");
_timer = queue.CreateTimer();
_timer.IsRepeating = false; // one-shot debounce: DispatcherQueueTimer repeats by default.
_timer.Tick += Timer_Tick;
}
@@ -27,19 +31,19 @@ namespace ColorPicker.Helpers
{
lock (_invokerLock)
{
if (_timer.IsEnabled)
if (_timer.IsRunning)
{
_timer.Stop();
}
_actionToRun = action;
_timer.Interval = new TimeSpan(0, 0, 0, 0, milliseconds);
_timer.Interval = TimeSpan.FromMilliseconds(milliseconds);
_timer.Start();
}
}
private void Timer_Tick(object sender, EventArgs e)
private void Timer_Tick(DispatcherQueueTimer sender, object e)
{
lock (_invokerLock)
{

View File

@@ -3,45 +3,59 @@
// See the LICENSE file in the project root for more information.
using System;
using System.ComponentModel.Composition;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Windows.Media.Imaging;
using System.Runtime.InteropServices;
using ColorPicker.ViewModelContracts;
using Microsoft.Graphics.Canvas;
using Windows.Graphics;
using Windows.Graphics.DirectX;
using WinUIEx;
using Point = Windows.Foundation.Point;
namespace ColorPicker.Helpers
{
[Export(typeof(ZoomWindowHelper))]
/// <summary>
/// Drives the zoom magnifier: captures a small screen region around the cursor with GDI,
/// hands it to the Win2D-backed <see cref="Views.ZoomView"/> (via <see cref="ZoomWindow"/>),
/// and centers the transparent magnifier window on the cursor once, when the zoom session first
/// starts. The window then stays put while the wheel keeps zooming the region it opened over.
/// </summary>
public class ZoomWindowHelper
{
private const int ZoomFactor = 2;
private const int BaseZoomImageSize = 50;
private const int MaxZoomLevel = 4;
private const int MinZoomLevel = 0;
private const int WindowChrome = 30; // Border (12) + canvas (3) margins on each side.
// ALT-1: the magnifier window is a constant size for the whole session — the level-4
// (factor 8) bounding box — so the centred card always fits and never clips. Only the inner
// card animates its size (Composition scale in ZoomView), so there is no per-step window
// resize and no shrink-trim bookkeeping.
private const int MaxWindowSize = (BaseZoomImageSize * 8) + WindowChrome; // 50*8 + 30 = 430
private static readonly Bitmap _bmp = new Bitmap(BaseZoomImageSize, BaseZoomImageSize, PixelFormat.Format32bppArgb);
private static readonly Graphics _graphics = Graphics.FromImage(_bmp);
private readonly IZoomViewModel _zoomViewModel;
private readonly AppStateHandler _appStateHandler;
private int _currentZoomLevel;
private int _previousZoomLevel;
private ZoomWindow _zoomWindow;
private CanvasBitmap _capturedBitmap;
private double _zoomFactorValue = 1;
private bool _zoomWindowVisible;
[ImportingConstructor]
public ZoomWindowHelper(IZoomViewModel zoomViewModel, AppStateHandler appStateHandler)
public ZoomWindowHelper(AppStateHandler appStateHandler)
{
_zoomViewModel = zoomViewModel;
_appStateHandler = appStateHandler;
_appStateHandler.AppClosed += AppStateHandler_AppClosed;
_appStateHandler.AppHidden += AppStateHandler_AppClosed;
_appStateHandler.AppClosed += (s, e) => CloseZoomWindow();
_appStateHandler.AppHidden += (s, e) => CloseZoomWindow();
}
public void Zoom(System.Windows.Point position, bool zoomIn)
public void Zoom(Point position, bool zoomIn)
{
if (zoomIn && _currentZoomLevel < MaxZoomLevel)
{
@@ -65,39 +79,53 @@ namespace ColorPicker.Helpers
{
_currentZoomLevel = 0;
_previousZoomLevel = 0;
HideZoomWindow(true);
_zoomWindowVisible = false;
_zoomWindow?.ZoomViewControl.ResetScale();
_zoomWindow?.ZoomViewControl.ClearBitmap();
_zoomWindow?.Hide();
// Release this session's captured GPU surface (the ZoomView reference is cleared above
// so the canvas will not draw a disposed bitmap).
_capturedBitmap?.Dispose();
_capturedBitmap = null;
}
private void SetZoomImage(System.Windows.Point point)
private void SetZoomImage(Point point)
{
if (_currentZoomLevel == 0)
{
HideZoomWindow();
_zoomWindowVisible = false;
_zoomWindow?.ZoomViewControl.ResetScale();
_zoomWindow?.Hide();
return;
}
// we just started zooming, copy screen area
// Capture once when a zoom session starts (previous level was 0).
if (_previousZoomLevel == 0)
{
// First, exclude the color picker window from the capture; otherwise its
// corner will be included in the zoomed-in image.
var mainWindowHandle = _appStateHandler.GetMainWindowHandle();
bool exclusionSuccess =
WindowCaptureExclusionHelper.Exclude(mainWindowHandle);
// Release the previous session's GPU surface before capturing a new one. The window
// is hidden between sessions, so clear the ZoomView reference first (it is not
// drawing this bitmap) and then dispose.
_zoomWindow?.ZoomViewControl.ClearBitmap();
_capturedBitmap?.Dispose();
_capturedBitmap = null;
var mainWindowHandle = _appStateHandler.GetMainWindowHandle();
bool exclusionSuccess = WindowCaptureExclusionHelper.Exclude(mainWindowHandle);
try
{
var x = (int)point.X - (BaseZoomImageSize / 2);
var y = (int)point.Y - (BaseZoomImageSize / 2);
_graphics.CopyFromScreen(x, y, 0, 0, _bmp.Size, CopyPixelOperation.SourceCopy);
_zoomViewModel.ZoomArea = BitmapToImageSource(_bmp);
_capturedBitmap = BitmapToCanvasBitmap(_bmp);
}
catch (Exception)
{
// CopyFromScreen can fail (invalid screen DC) on a non-interactive session.
_capturedBitmap = null;
}
finally
{
// Restore the color picker window to normal display affinity so that
// it can be captured again.
if (exclusionSuccess)
{
WindowCaptureExclusionHelper.Include(mainWindowHandle);
@@ -105,94 +133,69 @@ namespace ColorPicker.Helpers
}
}
_zoomViewModel.ZoomFactor = Math.Pow(ZoomFactor, _currentZoomLevel - 1);
_zoomFactorValue = Math.Pow(ZoomFactor, _currentZoomLevel - 1);
ShowZoomWindow(point);
// The size the card is animating FROM: the previous level's factor (or the current one
// on first appearance, which makes the scale a no-op snap).
double previousFactor = _previousZoomLevel >= 1 ? Math.Pow(ZoomFactor, _previousZoomLevel - 1) : _zoomFactorValue;
ShowZoomWindow(point, previousFactor);
}
private static BitmapImage BitmapToImageSource(Bitmap bitmap)
private static CanvasBitmap BitmapToCanvasBitmap(Bitmap bitmap)
{
using (MemoryStream memory = new MemoryStream())
var data = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
try
{
bitmap.Save(memory, ImageFormat.Bmp);
memory.Position = 0;
var bytes = new byte[data.Stride * data.Height];
Marshal.Copy(data.Scan0, bytes, 0, bytes.Length);
BitmapImage bitmapimage = new BitmapImage();
bitmapimage.BeginInit();
bitmapimage.StreamSource = memory;
bitmapimage.CacheOption = BitmapCacheOption.OnLoad;
bitmapimage.EndInit();
return bitmapimage;
// GDI 32bppArgb is BGRA in memory, matching B8G8R8A8.
return CanvasBitmap.CreateFromBytes(CanvasDevice.GetSharedDevice(), bytes, bitmap.Width, bitmap.Height, DirectXPixelFormat.B8G8R8A8UIntNormalized);
}
finally
{
bitmap.UnlockBits(data);
}
}
private void HideZoomWindow(bool fully = false)
private void ShowZoomWindow(Point point, double previousFactor)
{
if (_zoomWindow != null)
if (_capturedBitmap == null)
{
_zoomWindow.Opacity = 0;
_zoomViewModel.DesiredWidth = 0;
_zoomViewModel.DesiredHeight = 0;
if (fully)
{
_zoomWindow.Hide();
}
return;
}
}
private void ShowZoomWindow(System.Windows.Point point)
{
_zoomWindow ??= new ZoomWindow
{
Content = _zoomViewModel,
Opacity = 0,
};
_zoomWindow ??= new ZoomWindow();
if (!_zoomWindow.IsVisible)
// Draw the capture at the FINAL zoom factor now; the compositor scales that crisp texture
// during the resize tween (see ZoomView.AnimateResize).
_zoomWindow.ZoomViewControl.SetZoom(_capturedBitmap, _zoomFactorValue);
if (!_zoomWindowVisible)
{
// First appearance this session. Give the window its constant max size (the level-4
// bounding box, so the centred card never clips) and center it on the cursor ONCE,
// then leave it there for the rest of the session — matching the WPF behavior, which
// only repositions the window while it is still transparent (Opacity < 0.5). Moving
// it on every scroll step would drag the magnifier to wherever the cursor currently
// is instead of keeping it on — and zooming — the region it was first opened over.
// (AppWindow.Size and the cursor are both physical pixels; the card is centred inside.)
_zoomWindow.SetWindowSize(MaxWindowSize, MaxWindowSize);
var appWindow = _zoomWindow.AppWindow;
appWindow.Move(new PointInt32((int)point.X - (appWindow.Size.Width / 2), (int)point.Y - (appWindow.Size.Height / 2)));
// The card shows directly at the current size (no tween), so there is no scale
// animation to race against the async Show().
_zoomWindow.ZoomViewControl.ResetScale();
_zoomWindow.Show();
_zoomWindowVisible = true;
}
if (_zoomWindow.Opacity < 0.5)
else
{
var halfWidth = _zoomWindow.Width / 2;
var halfHeight = _zoomWindow.Height / 2;
// usually takes 1-3 iterations to converge
// 5 is just an arbitrary limit to prevent infinite loops
for (var i = 0; i < 5; i++)
{
// mouse position relative to top left of _zoomWindow
var scaledPoint = _zoomWindow.PointFromScreen(point);
var diffX = scaledPoint.X - halfWidth;
var diffY = scaledPoint.Y - halfHeight;
// minimum difference that is considered important
const double minDiff = 0.05;
if (Math.Abs(diffX) < minDiff && Math.Abs(diffY) < minDiff)
{
break;
}
_zoomWindow.Left += diffX;
_zoomWindow.Top += diffY;
}
// make sure color picker window is on top of just opened zoom window
AppStateHandler.SetTopMost();
_zoomWindow.Opacity = 1;
// Same already-shown, laid-out window: animate the card between the old and new size.
_zoomWindow.ZoomViewControl.AnimateResize(previousFactor, _zoomFactorValue);
}
_zoomViewModel.DesiredHeight = BaseZoomImageSize * _zoomViewModel.ZoomFactor;
_zoomViewModel.DesiredWidth = BaseZoomImageSize * _zoomViewModel.ZoomFactor;
}
private void AppStateHandler_AppClosed(object sender, EventArgs e)
{
CloseZoomWindow();
}
}
}

View File

@@ -1,21 +1,19 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Windows.Input;
using ColorPicker.Helpers;
using ColorPicker.Settings;
using Microsoft.PowerToys.Settings.UI.Library.Utilities;
using Windows.System;
using static ColorPicker.NativeMethods;
namespace ColorPicker.Keyboard
{
[Export(typeof(KeyboardMonitor))]
public class KeyboardMonitor : IDisposable
{
private readonly AppStateHandler _appStateHandler;
@@ -26,9 +24,8 @@ namespace ColorPicker.Keyboard
private GlobalKeyboardHook _keyboardHook;
private bool _activationShortcutPressed;
private int keyboardMoveSpeed;
private Key lastArrowKeyPressed = Key.None;
private VirtualKey lastArrowKeyPressed = VirtualKey.None;
[ImportingConstructor]
public KeyboardMonitor(AppStateHandler appStateHandler, IUserSettings userSettings)
{
_appStateHandler = appStateHandler;
@@ -70,40 +67,40 @@ namespace ColorPicker.Keyboard
var virtualCode = e.KeyboardData.VirtualCode;
// ESC pressed
if (virtualCode == KeyInterop.VirtualKeyFromKey(Key.Escape) && e.KeyboardState == GlobalKeyboardHook.KeyboardState.KeyDown)
if (virtualCode == (int)VirtualKey.Escape && e.KeyboardState == GlobalKeyboardHook.KeyboardState.KeyDown)
{
e.Handled = _appStateHandler.HandleEscPressed();
return;
}
if ((virtualCode == KeyInterop.VirtualKeyFromKey(Key.Space) || virtualCode == KeyInterop.VirtualKeyFromKey(Key.Enter)) && (e.KeyboardState == GlobalKeyboardHook.KeyboardState.KeyDown))
if ((virtualCode == (int)VirtualKey.Space || virtualCode == (int)VirtualKey.Enter) && (e.KeyboardState == GlobalKeyboardHook.KeyboardState.KeyDown))
{
e.Handled = _appStateHandler.HandleEnterPressed();
return;
}
if (virtualCode == KeyInterop.VirtualKeyFromKey(Key.Back) && e.KeyboardState == GlobalKeyboardHook.KeyboardState.KeyDown)
if (virtualCode == (int)VirtualKey.Back && e.KeyboardState == GlobalKeyboardHook.KeyboardState.KeyDown)
{
e.Handled = _appStateHandler.HandleEscPressed();
return;
}
if (CheckMoveNeeded(virtualCode, Key.Up, e, 0, -1))
if (CheckMoveNeeded(virtualCode, VirtualKey.Up, e, 0, -1))
{
e.Handled = true;
return;
}
else if (CheckMoveNeeded(virtualCode, Key.Down, e, 0, 1))
else if (CheckMoveNeeded(virtualCode, VirtualKey.Down, e, 0, 1))
{
e.Handled = true;
return;
}
else if (CheckMoveNeeded(virtualCode, Key.Left, e, -1, 0))
else if (CheckMoveNeeded(virtualCode, VirtualKey.Left, e, -1, 0))
{
e.Handled = true;
return;
}
else if (CheckMoveNeeded(virtualCode, Key.Right, e, 1, 0))
else if (CheckMoveNeeded(virtualCode, VirtualKey.Right, e, 1, 0))
{
e.Handled = true;
return;
@@ -144,9 +141,9 @@ namespace ColorPicker.Keyboard
}
}
private bool CheckMoveNeeded(int virtualCode, Key key, GlobalKeyboardHookEventArgs e, int xMove, int yMove)
private bool CheckMoveNeeded(int virtualCode, VirtualKey key, GlobalKeyboardHookEventArgs e, int xMove, int yMove)
{
if (virtualCode == KeyInterop.VirtualKeyFromKey(key))
if (virtualCode == (int)key)
{
if (e.KeyboardState == GlobalKeyboardHook.KeyboardState.KeyDown && _appStateHandler.IsColorPickerVisible())
{
@@ -165,7 +162,7 @@ namespace ColorPicker.Keyboard
}
else if (e.KeyboardState == GlobalKeyboardHook.KeyboardState.KeyUp)
{
lastArrowKeyPressed = Key.None;
lastArrowKeyPressed = VirtualKey.None;
keyboardMoveSpeed = 0;
}
}

View File

@@ -1,29 +0,0 @@
<Window
x:Class="ColorPicker.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:behaviors="clr-namespace:ColorPicker.Behaviors"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:e="http://schemas.microsoft.com/xaml/behaviors"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Width="120"
Height="64"
MinWidth="0"
MinHeight="0"
AllowsTransparency="True"
AutomationProperties.Name="Color Picker"
Background="Transparent"
Opacity="0.1"
ResizeMode="NoResize"
ShowInTaskbar="False"
SizeToContent="WidthAndHeight"
SourceInitialized="MainWindowSourceInitialized"
Topmost="True"
WindowStyle="None"
mc:Ignorable="d">
<e:Interaction.Behaviors>
<behaviors:ChangeWindowPositionBehavior />
<behaviors:AppearAnimationBehavior />
</e:Interaction.Behaviors>
<ContentControl x:Name="MainView" Content="{Binding MainViewModel}" />
</Window>

View File

@@ -1,42 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.ComponentModel.Composition;
using System.Windows;
using System.Windows.Interop;
using ColorPicker.ViewModelContracts;
namespace ColorPicker
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
Closing += MainWindow_Closing;
Bootstrapper.InitializeContainer(this);
InitializeComponent();
DataContext = this;
Show(); // Call show just to make sure source is initialized at startup.
Hide();
}
[Import]
public IMainViewModel MainViewModel { get; set; }
private void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
Closing -= MainWindow_Closing;
Bootstrapper.Dispose();
}
private void MainWindowSourceInitialized(object sender, System.EventArgs e)
{
this.MainViewModel.RegisterWindowHandle(HwndSource.FromHwnd(new WindowInteropHelper(this).Handle));
}
}
}

View File

@@ -3,10 +3,10 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Windows.Media;
using ColorPicker.Helpers;
using ManagedCommon;
using Windows.UI;
namespace ColorPicker.Models
{

View File

@@ -11,10 +11,10 @@ namespace ColorPicker.Mouse
{
event EventHandler<Color> MouseColorChanged;
event EventHandler<System.Windows.Point> MousePositionChanged;
event EventHandler<Windows.Foundation.Point> MousePositionChanged;
// position and bool indicating zoom in or zoom out
event EventHandler<Tuple<System.Windows.Point, bool>> OnMouseWheel;
event EventHandler<Tuple<Windows.Foundation.Point, bool>> OnMouseWheel;
event PrimaryMouseDownEventHandler OnPrimaryMouseDown;
@@ -22,7 +22,7 @@ namespace ColorPicker.Mouse
event MiddleMouseDownEventHandler OnMiddleMouseDown;
System.Windows.Point CurrentPosition { get; }
Windows.Foundation.Point CurrentPosition { get; }
Color CurrentColor { get; }
}

View File

@@ -1,11 +1,10 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Input;
using ManagedCommon;
@@ -19,6 +18,11 @@ namespace ColorPicker.Mouse
public delegate void MiddleMouseDownEventHandler(object sender, IntPtr wParam);
// WinUI 3 has no System.Windows.Input.MouseWheelEventArgs/InputManager. Carry the raw
// wheel delta (the signed high word of MSLLHOOKSTRUCT.mouseData, +/-120 per notch)
// directly; consumers decide zoom direction from its sign.
public delegate void MouseWheelEventHandler(object sender, int delta);
internal class MouseHook
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Interop object")]
@@ -184,8 +188,9 @@ namespace ColorPicker.Mouse
{
if (MouseWheel != null)
{
MouseDevice mouseDev = InputManager.Current.PrimaryMouseDevice;
MouseWheel.Invoke(null, new MouseWheelEventArgs(mouseDev, Environment.TickCount, (int)mouseHookStruct.mouseData >> 16));
// High word of mouseData is the signed wheel delta.
int delta = (int)mouseHookStruct.mouseData >> 16;
MouseWheel.Invoke(null, delta);
return new IntPtr(-1);
}
}

View File

@@ -1,38 +1,37 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.ComponentModel.Composition;
using System.Configuration;
using System.Drawing;
using System.Drawing.Imaging;
using System.Windows.Input;
using System.Windows.Threading;
using ColorPicker.Helpers;
using ColorPicker.Settings;
using Microsoft.UI.Dispatching;
using static ColorPicker.NativeMethods;
using Point = Windows.Foundation.Point;
namespace ColorPicker.Mouse
{
[Export(typeof(IMouseInfoProvider))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class MouseInfoProvider : IMouseInfoProvider
{
private readonly double _mousePullInfoIntervalInMs;
private readonly DispatcherTimer _timer = new DispatcherTimer();
private readonly DispatcherQueueTimer _timer;
private readonly MouseHook _mouseHook;
private readonly IUserSettings _userSettings;
private System.Windows.Point _previousMousePosition = new System.Windows.Point(-1, 1);
private Point _previousMousePosition = new Point(-1, 1);
private Color _previousColor = Color.Transparent;
private bool _colorFormatChanged;
[ImportingConstructor]
public MouseInfoProvider(AppStateHandler appStateMonitor, IUserSettings userSettings)
{
_mousePullInfoIntervalInMs = 1000.0 / GetMainDisplayRefreshRate();
// WPF DispatcherTimer -> the UI-thread DispatcherQueueTimer. Resolve on the UI thread.
_timer = DispatcherQueue.GetForCurrentThread().CreateTimer();
_timer.Interval = TimeSpan.FromMilliseconds(_mousePullInfoIntervalInMs);
_timer.Tick += Timer_Tick;
@@ -52,9 +51,9 @@ namespace ColorPicker.Mouse
public event EventHandler<Color> MouseColorChanged;
public event EventHandler<System.Windows.Point> MousePositionChanged;
public event EventHandler<Point> MousePositionChanged;
public event EventHandler<Tuple<System.Windows.Point, bool>> OnMouseWheel;
public event EventHandler<Tuple<Point, bool>> OnMouseWheel;
public event PrimaryMouseDownEventHandler OnPrimaryMouseDown;
@@ -62,23 +61,11 @@ namespace ColorPicker.Mouse
public event MiddleMouseDownEventHandler OnMiddleMouseDown;
public System.Windows.Point CurrentPosition
{
get
{
return _previousMousePosition;
}
}
public Point CurrentPosition => _previousMousePosition;
public Color CurrentColor
{
get
{
return _previousColor;
}
}
public Color CurrentColor => _previousColor;
private void Timer_Tick(object sender, EventArgs e)
private void Timer_Tick(DispatcherQueueTimer sender, object args)
{
UpdateMouseInfo();
}
@@ -101,24 +88,35 @@ namespace ColorPicker.Mouse
}
}
private static Color GetPixelColor(System.Windows.Point mousePosition)
private static Color GetPixelColor(Point mousePosition)
{
var rect = new Rectangle((int)mousePosition.X, (int)mousePosition.Y, 1, 1);
using (var bmp = new Bitmap(rect.Width, rect.Height, PixelFormat.Format32bppArgb))
try
{
using (var g = Graphics.FromImage(bmp)) // Ensure Graphics object is disposed
var rect = new Rectangle((int)mousePosition.X, (int)mousePosition.Y, 1, 1);
using (var bmp = new Bitmap(rect.Width, rect.Height, PixelFormat.Format32bppArgb))
{
g.CopyFromScreen(rect.Left, rect.Top, 0, 0, bmp.Size, CopyPixelOperation.SourceCopy);
}
using (var g = Graphics.FromImage(bmp))
{
g.CopyFromScreen(rect.Left, rect.Top, 0, 0, bmp.Size, CopyPixelOperation.SourceCopy);
}
return bmp.GetPixel(0, 0);
return bmp.GetPixel(0, 0);
}
}
catch (Exception)
{
// GDI CopyFromScreen can throw "the handle is invalid" when no screen device
// context is available (e.g. a non-interactive / disconnected session, or before
// the desktop is ready at startup). Degrade gracefully instead of crashing the
// app; the next sample succeeds once a real desktop is present.
return Color.Black;
}
}
private static System.Windows.Point GetCursorPosition()
private static Point GetCursorPosition()
{
GetCursorPos(out PointInter lpPoint);
return (System.Windows.Point)lpPoint;
return (Point)lpPoint;
}
private static double GetMainDisplayRefreshRate()
@@ -145,7 +143,7 @@ namespace ColorPicker.Mouse
private void AppStateMonitor_AppShown(object sender, EventArgs e)
{
UpdateMouseInfo();
if (!_timer.IsEnabled)
if (!_timer.IsRunning)
{
_timer.Start();
}
@@ -161,15 +159,15 @@ namespace ColorPicker.Mouse
}
}
private void MouseHook_OnMouseWheel(object sender, MouseWheelEventArgs e)
private void MouseHook_OnMouseWheel(object sender, int delta)
{
if (e.Delta == 0)
if (delta == 0)
{
return;
}
var zoomIn = e.Delta > 0;
OnMouseWheel?.Invoke(this, new Tuple<System.Windows.Point, bool>(_previousMousePosition, zoomIn));
var zoomIn = delta > 0;
OnMouseWheel?.Invoke(this, new Tuple<Point, bool>(_previousMousePosition, zoomIn));
}
private void MouseHook_OnPrimaryMouseDown(object sender, IntPtr wParam)
@@ -197,12 +195,12 @@ namespace ColorPicker.Mouse
private void DisposeHook()
{
if (_timer.IsEnabled)
if (_timer.IsRunning)
{
_timer.Stop();
}
_previousMousePosition = new System.Windows.Point(-1, 1);
_previousMousePosition = new Point(-1, 1);
_mouseHook.OnPrimaryMouseDown -= MouseHook_OnPrimaryMouseDown;
_mouseHook.OnMouseWheel -= MouseHook_OnMouseWheel;
_mouseHook.OnSecondaryMouseUp -= MouseHook_OnSecondaryMouseUp;

View File

@@ -6,17 +6,15 @@ using System;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using System.Windows;
using System.Windows.Interop;
// System.Windows / System.Windows.Interop removed: WinUI 3 has no WindowInteropHelper.
// HandleRef lives in System.Runtime.InteropServices (already imported).
namespace ColorPicker
{
// https://learn.microsoft.com/visualstudio/code-quality/ca1060?view=vs-2019
// will have to rename
public static class NativeMethods
{
private const int GWL_EX_STYLE = -20;
private const int WS_EX_TOOLWINDOW = 0x00000080;
public const int WH_KEYBOARD_LL = 13;
public const int VkSnapshot = 0x2c;
public const int KfAltdown = 0x2000;
@@ -117,7 +115,7 @@ namespace ColorPicker
public int X;
public int Y;
public static explicit operator System.Windows.Point(PointInter point) => new System.Windows.Point(point.X, point.Y);
public static explicit operator Windows.Foundation.Point(PointInter point) => new Windows.Foundation.Point(point.X, point.Y);
}
[StructLayout(LayoutKind.Sequential)]
@@ -220,18 +218,6 @@ namespace ColorPicker
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
internal static extern int GetWindowText(int hwnd, StringBuilder text, int count);
[DllImport("user32.dll", SetLastError = true)]
internal static extern int GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll")]
internal static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
internal static void SetToolWindowStyle(Window win)
{
var hwnd = new WindowInteropHelper(win).Handle;
_ = SetWindowLong(hwnd, GWL_EX_STYLE, GetWindowLong(hwnd, GWL_EX_STYLE) | WS_EX_TOOLWINDOW);
}
/// <summary>
/// Sets the display affinity of a window, which controls how the window is
/// displayed on a monitor. Used to exclude the picker window from ZoomWindow's

View File

@@ -1,62 +1,38 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using ColorPicker.Helpers;
using ColorPicker.Mouse;
using ColorPickerUI;
using ManagedCommon;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
namespace ColorPicker
{
public static class Program
{
private static string[] _args;
[STAThread]
public static void Main(string[] args)
{
Logger.InitializeLogger("\\ColorPicker\\Logs");
_args = args;
Logger.LogInfo($"Color Picker started with pid={Environment.ProcessId}");
if (PowerToys.GPOWrapperProjection.GPOWrapper.GetConfiguredColorPickerEnabledValue() == PowerToys.GPOWrapperProjection.GpoRuleConfigured.Disabled)
if (PowerToys.GPOWrapper.GPOWrapper.GetConfiguredColorPickerEnabledValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
{
Logger.LogWarning("Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.");
return;
}
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
try
WinRT.ComWrappersSupport.InitializeComWrappers();
Application.Start((p) =>
{
using (var application = new App())
{
application.InitializeComponent();
application.Run();
}
}
catch (Exception ex)
{
Logger.LogError("Unhandled exception", ex);
CursorManager.RestoreOriginalCursors();
}
}
private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
if (e.ExceptionObject is Exception ex)
{
Logger.LogError("Unhandled exception", ex);
}
else
{
Logger.LogError("Unhandled exception");
}
CursorManager.RestoreOriginalCursors();
var context = new DispatcherQueueSynchronizationContext(
DispatcherQueue.GetForCurrentThread());
SynchronizationContext.SetSynchronizationContext(context);
_ = new App(args);
});
}
}
}

View File

@@ -1,738 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace ColorPicker.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ColorPicker.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Blue value.
/// </summary>
public static string Blue_value {
get {
return ResourceManager.GetString("Blue_value", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Cancel.
/// </summary>
public static string Cancel {
get {
return ResourceManager.GetString("Cancel", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Color code.
/// </summary>
public static string Color_Code {
get {
return ResourceManager.GetString("Color_Code", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Color history.
/// </summary>
public static string Color_History {
get {
return ResourceManager.GetString("Color_History", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Color palette.
/// </summary>
public static string Color_Palette {
get {
return ResourceManager.GetString("Color_Palette", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Color picker control.
/// </summary>
public static string Color_Picker_Control {
get {
return ResourceManager.GetString("Color_Picker_Control", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Copied to clipboard.
/// </summary>
public static string Copied_to_clipboard {
get {
return ResourceManager.GetString("Copied_to_clipboard", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Copy to clipboard.
/// </summary>
public static string Copy_to_clipboard {
get {
return ResourceManager.GetString("Copy_to_clipboard", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Color Picker editor.
/// </summary>
public static string cp_editor {
get {
return ResourceManager.GetString("cp_editor", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Color Picker.
/// </summary>
public static string CP_Title {
get {
return ResourceManager.GetString("CP_Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Color dark 1.
/// </summary>
public static string Darker_color {
get {
return ResourceManager.GetString("Darker_color", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Color dark 2.
/// </summary>
public static string Darkest_color {
get {
return ResourceManager.GetString("Darkest_color", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Export.
/// </summary>
public static string Export_by {
get {
return ResourceManager.GetString("Export_by", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Grouped by color.
/// </summary>
public static string Export_by_color {
get {
return ResourceManager.GetString("Export_by_color", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Grouped by format.
/// </summary>
public static string Export_by_format {
get {
return ResourceManager.GetString("Export_by_format", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Green value.
/// </summary>
public static string Green_value {
get {
return ResourceManager.GetString("Green_value", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Hex value.
/// </summary>
public static string Hex_value {
get {
return ResourceManager.GetString("Hex_value", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Hue slider.
/// </summary>
public static string Hue_slider {
get {
return ResourceManager.GetString("Hue_slider", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Color light 1.
/// </summary>
public static string Lighter_color {
get {
return ResourceManager.GetString("Lighter_color", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Color light 2.
/// </summary>
public static string Lightest_color {
get {
return ResourceManager.GetString("Lightest color", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Pick.
/// </summary>
public static string New_Label {
get {
return ResourceManager.GetString("New_Label", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Press the Color Picker icon to capture a color from your screen..
/// </summary>
public static string No_colors_yet {
get {
return ResourceManager.GetString("No_colors_yet", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open settings.
/// </summary>
public static string Open_settings {
get {
return ResourceManager.GetString("Open_settings", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Pick color from screen.
/// </summary>
public static string Pick_color {
get {
return ResourceManager.GetString("Pick_color", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Red value.
/// </summary>
public static string Red_value {
get {
return ResourceManager.GetString("Red_value", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Remove.
/// </summary>
public static string Remove {
get {
return ResourceManager.GetString("Remove", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Saturation slider.
/// </summary>
public static string Saturation_slider {
get {
return ResourceManager.GetString("Saturation_slider", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Select.
/// </summary>
public static string Select {
get {
return ResourceManager.GetString("Select", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Select color.
/// </summary>
public static string Select_color {
get {
return ResourceManager.GetString("Select_color", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Selected color.
/// </summary>
public static string Selected_color {
get {
return ResourceManager.GetString("Selected_color", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to This color can be adjusted.
/// </summary>
public static string Selected_color_helptext {
get {
return ResourceManager.GetString("Selected_color_helptext", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Adjust color.
/// </summary>
public static string Selected_color_tooltip {
get {
return ResourceManager.GetString("Selected_color_tooltip", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Aqua.
/// </summary>
public static string TEXT_COLOR_AQUA {
get {
return ResourceManager.GetString("TEXT_COLOR_AQUA", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Black.
/// </summary>
public static string TEXT_COLOR_BLACK {
get {
return ResourceManager.GetString("TEXT_COLOR_BLACK", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Blue.
/// </summary>
public static string TEXT_COLOR_BLUE {
get {
return ResourceManager.GetString("TEXT_COLOR_BLUE", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Blue gray.
/// </summary>
public static string TEXT_COLOR_BLUEGRAY {
get {
return ResourceManager.GetString("TEXT_COLOR_BLUEGRAY", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Bright green.
/// </summary>
public static string TEXT_COLOR_BRIGHTGREEN {
get {
return ResourceManager.GetString("TEXT_COLOR_BRIGHTGREEN", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Brown.
/// </summary>
public static string TEXT_COLOR_BROWN {
get {
return ResourceManager.GetString("TEXT_COLOR_BROWN", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Coral.
/// </summary>
public static string TEXT_COLOR_CORAL {
get {
return ResourceManager.GetString("TEXT_COLOR_CORAL", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Dark blue.
/// </summary>
public static string TEXT_COLOR_DARKBLUE {
get {
return ResourceManager.GetString("TEXT_COLOR_DARKBLUE", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Dark gray.
/// </summary>
public static string TEXT_COLOR_DARKGRAY {
get {
return ResourceManager.GetString("TEXT_COLOR_DARKGRAY", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Dark green.
/// </summary>
public static string TEXT_COLOR_DARKGREEN {
get {
return ResourceManager.GetString("TEXT_COLOR_DARKGREEN", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Dark purple.
/// </summary>
public static string TEXT_COLOR_DARKPURPLE {
get {
return ResourceManager.GetString("TEXT_COLOR_DARKPURPLE", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Dark red.
/// </summary>
public static string TEXT_COLOR_DARKRED {
get {
return ResourceManager.GetString("TEXT_COLOR_DARKRED", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Dark teal.
/// </summary>
public static string TEXT_COLOR_DARKTEAL {
get {
return ResourceManager.GetString("TEXT_COLOR_DARKTEAL", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Dark yellow.
/// </summary>
public static string TEXT_COLOR_DARKYELLOW {
get {
return ResourceManager.GetString("TEXT_COLOR_DARKYELLOW", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Gold.
/// </summary>
public static string TEXT_COLOR_GOLD {
get {
return ResourceManager.GetString("TEXT_COLOR_GOLD", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Gray.
/// </summary>
public static string TEXT_COLOR_GRAY {
get {
return ResourceManager.GetString("TEXT_COLOR_GRAY", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Green.
/// </summary>
public static string TEXT_COLOR_GREEN {
get {
return ResourceManager.GetString("TEXT_COLOR_GREEN", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Ice blue.
/// </summary>
public static string TEXT_COLOR_ICEBLUE {
get {
return ResourceManager.GetString("TEXT_COLOR_ICEBLUE", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Indigo.
/// </summary>
public static string TEXT_COLOR_INDIGO {
get {
return ResourceManager.GetString("TEXT_COLOR_INDIGO", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Lavender.
/// </summary>
public static string TEXT_COLOR_LAVENDER {
get {
return ResourceManager.GetString("TEXT_COLOR_LAVENDER", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Light blue.
/// </summary>
public static string TEXT_COLOR_LIGHTBLUE {
get {
return ResourceManager.GetString("TEXT_COLOR_LIGHTBLUE", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Light gray.
/// </summary>
public static string TEXT_COLOR_LIGHTGRAY {
get {
return ResourceManager.GetString("TEXT_COLOR_LIGHTGRAY", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Light green.
/// </summary>
public static string TEXT_COLOR_LIGHTGREEN {
get {
return ResourceManager.GetString("TEXT_COLOR_LIGHTGREEN", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Light orange.
/// </summary>
public static string TEXT_COLOR_LIGHTORANGE {
get {
return ResourceManager.GetString("TEXT_COLOR_LIGHTORANGE", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Light turquoise.
/// </summary>
public static string TEXT_COLOR_LIGHTTURQUOISE {
get {
return ResourceManager.GetString("TEXT_COLOR_LIGHTTURQUOISE", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Light yellow.
/// </summary>
public static string TEXT_COLOR_LIGHTYELLOW {
get {
return ResourceManager.GetString("TEXT_COLOR_LIGHTYELLOW", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Lime.
/// </summary>
public static string TEXT_COLOR_LIME {
get {
return ResourceManager.GetString("TEXT_COLOR_LIME", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Olive green.
/// </summary>
public static string TEXT_COLOR_OLIVEGREEN {
get {
return ResourceManager.GetString("TEXT_COLOR_OLIVEGREEN", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Orange.
/// </summary>
public static string TEXT_COLOR_ORANGE {
get {
return ResourceManager.GetString("TEXT_COLOR_ORANGE", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Pale blue.
/// </summary>
public static string TEXT_COLOR_PALEBLUE {
get {
return ResourceManager.GetString("TEXT_COLOR_PALEBLUE", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Periwinkle.
/// </summary>
public static string TEXT_COLOR_PERIWINKLE {
get {
return ResourceManager.GetString("TEXT_COLOR_PERIWINKLE", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Pink.
/// </summary>
public static string TEXT_COLOR_PINK {
get {
return ResourceManager.GetString("TEXT_COLOR_PINK", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Plum.
/// </summary>
public static string TEXT_COLOR_PLUM {
get {
return ResourceManager.GetString("TEXT_COLOR_PLUM", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Purple.
/// </summary>
public static string TEXT_COLOR_PURPLE {
get {
return ResourceManager.GetString("TEXT_COLOR_PURPLE", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Red.
/// </summary>
public static string TEXT_COLOR_RED {
get {
return ResourceManager.GetString("TEXT_COLOR_RED", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Rose.
/// </summary>
public static string TEXT_COLOR_ROSE {
get {
return ResourceManager.GetString("TEXT_COLOR_ROSE", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Sky blue.
/// </summary>
public static string TEXT_COLOR_SKYBLUE {
get {
return ResourceManager.GetString("TEXT_COLOR_SKYBLUE", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Tan.
/// </summary>
public static string TEXT_COLOR_TAN {
get {
return ResourceManager.GetString("TEXT_COLOR_TAN", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Teal.
/// </summary>
public static string TEXT_COLOR_TEAL {
get {
return ResourceManager.GetString("TEXT_COLOR_TEAL", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Turquoise.
/// </summary>
public static string TEXT_COLOR_TURQUOISE {
get {
return ResourceManager.GetString("TEXT_COLOR_TURQUOISE", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to White.
/// </summary>
public static string TEXT_COLOR_WHITE {
get {
return ResourceManager.GetString("TEXT_COLOR_WHITE", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Yellow.
/// </summary>
public static string TEXT_COLOR_YELLOW {
get {
return ResourceManager.GetString("TEXT_COLOR_YELLOW", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Value slider.
/// </summary>
public static string Value_slider {
get {
return ResourceManager.GetString("Value_slider", resourceCulture);
}
}
}
}

View File

@@ -1,26 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace ColorPicker.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.6.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default {
get {
return defaultInstance;
}
}
}
}

View File

@@ -1,7 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
<Settings />
</SettingsFile>

View File

@@ -1,218 +1,88 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:ColorPicker.Converters"
xmlns:helpers="clr-namespace:ColorPicker.Helpers">
xmlns:converters="using:ColorPicker.Converters">
<FontFamily x:Key="IconFontFamily">Segoe Fluent Icons, Segoe MDL2 Assets</FontFamily>
<!-- CONVERTERS -->
<BooleanToVisibilityConverter x:Key="bool2VisibilityConverter" />
<converters:BoolToVisibilityConverter x:Key="bool2VisibilityConverter" />
<converters:BoolToInvertedVisibilityConverter x:Key="bool2InvertedVisibilityConverter" />
<converters:ColorToBrushConverter x:Key="colorToBrushConverter" />
<converters:ColorToStringConverter x:Key="colorToStringConverter" />
<converters:NumberToVisibilityConverter x:Key="numberToVisibilityConverter" />
<converters:NumberToInvertedVisibilityConverter x:Key="numberToInvertedVisibilityConverter" />
<!--
Copied from https://github.com/dotnet/wpf/blob/v9.0.1/src/Microsoft.DotNet.Wpf/src/Themes/PresentationFramework.Fluent/Styles/Button.xaml
This can be removed and use AccentButtonStyle when https://github.com/dotnet/wpf/issues/10413 gets fixed.
Subtle (transparent, hover-fill) icon button. Built on the native WinUI button
template so it gets proper Fluent visual states for free.
-->
<Style x:Key="CustomAccentButtonStyle" TargetType="{x:Type Button}">
<Setter Property="FocusVisualStyle" Value="{DynamicResource DefaultControlFocusVisualStyle}" />
<Setter Property="Background" Value="{DynamicResource AccentButtonBackground}" />
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForeground}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentControlElevationBorderBrush}" />
<Setter Property="BorderThickness" Value="{StaticResource ButtonBorderThemeThickness}" />
<Setter Property="Padding" Value="{StaticResource ButtonPadding}" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="Border.CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border
x:Name="ContentBorder"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
MinWidth="{TemplateBinding MinWidth}"
MinHeight="{TemplateBinding MinHeight}"
Padding="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding Border.CornerRadius}">
<ContentPresenter
x:Name="ContentPresenter"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
TextElement.Foreground="{TemplateBinding Foreground}" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="ContentBorder" Property="Background" Value="{DynamicResource AccentButtonBackgroundPointerOver}" />
<Setter TargetName="ContentBorder" Property="BorderBrush" Value="{DynamicResource ControlElevationBorderBrush}" />
<Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="{DynamicResource AccentButtonForegroundPointerOver}" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="ContentBorder" Property="Background" Value="{DynamicResource AccentButtonBackgroundPressed}" />
<Setter TargetName="ContentBorder" Property="BorderBrush" Value="{DynamicResource AccentControlElevationBorderBrush}" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="ContentBorder" Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
<Setter TargetName="ContentBorder" Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
<Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="TitleBarCloseButtonStyle" TargetType="{x:Type Button}">
<Style
x:Key="SubtleButtonStyle"
BasedOn="{StaticResource DefaultButtonStyle}"
TargetType="Button">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border
x:Name="ContentBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding Border.CornerRadius}">
<ContentPresenter
x:Name="ContentPresenter"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
TextElement.Foreground="{TemplateBinding Foreground}" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="ContentBorder" Property="Background" Value="Red" />
<Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="White" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="SubtleButtonStyle" TargetType="{x:Type Button}">
<Setter Property="FocusVisualStyle" Value="{DynamicResource DefaultControlFocusVisualStyle}" />
<Setter Property="Foreground" Value="{DynamicResource TextFillColorPrimaryBrush}" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Focusable" Value="True" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Padding" Value="1" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border
x:Name="border"
Background="Transparent"
BorderBrush="Transparent"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4"
SnapsToDevicePixels="true">
<ContentPresenter
x:Name="contentPresenter"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Focusable="False"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
TextElement.Foreground="{TemplateBinding Foreground}" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsDefaulted" Value="true">
<Setter TargetName="border" Property="Background" Value="Transparent" />
</Trigger>
<Trigger Property="IsMouseOver" Value="true">
<Setter TargetName="border" Property="Background" Value="{DynamicResource SubtleFillColorSecondaryBrush}" />
<Setter TargetName="contentPresenter" Property="TextElement.Foreground" Value="{DynamicResource TextFillColorSecondaryBrush}" />
</Trigger>
<Trigger Property="IsPressed" Value="true">
<Setter TargetName="border" Property="Background" Value="{DynamicResource SubtleFillColorTertiaryBrush}" />
<Setter TargetName="contentPresenter" Property="TextElement.Foreground" Value="{DynamicResource TextFillColorSecondaryBrush}" />
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<!-- <Setter TargetName="border" Property="Background" Value="{DynamicResource ControlFillColorDisabledBrush}" />
<Setter Property="TextElement.Foreground" TargetName="contentPresenter" Value="{DynamicResource TextFillColorDisabledBrush}"/>-->
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!--
Color-shade swatch button: a thin colored bar whose content (a glyph/label) is
revealed on hover. No native equivalent, so this keeps a custom template, rewritten
with the VisualStateManager and the built-in Control.CornerRadius (the WPF-era
ControlHelper.CornerRadius attached property is unnecessary in WinUI).
-->
<Style x:Key="ColorShadeButtonStyle" TargetType="Button">
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="Background" Value="{DynamicResource ButtonBackground}" />
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrush}" />
<Setter Property="BorderThickness" Value="{DynamicResource ButtonBorderThemeThickness}" />
<Setter Property="Background" Value="{ThemeResource ButtonBackground}" />
<Setter Property="Foreground" Value="{ThemeResource ButtonForeground}" />
<Setter Property="BorderBrush" Value="{ThemeResource ButtonBorderBrush}" />
<Setter Property="BorderThickness" Value="{ThemeResource ButtonBorderThemeThickness}" />
<Setter Property="Padding" Value="0" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="FontFamily" Value="{DynamicResource ContentControlThemeFontFamily}" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="Width" Value="12" />
<Setter Property="MinWidth" Value="20" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="FocusVisualStyle" Value="{DynamicResource {x:Static SystemParameters.FocusVisualStyleKey}}" />
<!--<Setter Property="ui:FocusVisualHelper.UseSystemFocusVisuals" Value="{DynamicResource UseSystemFocusVisuals}" />
<Setter Property="ui:FocusVisualHelper.FocusVisualMargin" Value="-2" />-->
<Setter Property="Stylus.IsPressAndHoldEnabled" Value="False" />
<Setter Property="Width" Value="12" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border
x:Name="Background"
Background="{TemplateBinding Background}"
CornerRadius="{TemplateBinding helpers:ControlHelper.CornerRadius}"
SnapsToDevicePixels="True">
<Grid x:Name="RootGrid">
<Border
x:Name="Border"
Padding="{TemplateBinding Padding}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ContentPresenter
x:Name="ContentPresenter"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Focusable="False"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
Visibility="Collapsed" />
x:Name="BackgroundBorder"
Background="{TemplateBinding Background}"
CornerRadius="{TemplateBinding CornerRadius}">
<Border
Padding="{TemplateBinding Padding}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ContentPresenter
x:Name="ContentPresenter"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
Opacity="0" />
</Border>
</Border>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Background" Property="Opacity" Value="0.8" />
<Setter TargetName="ContentPresenter" Property="Visibility" Value="Visible" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="Background" Property="Opacity" Value="0.9" />
</Trigger>
</ControlTemplate.Triggers>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<VisualState.Setters>
<Setter Target="BackgroundBorder.Opacity" Value="0.8" />
<Setter Target="ContentPresenter.Opacity" Value="1" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Target="BackgroundBorder.Opacity" Value="0.9" />
<Setter Target="ContentPresenter.Opacity" Value="1" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Disabled" />
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
</ResourceDictionary>

View File

@@ -1,17 +0,0 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewModels="clr-namespace:ColorPicker.ViewModels"
xmlns:views="clr-namespace:ColorPicker.Views">
<DataTemplate DataType="{x:Type viewModels:MainViewModel}">
<views:MainView />
</DataTemplate>
<DataTemplate DataType="{x:Type viewModels:ZoomViewModel}">
<views:ZoomView />
</DataTemplate>
<DataTemplate DataType="{x:Type viewModels:ColorEditorViewModel}">
<views:ColorEditorView />
</DataTemplate>
</ResourceDictionary>

Some files were not shown because too many files have changed in this diff Show More