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;
}
}