Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions windows/ScreenHere/ClipboardController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ internal sealed class ClipboardController
private readonly ClipboardHistoryStore store = new();
private readonly ClipboardImageStore images = new();
private readonly Dictionary<string, BitmapSource> thumbnails = new();
/// The pictures whose thumbnail is being decoded, so a list that is
/// rebuilt while they arrive does not ask for the same one again.
private readonly HashSet<string> thumbnailsAsked = [];
private readonly DispatcherTimer saveTimer = new() { Interval = TimeSpan.FromSeconds(2) };
private readonly DispatcherTimer readTimer = new() { Interval = TimeSpan.FromMilliseconds(90) };
private HwndSource? listener;
Expand Down Expand Up @@ -449,12 +452,27 @@ private void ReadNextCaption()
});
}

/// The small picture a row leads with, once it has been decoded. A
/// thumbnail is a read from disk and a decompression, and a history full of
/// screenshots did every one of them on the interface's thread before the
/// list drew a single row — which is why it took so long to appear and
/// answered nothing while it did. Asked for here, drawn when it is there.
public BitmapSource? Thumbnail(ClipImage image)
{
if (thumbnails.TryGetValue(image.Digest, out var cached)) return cached;
var small = images.Thumbnail(image);
if (small != null) thumbnails[image.Digest] = small;
return small;
if (posed || !thumbnailsAsked.Add(image.Digest)) return null;
Task.Run(() =>
{
// Frozen where it is decoded, so the interface can use it as it is.
var small = images.Thumbnail(image);
if (small == null) return;
Application.Current?.Dispatcher.BeginInvoke(() =>
{
thumbnails[image.Digest] = small;
Changed?.Invoke();
});
});
return null;
}

/// Where the picture sits on disk, so a row can be dragged into any app.
Expand All @@ -479,6 +497,7 @@ public void Clear()
captionsAsked.Clear();
store.Delete();
thumbnails.Clear();
thumbnailsAsked.Clear();
FileThumbnails.Shared.Clear();
LinkPreviews.Shared.Clear();
Task.Run(images.DeleteAll);
Expand Down Expand Up @@ -518,7 +537,11 @@ private void Apply(ClipboardHistory next)
saveTimer.Stop();
saveTimer.Start();
}
foreach (var digest in dropped) thumbnails.Remove(digest);
foreach (var digest in dropped)
{
thumbnails.Remove(digest);
thumbnailsAsked.Remove(digest);
}
if (dropped.Count > 0 && !posed) Task.Run(() => images.Remove(dropped));
Changed?.Invoke();
}
Expand Down
41 changes: 34 additions & 7 deletions windows/ScreenHere/HistoryWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Threading;

namespace ScreenHere;

Expand Down Expand Up @@ -44,6 +45,13 @@ internal sealed class HistoryWindow : Window
private Point lastPointer;
/// The window that had the keyboard before the list took it.
private IntPtr previous;
/// Rebuilding the list is the expensive part of it, and what the list is
/// waiting for arrives one piece at a time: a picture decoded, the words
/// read in another, a page's title, a file's icon. Rebuilt once they have
/// settled rather than once per piece — a history full of screenshots
/// rebuilt itself a hundred times over, and a list rebuilt under the
/// pointer is a list that cannot be scrolled.
private readonly DispatcherTimer redraw = new() { Interval = TimeSpan.FromMilliseconds(200) };

public static bool IsOpen => current != null;

Expand Down Expand Up @@ -84,14 +92,20 @@ public HistoryWindow()
Content = Ui.Surface(Build(), radius: 14, margin: ShadowRoom, shadow: 0.4);
lastPointer = Displays.Pointer();

