diff --git a/CHANGELOG.md b/CHANGELOG.md index d96803c..40ecb38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,30 @@ All notable changes to Aperture Image Viewer are documented here. The format fol ## [Unreleased] +## [0.8.6-beta1] - 2026-08-21 + +### Fixed +- **Folder-tree keyboard** — Tab / Shift+Tab cycle only the folder tree and the tile + list (ribbon and the Everything button stay out). Tabbing in does not change the + destination selection. Clicking or Tabbing to the tree gives it keyboard focus so + arrows move folders immediately. Arrowing the tree commits the selected node at + once; the tile pane rebuilds in the background and shows the existing Loading… + spinner if that rebuild is slow, instead of gating the next arrow on grid render. +- **First click** on an unfocused window or pane focuses the clicked folder / tile / + canvas in that same click (Explorer-style). The activating click is not swallowed. + The folder expand arrow and include checkbox do not change the selected folder. + +## [0.8.5-beta1] - 2026-08-21 + +### Fixed +- **Tile-pane Up/Down** stay in the same *visual* column at every zoom. 0.8.4 + stepped by a width-based column count that came out one low once the + scrollbar (and padding) were already excluded from the wrap viewport — a + 4-column grid walked as 3, so selection moved diagonally. The picker now + uses layout X (and a column count that cannot undercut columns already on + screen). PageUp/PageDown still move one viewport in that same column; + Left/Right are unchanged. Headers are not stops. + ## [0.8.4-beta1] - 2026-08-21 ### Fixed diff --git a/src/Aperture.App/Aperture.App.csproj b/src/Aperture.App/Aperture.App.csproj index 874e3f9..616b907 100644 --- a/src/Aperture.App/Aperture.App.csproj +++ b/src/Aperture.App/Aperture.App.csproj @@ -16,7 +16,7 @@ true Assets\aperture.ico Aperture - 0.8.4-beta1 + 0.8.6-beta1 Aperture Image Viewer Aperture Image Viewer A fast local image & video browser for Windows. diff --git a/src/Aperture.App/LibraryPaneFocus.cs b/src/Aperture.App/LibraryPaneFocus.cs index bbd7ca0..c29b3e2 100644 --- a/src/Aperture.App/LibraryPaneFocus.cs +++ b/src/Aperture.App/LibraryPaneFocus.cs @@ -8,9 +8,10 @@ namespace Aperture.App; /// -/// Keyboard focus for the library contents/tile pane. Clicking a tile already -/// focuses that ; clicking the empty canvas did not, -/// so arrow / PageUp / PageDown / scroll keys stayed on the folder tree. +/// Keyboard focus for the library: folder tree ↔ tile pane. Clicking a tile +/// already focuses that ; clicking the empty canvas +/// did not, so arrow / PageUp / PageDown / scroll keys stayed on the folder +/// tree. Tab onto either pane must not change its selection. /// internal static class LibraryPaneFocus { @@ -47,6 +48,127 @@ public static bool ShouldFocusContentsPane(DependencyObject? originalSource, IIn public static bool ShouldKeepExistingSelection(bool fromOutside, bool newFocusIsItem, bool newFocusIsSelected) => fromOutside && newFocusIsItem && !newFocusIsSelected; + /// Win32 WM_MOUSEACTIVATE — the click that activates a window. + internal const int WmMouseActivate = 0x0021; + + /// Activate and deliver the click to the control (Explorer). Not MA_ACTIVATEANDEAT. + internal const int MaActivate = 1; + + /// + /// True when this is the click that activates the window. Return + /// so the same click focuses the folder / tile / + /// canvas — an activation-only first click would swallow it. + /// + public static bool TryPassActivatingClick(int msg, out IntPtr result) + { + if (msg != WmMouseActivate) + { + result = IntPtr.Zero; + return false; + } + result = new IntPtr(MaActivate); + return true; + } + + internal enum TreeFocusAction + { + /// Leave WPF's focus target alone (arrows inside the tree, or a click). + Leave, + /// Focus the already-selected folder so arrows move from there. + FocusSelectedItem, + /// + /// Keep focus on the TreeView — "Everything" is showing; do not select the first folder. + /// + FocusTree, + } + + /// What a left-click on the folder tree should do to keyboard focus. + internal enum TreeClickAction + { + /// Folder row / name / icon — TreeViewItem.Focus() (which also selects). + FocusItem, + /// + /// Expand chevron or include checkbox: do not Focus() the item. + /// That call selects the folder; Explorer and 0.8.5 keep those hits off selection. + /// Alias text boxes keep the caret. + /// + Leave, + /// Empty tree chrome — focus the TreeView without changing selection. + FocusTree, + } + + internal enum TreeHit + { + Row, + Expander, + IncludeCheckBox, + TextInput, + Canvas, + } + + /// + /// Same-click folder focus applies to the row only. The expander + /// (, Focusable=False) and the root include + /// checkbox must not select the folder. + /// + public static TreeClickAction OnTreePreviewClick(TreeHit hit) => hit switch + { + TreeHit.Row => TreeClickAction.FocusItem, + TreeHit.Canvas => TreeClickAction.FocusTree, + _ => TreeClickAction.Leave, + }; + + public static TreeClickAction OnTreePreviewClick(DependencyObject? originalSource) => + OnTreePreviewClick(ClassifyTreeHit(originalSource)); + + internal static TreeHit ClassifyTreeHit(DependencyObject? originalSource) + { + var node = originalSource; + while (node is not null) + { + if (node is TextBox or PasswordBox or ComboBox or RichTextBox) + return TreeHit.TextInput; + // CheckBox before ToggleButton — CheckBox is a ToggleButton. + if (node is CheckBox) + return TreeHit.IncludeCheckBox; + if (node is ToggleButton) + return TreeHit.Expander; + if (node is TreeViewItem) + return TreeHit.Row; + if (node is TreeView or Window) + return TreeHit.Canvas; + node = Parent(node); + } + return TreeHit.Canvas; + } + + /// + /// Where keyboard focus should land when it arrives on the folder tree. + /// Tab must not change the selected folder; click must not yank focus back + /// to a previous node while the new item's IsSelected is still catching up. + /// + public static TreeFocusAction OnTreeKeyboardArrival( + bool fromOutside, bool mousePressed, + bool newFocusIsItem, bool newFocusIsSelected, + bool hasSelectedItem) + { + if (!fromOutside) + return TreeFocusAction.Leave; + + // Row-click path focuses the item in PreviewMouseDown; IsSelected may still be false. + if (mousePressed) + return TreeFocusAction.Leave; + + if (newFocusIsItem && newFocusIsSelected) + return TreeFocusAction.Leave; + + if (hasSelectedItem) + return TreeFocusAction.FocusSelectedItem; + + // Tab onto the tree (or onto the first folder) while Everything is showing. + return TreeFocusAction.FocusTree; + } + public static Hit Classify(DependencyObject? originalSource, IInputElement? currentFocus) { // A dialog keeps focus even if the click somehow reached the pane diff --git a/src/Aperture.App/MainWindow.xaml b/src/Aperture.App/MainWindow.xaml index c1e8f0a..30ed633 100644 --- a/src/Aperture.App/MainWindow.xaml +++ b/src/Aperture.App/MainWindow.xaml @@ -497,12 +497,15 @@ + Width="120" VerticalAlignment="Center" VerticalContentAlignment="Center" Height="30" + IsTabStop="False" /> - - + +