Compare commits

...

6 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
6e3e02a6f8 Address code review feedback for CSV viewer search panel
Co-authored-by: emako <24737061+emako@users.noreply.github.com>
2025-11-28 07:54:29 +00:00
copilot-swe-agent[bot]
2792e8d708 Add search panel support for CSV viewer with Ctrl+F
Co-authored-by: emako <24737061+emako@users.noreply.github.com>
2025-11-28 07:51:16 +00:00
copilot-swe-agent[bot]
a234e469b2 Initial plan 2025-11-28 07:43:42 +00:00
ema
76dfa34928 Update NuGet package versions in project files
Some checks failed
MSBuild / build (push) Has been cancelled
MSBuild / publish (push) Has been cancelled
Upgraded several NuGet dependencies across multiple csproj files, including Microsoft.Web.WebView2, Google.Protobuf, MsgReader, bblanchon.PDFiumV8.Win32, and Lib.Harmony. This ensures compatibility with the latest features and bug fixes from upstream packages.
2025-11-26 01:07:28 +08:00
Copilot
bf7de3bd8e Add ShowInTaskbar setting to display window in taskbar (#1818) 2025-11-26 00:52:12 +08:00
Copilot
1adb7ce980 Add option to disable automatic update check at startup (#1819) 2025-11-26 00:45:15 +08:00
13 changed files with 444 additions and 8 deletions

View File

@@ -0,0 +1,85 @@
<UserControl x:Class="QuickLook.Plugin.CsvViewer.Controls.SearchPanel"
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"
d:DesignHeight="35"
d:DesignWidth="300"
mc:Ignorable="d">
<Border Margin="0,-1,16,0"
Padding="2"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Background="{DynamicResource WindowBackground}"
BorderBrush="#102E2E3E"
BorderThickness="1"
CornerRadius="4"
Cursor="Arrow">
<StackPanel Orientation="Horizontal">
<TextBox Name="searchTextBox"
Width="150"
Height="29"
Padding="4,5,0,0"
FocusVisualStyle="{x:Null}"
Focusable="True"
KeyDown="SearchTextBox_KeyDown"
TextChanged="SearchTextBox_TextChanged" />
<TextBlock Name="matchCountText"
Width="50"
Margin="4,0,0,0"
VerticalAlignment="Center"
FontSize="11"
Foreground="{DynamicResource WindowTextForeground}"
Text="" />
<Button Width="24"
Height="24"
Margin="3,3,0,3"
Padding="0"
Click="FindPrevious_Click"
ToolTip="Find Previous (Shift+Enter)">
<!-- ChevronUp -->
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
FontFamily="{DynamicResource SymbolThemeFontFamily}"
FontSize="12"
Text="&#xe70e;" />
</Button>
<Button Width="24"
Height="24"
Margin="3"
Padding="0"
Click="FindNext_Click"
ToolTip="Find Next (Enter)">
<!-- ChevronDown -->
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
FontFamily="{DynamicResource SymbolThemeFontFamily}"
FontSize="12"
Text="&#xe70d;" />
</Button>
<CheckBox Name="matchCaseCheckBox"
Margin="3,0"
VerticalAlignment="Center"
Checked="MatchCase_Changed"
Content="Match case"
Unchecked="MatchCase_Changed" />
<Button Width="16"
Height="16"
Padding="0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Click="CloseSearch_Click"
Focusable="False"
ToolTip="Close (Escape)">
<!-- CalculatorMultiply -->
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
FontFamily="{DynamicResource SymbolThemeFontFamily}"
FontSize="10"
Text="&#xe947;" />
</Button>
</StackPanel>
</Border>
</UserControl>

View File

@@ -0,0 +1,124 @@
// 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 <http://www.gnu.org/licenses/>.
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace QuickLook.Plugin.CsvViewer.Controls;
public partial class SearchPanel : UserControl
{
public event EventHandler<SearchEventArgs> SearchRequested;
public event EventHandler<NavigateEventArgs> 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(int totalCount, int currentIndex)
{
if (totalCount == 0)
{
matchCountText.Text = string.IsNullOrEmpty(searchTextBox.Text) ? "" : "0/0";
}
else
{
matchCountText.Text = $"{currentIndex + 1}/{totalCount}";
}
}
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;
}
}

View File

@@ -1,12 +1,14 @@
<UserControl x:Class="QuickLook.Plugin.CsvViewer.CsvViewerPanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:QuickLook.Plugin.CsvViewer.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:QuickLook.Plugin.CsvViewer"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:Name="csvViewer"
d:DesignHeight="300"
d:DesignWidth="300"
Focusable="True"
mc:Ignorable="d">
<UserControl.Resources>
<ResourceDictionary>
@@ -30,5 +32,7 @@
ItemsSource="{Binding Path=Rows, ElementName=csvViewer}"
RowBackground="#00FFFFFF"
VerticalGridLinesBrush="#19000000" />
<controls:SearchPanel x:Name="searchPanel"
Visibility="Collapsed" />
</Grid>
</UserControl>

View File

@@ -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.Linq;
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,228 @@ 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()
{
InitializeComponent();
KeyDown += CsvViewerPanel_KeyDown;
searchPanel.SearchRequested += SearchPanel_SearchRequested;
searchPanel.NavigateRequested += SearchPanel_NavigateRequested;
searchPanel.CloseRequested += SearchPanel_CloseRequested;
}
public List<string[]> 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(0, _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.Count, _currentResultIndex);
}
private void NavigateToNextResult()
{
if (_searchResults.Count == 0)
return;
_currentResultIndex = (_currentResultIndex + 1) % _searchResults.Count;
NavigateToCurrentResult();
searchPanel.UpdateMatchCount(_searchResults.Count, _currentResultIndex);
}
private void NavigateToPreviousResult()
{
if (_searchResults.Count == 0)
return;
_currentResultIndex = (_currentResultIndex - 1 + _searchResults.Count) % _searchResults.Count;
NavigateToCurrentResult();
searchPanel.UpdateMatchCount(_searchResults.Count, _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)
{
// Clear previous highlight first
ClearHighlighting();
try
{
var row = dataGrid.ItemContainerGenerator.ContainerFromIndex(rowIndex) as DataGridRow;
if (row != null)
{
var presenter = FindVisualChild<DataGridCellsPresenter>(row);
if (presenter != null)
{
var cell = presenter.ItemContainerGenerator.ContainerFromIndex(colIndex) as DataGridCell;
if (cell != null)
{
cell.Background = HighlightBrush;
_highlightedCell = cell;
}
}
}
}
catch (InvalidOperationException)
{
// 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()
{
// Only clear the previously highlighted cell instead of iterating all cells
if (_highlightedCell != null)
{
_highlightedCell.ClearValue(DataGridCell.BackgroundProperty);
_highlightedCell = null;
}
}
private static T FindVisualChild<T>(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<T>(child);
if (result != null)
{
return result;
}
}
return null;
}
public void LoadFile(string path)
{
const int limit = 10000;

View File

@@ -73,7 +73,7 @@
<ItemGroup>
<PackageReference Include="FreeTypeSharp" Version="3.0.1" />
<PackageReference Include="QuickLook.Typography.OpenFont" Version="1.0.1" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3537.50">
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3595.46">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

View File

@@ -61,7 +61,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3537.50">
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3595.46">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

View File

@@ -62,10 +62,10 @@
<PackageReference Include="Magick.NET-Q8-AnyCPU" Version="14.9.1">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3537.50">
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3595.46">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Google.Protobuf" Version="3.33.0">
<PackageReference Include="Google.Protobuf" Version="3.33.1">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="System.Memory" Version="4.6.3">

View File

@@ -55,7 +55,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MsgReader" Version="6.0.5" />
<PackageReference Include="MsgReader" Version="6.0.6" />
</ItemGroup>
<ItemGroup>

View File

@@ -61,7 +61,7 @@
<ItemGroup>
<PackageReference Include="UTF.Unknown" Version="2.6.0" />
<Reference Include="WindowsBase" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3537.50">
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3595.46">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

View File

@@ -64,7 +64,7 @@
<ItemGroup>
<PackageReference Include="PdfiumViewer.Updated" Version="2.14.5" />
<PackageReference Include="bblanchon.PDFiumV8.Win32" Version="143.0.7497" />
<PackageReference Include="bblanchon.PDFiumV8.Win32" Version="144.0.7543" />
<Reference Include="WindowsBase" />
</ItemGroup>

View File

@@ -259,6 +259,9 @@ public partial class App : Application
private void CheckUpdate()
{
if (SettingHelper.Get("DisableAutoUpdateCheck", false))
return;
if (DateTime.Now.Ticks - SettingHelper.Get<long>("LastUpdateTicks") < TimeSpan.FromDays(30).Ticks)
return;

View File

@@ -105,7 +105,7 @@
<PackageReference Include="WPF-UI.Violeta" Version="4.0.3.6">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Lib.Harmony" Version="2.4.1">
<PackageReference Include="Lib.Harmony" Version="2.4.2">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="UnblockZoneIdentifier" Version="1.0.0">

View File

@@ -64,6 +64,8 @@ public partial class ViewerWindow : Window
Topmost = SettingHelper.Get("Topmost", false);
buttonTop.Tag = Topmost ? "Top" : "Auto";
ShowInTaskbar = SettingHelper.Get("ShowInTaskbar", false);
buttonTop.Click += (_, _) =>
{
Topmost = !Topmost;