clipboard.Changed += Reload;
links.Changed += Reload;
filePictures.Changed += Reload;
redraw.Tick += (_, _) =>
{
redraw.Stop();
Reload(keepPlace: true);
};
clipboard.Changed += ReloadSoon;
links.Changed += ReloadSoon;
filePictures.Changed += ReloadSoon;
Closed += (_, _) =>
{
clipboard.Changed -= Reload;
links.Changed -= Reload;
filePictures.Changed -= Reload;
redraw.Stop();
clipboard.Changed -= ReloadSoon;
links.Changed -= ReloadSoon;
filePictures.Changed -= ReloadSoon;
if (current == this)
{
current = null;
Expand Down Expand Up @@ -193,8 +207,17 @@ private UIElement Build()
private static Border Divider() =>
new Border { Height = 1 }.With(b => b.SetResourceReference(Border.BackgroundProperty, "P10"));

private void Reload()
private void ReloadSoon()
{
redraw.Stop();
redraw.Start();
}

/// `keepPlace` for a rebuild the user did not ask for: what arrived while
/// they were reading is no reason to take them back to the top of the list.
private void Reload(bool keepPlace = false)
{
var place = keepPlace ? scroller.VerticalOffset : 0;
results = clipboard.History.Matching(search.Text);
selection = Math.Clamp(selection, 0, Math.Max(0, results.Count - 1));
if (clipboard.History.Items.Count == 0) confirmingClear = false;
Expand All @@ -209,6 +232,10 @@ private void Reload()
}
body.Content = results.Count == 0 ? EmptyState() : scroller;
RefreshFooter();
// Once the new rows have been measured: until then the list is one row
// tall as far as the scroller knows, and anywhere but the top is out
// of its reach.
if (place > 0) Dispatcher.BeginInvoke(() => scroller.ScrollToVerticalOffset(place), DispatcherPriority.Loaded);
}

private UIElement EmptyState()
Expand Down
20 changes: 7 additions & 13 deletions windows/ScreenHere/Memory.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
using System.Diagnostics;
using System.Runtime;
using System.Runtime.InteropServices;
using System.Windows.Threading;

namespace ScreenHere;

/// A tray app spends nearly all its time doing nothing, and should weigh
/// next to nothing while it does. A capture is tens of megabytes of pixels for
/// a second; once things have gone quiet, they are handed back to Windows.
/// a second; once things have gone quiet, they are collected.
///
/// What is no longer done here is emptying the working set. Handing every page
/// back to Windows read well in Task Manager, but the next panel or list then
/// read itself back from disk — with an antivirus between the two, that is
/// seconds of nothing before anything appears, which is what the app being
/// slow to open looked like from the outside.
internal static class Memory
{
private static DispatcherTimer? timer;
Expand Down Expand Up @@ -39,17 +43,7 @@ private static DispatcherTimer Make()
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
try
{
using var process = Process.GetCurrentProcess();
EmptyWorkingSet(process.Handle);
}
catch
{
}
};
return made;
}

[DllImport("psapi.dll")] private static extern bool EmptyWorkingSet(IntPtr process);
}
25 changes: 23 additions & 2 deletions windows/ScreenHere/PanelWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ internal sealed class PanelWindow : Window
private readonly ContentControl update = new() { Focusable = false };
private readonly DispatcherTimer poll = new() { Interval = TimeSpan.FromMilliseconds(100) };
private bool closing;
/// The ··· menu while it is up. It is a window of its own, so a click in
/// it is a click outside the panel.
private ContextMenu? openMenu;
private string held = "";
private string? problem;
private Func<Size, Point>? anchor;
Expand Down Expand Up @@ -136,8 +139,13 @@ public PanelWindow(PanelPose? pose)
}
Memory.SettleSoon(3);
};
// Clicking anywhere else dismisses it, like any flyout.
Deactivated += (_, _) => Dismiss();
// Clicking anywhere else dismisses it, like any flyout — but the ···
// menu is a window of its own, and the panel handing the keyboard to
// its own menu is not the user going elsewhere.
Deactivated += (_, _) =>
{
if (openMenu?.IsOpen != true) Dismiss();
};
// The panel grows and shrinks with what is switched on; it stays
// attached to the edge it opened from.
SizeChanged += (_, _) =>
Expand All @@ -161,6 +169,9 @@ private void Dismiss()
{
if (closing) return;
closing = true;
// Its placement target is about to go: a menu left open over nothing
// has no panel left to close it, and stays on screen.
if (openMenu != null) openMenu.IsOpen = false;
Tray.Shared.PanelClosedAt = DateTime.Now;
Close();
}
Expand All @@ -170,6 +181,11 @@ private void Dismiss()
private void ClickedAt(int x, int y)
{
if (closing) return;
// Choosing something in the ··· menu is a click outside the panel, and
// closing the panel from under an open menu left the menu behind,
// with nothing above it to dismiss it. The menu closes itself; the
// click that closes it is not the panel's to act on.
if (openMenu?.IsOpen == true) return;
var handle = new System.Windows.Interop.WindowInteropHelper(this).Handle;
if (handle == IntPtr.Zero || !Native.GetWindowRect(handle, out var frame)) return;
var shadow = (int)(ShadowRoom * VisualTreeHelper.GetDpi(this).DpiScaleX);
Expand Down Expand Up @@ -307,6 +323,11 @@ private UIElement Footer()
Dismiss();
Tray.Shared.SetHidden(true);
}));
menu.Closed += (_, _) =>
{
if (openMenu == menu) openMenu = null;
};
openMenu = menu;
menu.IsOpen = true;
}, "More");
footer.Children.Add(more.Docked(Dock.Right));
Expand Down
Loading