From a234e469b285650addd3c9742047c721cc74e228 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 07:43:42 +0000 Subject: [PATCH 1/3] Initial plan From 2792e8d7080af2978b7fdfadae6aa2f7b9403101 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 07:51:16 +0000 Subject: [PATCH 2/3] Add search panel support for CSV viewer with Ctrl+F Co-authored-by: emako <24737061+emako@users.noreply.github.com> --- .../Controls/SearchPanel.xaml | 85 +++++++ .../Controls/SearchPanel.xaml.cs | 131 ++++++++++ .../CsvViewerPanel.xaml | 4 + .../CsvViewerPanel.xaml.cs | 223 ++++++++++++++++++ 4 files changed, 443 insertions(+) create mode 100644 QuickLook.Plugin/QuickLook.Plugin.CsvViewer/Controls/SearchPanel.xaml create mode 100644 QuickLook.Plugin/QuickLook.Plugin.CsvViewer/Controls/SearchPanel.xaml.cs diff --git a/QuickLook.Plugin/QuickLook.Plugin.CsvViewer/Controls/SearchPanel.xaml b/QuickLook.Plugin/QuickLook.Plugin.CsvViewer/Controls/SearchPanel.xaml new file mode 100644 index 000000000..061f1a6c4 --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.CsvViewer/Controls/SearchPanel.xaml @@ -0,0 +1,85 @@ + + + + + + + + + + + + diff --git a/QuickLook.Plugin/QuickLook.Plugin.CsvViewer/Controls/SearchPanel.xaml.cs b/QuickLook.Plugin/QuickLook.Plugin.CsvViewer/Controls/SearchPanel.xaml.cs new file mode 100644 index 000000000..62c965094 --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.CsvViewer/Controls/SearchPanel.xaml.cs @@ -0,0 +1,131 @@ +// Copyright © 2017-2025 QL-Win Contributors +// +// This file is part of QuickLook program. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using System; +using System.Collections.Generic; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; + +namespace QuickLook.Plugin.CsvViewer.Controls; + +public partial class SearchPanel : UserControl +{ + private List<(int Row, int Column)> _searchResults = new List<(int, int)>(); + private int _currentResultIndex = -1; + + public event EventHandler SearchRequested; + public event EventHandler NavigateRequested; + public event EventHandler CloseRequested; + + public SearchPanel() + { + InitializeComponent(); + } + + public string SearchText => searchTextBox.Text; + public bool MatchCase => matchCaseCheckBox.IsChecked == true; + + public new void Focus() + { + searchTextBox.Focus(); + searchTextBox.SelectAll(); + } + + public void UpdateMatchCount(List<(int Row, int Column)> results, int currentIndex) + { + _searchResults = results ?? new List<(int, int)>(); + _currentResultIndex = currentIndex; + + if (_searchResults.Count == 0) + { + matchCountText.Text = string.IsNullOrEmpty(searchTextBox.Text) ? "" : "0/0"; + } + else + { + matchCountText.Text = $"{currentIndex + 1}/{_searchResults.Count}"; + } + } + + private void SearchTextBox_TextChanged(object sender, TextChangedEventArgs e) + { + SearchRequested?.Invoke(this, new SearchEventArgs(searchTextBox.Text, MatchCase)); + } + + private void SearchTextBox_KeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Enter) + { + if (Keyboard.Modifiers == ModifierKeys.Shift) + { + FindPrevious_Click(sender, e); + } + else + { + FindNext_Click(sender, e); + } + e.Handled = true; + } + else if (e.Key == Key.Escape) + { + CloseSearch_Click(sender, e); + e.Handled = true; + } + } + + private void FindPrevious_Click(object sender, RoutedEventArgs e) + { + NavigateRequested?.Invoke(this, new NavigateEventArgs(false)); + } + + private void FindNext_Click(object sender, RoutedEventArgs e) + { + NavigateRequested?.Invoke(this, new NavigateEventArgs(true)); + } + + private void MatchCase_Changed(object sender, RoutedEventArgs e) + { + SearchRequested?.Invoke(this, new SearchEventArgs(searchTextBox.Text, MatchCase)); + } + + private void CloseSearch_Click(object sender, RoutedEventArgs e) + { + CloseRequested?.Invoke(this, EventArgs.Empty); + } +} + +public class SearchEventArgs : EventArgs +{ + public string SearchText { get; } + public bool MatchCase { get; } + + public SearchEventArgs(string searchText, bool matchCase) + { + SearchText = searchText; + MatchCase = matchCase; + } +} + +public class NavigateEventArgs : EventArgs +{ + public bool Forward { get; } + + public NavigateEventArgs(bool forward) + { + Forward = forward; + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.CsvViewer/CsvViewerPanel.xaml b/QuickLook.Plugin/QuickLook.Plugin.CsvViewer/CsvViewerPanel.xaml index 570897aab..df33e8ee5 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.CsvViewer/CsvViewerPanel.xaml +++ b/QuickLook.Plugin/QuickLook.Plugin.CsvViewer/CsvViewerPanel.xaml @@ -1,12 +1,14 @@  @@ -30,5 +32,7 @@ ItemsSource="{Binding Path=Rows, ElementName=csvViewer}" RowBackground="#00FFFFFF" VerticalGridLinesBrush="#19000000" /> + diff --git a/QuickLook.Plugin/QuickLook.Plugin.CsvViewer/CsvViewerPanel.xaml.cs b/QuickLook.Plugin/QuickLook.Plugin.CsvViewer/CsvViewerPanel.xaml.cs index c6d1daa25..23ea01fbe 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.CsvViewer/CsvViewerPanel.xaml.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.CsvViewer/CsvViewerPanel.xaml.cs @@ -17,6 +17,7 @@ using CsvHelper; using CsvHelper.Configuration; +using QuickLook.Plugin.CsvViewer.Controls; using System; using System.Collections.Generic; using System.Globalization; @@ -25,7 +26,9 @@ using System.Text; using System.Windows; using System.Windows.Controls; +using System.Windows.Controls.Primitives; using System.Windows.Data; +using System.Windows.Input; using System.Windows.Media; using UtfUnknown; @@ -33,13 +36,233 @@ namespace QuickLook.Plugin.CsvViewer; public partial class CsvViewerPanel : UserControl { + private List<(int Row, int Column)> _searchResults = new List<(int, int)>(); + private int _currentResultIndex = -1; + private string _currentSearchText = string.Empty; + private bool _currentMatchCase; + public CsvViewerPanel() { InitializeComponent(); + + KeyDown += CsvViewerPanel_KeyDown; + searchPanel.SearchRequested += SearchPanel_SearchRequested; + searchPanel.NavigateRequested += SearchPanel_NavigateRequested; + searchPanel.CloseRequested += SearchPanel_CloseRequested; } public List Rows { get; private set; } = []; + private void CsvViewerPanel_KeyDown(object sender, KeyEventArgs e) + { + if ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control && e.Key == Key.F) + { + OpenSearchPanel(); + e.Handled = true; + } + else if (e.Key == Key.Escape && searchPanel.Visibility == Visibility.Visible) + { + CloseSearchPanel(); + e.Handled = true; + } + else if (e.Key == Key.F3) + { + if (searchPanel.Visibility == Visibility.Visible && _searchResults.Count > 0) + { + if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift) + { + NavigateToPreviousResult(); + } + else + { + NavigateToNextResult(); + } + } + e.Handled = true; + } + } + + private void OpenSearchPanel() + { + searchPanel.Visibility = Visibility.Visible; + searchPanel.Focus(); + } + + private void CloseSearchPanel() + { + searchPanel.Visibility = Visibility.Collapsed; + ClearHighlighting(); + _searchResults.Clear(); + _currentResultIndex = -1; + dataGrid.Focus(); + } + + private void SearchPanel_SearchRequested(object sender, SearchEventArgs e) + { + _currentSearchText = e.SearchText; + _currentMatchCase = e.MatchCase; + PerformSearch(); + } + + private void SearchPanel_NavigateRequested(object sender, NavigateEventArgs e) + { + if (e.Forward) + { + NavigateToNextResult(); + } + else + { + NavigateToPreviousResult(); + } + } + + private void SearchPanel_CloseRequested(object sender, EventArgs e) + { + CloseSearchPanel(); + } + + private void PerformSearch() + { + ClearHighlighting(); + _searchResults.Clear(); + _currentResultIndex = -1; + + if (string.IsNullOrEmpty(_currentSearchText)) + { + searchPanel.UpdateMatchCount(_searchResults, _currentResultIndex); + return; + } + + var comparison = _currentMatchCase ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; + + for (int rowIndex = 0; rowIndex < Rows.Count; rowIndex++) + { + var row = Rows[rowIndex]; + for (int colIndex = 0; colIndex < row.Length; colIndex++) + { + if (row[colIndex] != null && row[colIndex].IndexOf(_currentSearchText, comparison) >= 0) + { + _searchResults.Add((rowIndex, colIndex)); + } + } + } + + if (_searchResults.Count > 0) + { + _currentResultIndex = 0; + NavigateToCurrentResult(); + } + + searchPanel.UpdateMatchCount(_searchResults, _currentResultIndex); + } + + private void NavigateToNextResult() + { + if (_searchResults.Count == 0) + return; + + _currentResultIndex = (_currentResultIndex + 1) % _searchResults.Count; + NavigateToCurrentResult(); + searchPanel.UpdateMatchCount(_searchResults, _currentResultIndex); + } + + private void NavigateToPreviousResult() + { + if (_searchResults.Count == 0) + return; + + _currentResultIndex = (_currentResultIndex - 1 + _searchResults.Count) % _searchResults.Count; + NavigateToCurrentResult(); + searchPanel.UpdateMatchCount(_searchResults, _currentResultIndex); + } + + private void NavigateToCurrentResult() + { + if (_currentResultIndex < 0 || _currentResultIndex >= _searchResults.Count) + return; + + var (rowIndex, colIndex) = _searchResults[_currentResultIndex]; + + // Scroll to the row + if (rowIndex < dataGrid.Items.Count) + { + dataGrid.ScrollIntoView(dataGrid.Items[rowIndex]); + dataGrid.UpdateLayout(); + + // Select the cell + dataGrid.SelectedIndex = rowIndex; + + // Try to highlight the specific cell + HighlightCurrentCell(rowIndex, colIndex); + } + } + + private void HighlightCurrentCell(int rowIndex, int colIndex) + { + try + { + var row = dataGrid.ItemContainerGenerator.ContainerFromIndex(rowIndex) as DataGridRow; + if (row != null) + { + var presenter = FindVisualChild(row); + if (presenter != null) + { + var cell = presenter.ItemContainerGenerator.ContainerFromIndex(colIndex) as DataGridCell; + if (cell != null) + { + cell.Background = new SolidColorBrush(Color.FromArgb(128, 255, 255, 0)); // Semi-transparent yellow + } + } + } + } + catch + { + // Ignore errors when highlighting + } + } + + private void ClearHighlighting() + { + // Reset cell backgrounds by iterating through visible rows + foreach (var item in dataGrid.Items) + { + var row = dataGrid.ItemContainerGenerator.ContainerFromItem(item) as DataGridRow; + if (row != null) + { + var presenter = FindVisualChild(row); + if (presenter != null) + { + for (int i = 0; i < dataGrid.Columns.Count; i++) + { + var cell = presenter.ItemContainerGenerator.ContainerFromIndex(i) as DataGridCell; + if (cell != null) + { + cell.ClearValue(DataGridCell.BackgroundProperty); + } + } + } + } + } + } + + private static T FindVisualChild(DependencyObject parent) where T : DependencyObject + { + for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++) + { + var child = VisualTreeHelper.GetChild(parent, i); + if (child is T typedChild) + { + return typedChild; + } + var result = FindVisualChild(child); + if (result != null) + { + return result; + } + } + return null; + } + public void LoadFile(string path) { const int limit = 10000; From 6e3e02a6f8a9a6844e6b21e6f9523daf82cb43fb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 07:54:29 +0000 Subject: [PATCH 3/3] Address code review feedback for CSV viewer search panel Co-authored-by: emako <24737061+emako@users.noreply.github.com> --- .../Controls/SearchPanel.xaml.cs | 13 ++---- .../CsvViewerPanel.xaml.cs | 45 +++++++++---------- 2 files changed, 23 insertions(+), 35 deletions(-) diff --git a/QuickLook.Plugin/QuickLook.Plugin.CsvViewer/Controls/SearchPanel.xaml.cs b/QuickLook.Plugin/QuickLook.Plugin.CsvViewer/Controls/SearchPanel.xaml.cs index 62c965094..8743a787f 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.CsvViewer/Controls/SearchPanel.xaml.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.CsvViewer/Controls/SearchPanel.xaml.cs @@ -16,7 +16,6 @@ // along with this program. If not, see . using System; -using System.Collections.Generic; using System.Windows; using System.Windows.Controls; using System.Windows.Input; @@ -25,9 +24,6 @@ namespace QuickLook.Plugin.CsvViewer.Controls; public partial class SearchPanel : UserControl { - private List<(int Row, int Column)> _searchResults = new List<(int, int)>(); - private int _currentResultIndex = -1; - public event EventHandler SearchRequested; public event EventHandler NavigateRequested; public event EventHandler CloseRequested; @@ -46,18 +42,15 @@ public SearchPanel() searchTextBox.SelectAll(); } - public void UpdateMatchCount(List<(int Row, int Column)> results, int currentIndex) + public void UpdateMatchCount(int totalCount, int currentIndex) { - _searchResults = results ?? new List<(int, int)>(); - _currentResultIndex = currentIndex; - - if (_searchResults.Count == 0) + if (totalCount == 0) { matchCountText.Text = string.IsNullOrEmpty(searchTextBox.Text) ? "" : "0/0"; } else { - matchCountText.Text = $"{currentIndex + 1}/{_searchResults.Count}"; + matchCountText.Text = $"{currentIndex + 1}/{totalCount}"; } } diff --git a/QuickLook.Plugin/QuickLook.Plugin.CsvViewer/CsvViewerPanel.xaml.cs b/QuickLook.Plugin/QuickLook.Plugin.CsvViewer/CsvViewerPanel.xaml.cs index 23ea01fbe..b1e7434f2 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.CsvViewer/CsvViewerPanel.xaml.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.CsvViewer/CsvViewerPanel.xaml.cs @@ -36,10 +36,14 @@ namespace QuickLook.Plugin.CsvViewer; public partial class CsvViewerPanel : UserControl { + // Highlight color for search results (semi-transparent yellow) + private static readonly SolidColorBrush HighlightBrush = new SolidColorBrush(Color.FromArgb(128, 255, 255, 0)); + private List<(int Row, int Column)> _searchResults = new List<(int, int)>(); private int _currentResultIndex = -1; private string _currentSearchText = string.Empty; private bool _currentMatchCase; + private DataGridCell _highlightedCell; // Track currently highlighted cell for efficient clearing public CsvViewerPanel() { @@ -129,7 +133,7 @@ private void PerformSearch() if (string.IsNullOrEmpty(_currentSearchText)) { - searchPanel.UpdateMatchCount(_searchResults, _currentResultIndex); + searchPanel.UpdateMatchCount(0, _currentResultIndex); return; } @@ -153,7 +157,7 @@ private void PerformSearch() NavigateToCurrentResult(); } - searchPanel.UpdateMatchCount(_searchResults, _currentResultIndex); + searchPanel.UpdateMatchCount(_searchResults.Count, _currentResultIndex); } private void NavigateToNextResult() @@ -163,7 +167,7 @@ private void NavigateToNextResult() _currentResultIndex = (_currentResultIndex + 1) % _searchResults.Count; NavigateToCurrentResult(); - searchPanel.UpdateMatchCount(_searchResults, _currentResultIndex); + searchPanel.UpdateMatchCount(_searchResults.Count, _currentResultIndex); } private void NavigateToPreviousResult() @@ -173,7 +177,7 @@ private void NavigateToPreviousResult() _currentResultIndex = (_currentResultIndex - 1 + _searchResults.Count) % _searchResults.Count; NavigateToCurrentResult(); - searchPanel.UpdateMatchCount(_searchResults, _currentResultIndex); + searchPanel.UpdateMatchCount(_searchResults.Count, _currentResultIndex); } private void NavigateToCurrentResult() @@ -199,6 +203,9 @@ private void NavigateToCurrentResult() private void HighlightCurrentCell(int rowIndex, int colIndex) { + // Clear previous highlight first + ClearHighlighting(); + try { var row = dataGrid.ItemContainerGenerator.ContainerFromIndex(rowIndex) as DataGridRow; @@ -210,38 +217,26 @@ private void HighlightCurrentCell(int rowIndex, int colIndex) var cell = presenter.ItemContainerGenerator.ContainerFromIndex(colIndex) as DataGridCell; if (cell != null) { - cell.Background = new SolidColorBrush(Color.FromArgb(128, 255, 255, 0)); // Semi-transparent yellow + cell.Background = HighlightBrush; + _highlightedCell = cell; } } } } - catch + catch (InvalidOperationException) { - // Ignore errors when highlighting + // Can occur when visual tree is being rebuilt during scrolling. + // Safe to ignore as the cell will be highlighted on next navigation. } } private void ClearHighlighting() { - // Reset cell backgrounds by iterating through visible rows - foreach (var item in dataGrid.Items) + // Only clear the previously highlighted cell instead of iterating all cells + if (_highlightedCell != null) { - var row = dataGrid.ItemContainerGenerator.ContainerFromItem(item) as DataGridRow; - if (row != null) - { - var presenter = FindVisualChild(row); - if (presenter != null) - { - for (int i = 0; i < dataGrid.Columns.Count; i++) - { - var cell = presenter.ItemContainerGenerator.ContainerFromIndex(i) as DataGridCell; - if (cell != null) - { - cell.ClearValue(DataGridCell.BackgroundProperty); - } - } - } - } + _highlightedCell.ClearValue(DataGridCell.BackgroundProperty); + _highlightedCell = null; } }