-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMainWindow.xaml.cs
More file actions
404 lines (337 loc) · 16.3 KB
/
Copy pathMainWindow.xaml.cs
File metadata and controls
404 lines (337 loc) · 16.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Threading;
using RAM_Cleaner.Services;
using Forms = System.Windows.Forms;
using Drawing = System.Drawing;
namespace RAM_Cleaner
{
public partial class MainWindow : Window
{
private const int MaxHistoryPoints = 40;
private readonly DispatcherTimer _refreshTimer = new DispatcherTimer();
private readonly DispatcherTimer _autoOptimizeTimer = new DispatcherTimer();
private readonly DispatcherTimer _recoveredHideTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(5) };
private readonly List<int> _historyPercents = new List<int>();
private Forms.NotifyIcon _trayIcon;
private Drawing.Icon _trayIconImage;
private AppSettings _settings;
private bool _isOptimizing;
private bool _isExiting;
private int _lastPercentUsed;
private DateTime? _lastOptimizedAt;
public MainWindow()
{
InitializeComponent();
_settings = AppSettings.Load();
if (!_settings.HasShownFirstRunDisclaimer)
{
new FirstRunWindow().ShowDialog();
_settings.HasShownFirstRunDisclaimer = true;
_settings.Save();
}
SetupTrayIcon();
ApplyDisplayMode();
RefreshMemoryDisplay();
_refreshTimer.Tick += (s, e) => { if (!_isOptimizing) RefreshMemoryDisplay(); };
ApplyRefreshSchedule();
ApplyAutoOptimizeSchedule();
_recoveredHideTimer.Tick += RecoveredHideTimer_Tick;
Closing += MainWindow_Closing;
if (_settings.LaunchMinimized)
{
// Hide during Loaded (just before the window would actually appear on screen)
// instead of calling Hide() here, so there's no visible flash on startup.
ShowInTaskbar = false;
Loaded += (s, e) => Hide();
}
}
// ===================== Memory display =====================
private void RefreshMemoryDisplay()
{
try
{
var status = MemoryOptimizer.GetStatus();
TotalMemoryText.Text = status.TotalDisplay;
UsedMemoryText.Text = status.UsedDisplay;
FreeMemoryText.Text = status.FreeDisplay;
_lastPercentUsed = status.PercentUsed;
UpdateProgressFillWidth();
PercentText.Text = $"{status.PercentUsed}% Used";
ChartPercentText.Text = $"{status.PercentUsed}% Used";
_historyPercents.Add(status.PercentUsed);
if (_historyPercents.Count > MaxHistoryPoints)
_historyPercents.RemoveAt(0);
UpdateSparkline();
if (_trayIcon != null)
_trayIcon.Text = $"RAM Cleaner - {status.PercentUsed}% used";
if (_settings.AutoOptimizeAtThreshold && !_isOptimizing && status.PercentUsed >= _settings.AutoOptimizeThresholdPercent
&& !(_settings.SkipAutoOptimizeWhenFullscreen && FullscreenDetector.IsForegroundFullscreen()))
_ = RunOptimizeAsync(isAutomatic: true);
}
catch
{
// A transient failure to read memory status just skips this tick - retried in 2s.
}
}
private void ProgressTrack_SizeChanged(object sender, System.Windows.SizeChangedEventArgs e) => UpdateProgressFillWidth();
private void UpdateProgressFillWidth() =>
ProgressFill.Width = ProgressTrack.ActualWidth * (_lastPercentUsed / 100.0);
private void SparklineTrack_SizeChanged(object sender, System.Windows.SizeChangedEventArgs e) => UpdateSparkline();
// Redraws the memory-usage history line. Right-aligns points so the most recent sample is
// always at the right edge, even before the rolling buffer has filled up.
private void UpdateSparkline()
{
double width = SparklineTrack.ActualWidth;
double height = SparklineTrack.ActualHeight;
if (width <= 0 || height <= 0 || _historyPercents.Count < 2)
{
HistorySparkline.Points = null;
return;
}
var points = new PointCollection();
double stepX = width / (MaxHistoryPoints - 1);
int startIndex = MaxHistoryPoints - _historyPercents.Count;
for (int i = 0; i < _historyPercents.Count; i++)
{
double x = (startIndex + i) * stepX;
double y = height - (_historyPercents[i] / 100.0 * height);
points.Add(new Point(x, y));
}
HistorySparkline.Points = points;
}
// Formats how long it had been since the PREVIOUS optimize - called once, right when a new
// optimize finishes. The very first optimize ever (no previous run to compare against) just
// shows "just now".
private static string FormatTimeSincePreviousOptimize(DateTime? previousOptimizedAt, DateTime currentOptimizedAt)
{
if (previousOptimizedAt == null)
return "Last optimized: just now";
var elapsed = currentOptimizedAt - previousOptimizedAt.Value;
if (elapsed < TimeSpan.Zero) elapsed = TimeSpan.Zero; // guard against any clock drift
int totalSeconds = (int)elapsed.TotalSeconds;
int hours = totalSeconds / 3600;
int minutes = (totalSeconds % 3600) / 60;
int seconds = totalSeconds % 60;
string Plural(int n, string unit) => $"{n} {unit}{(n == 1 ? "" : "s")}";
string when;
if (hours > 0)
when = $"{Plural(hours, "hour")} {Plural(minutes, "minute")} {Plural(seconds, "second")} ago";
else if (minutes > 0)
when = $"{Plural(minutes, "minute")} {Plural(seconds, "second")} ago";
else
when = $"{Plural(seconds, "second")} ago";
return $"Last optimized: {when}";
}
// Shows either the progress bar or the chart, never both, based on Options > Usage display.
private void ApplyDisplayMode()
{
bool showChart = _settings.DisplayMode == "Chart";
ProgressTrack.Visibility = showChart ? Visibility.Collapsed : Visibility.Visible;
ChartDisplay.Visibility = showChart ? Visibility.Visible : Visibility.Collapsed;
if (showChart)
UpdateSparkline(); // the chart may not have been sized/drawn yet if it was hidden until now
}
// Falls back to a 1-second default when the person hasn't turned on a custom refresh interval.
private void ApplyRefreshSchedule()
{
_refreshTimer.Stop();
int amount = Math.Max(1, _settings.RefreshIntervalAmount);
_refreshTimer.Interval = _settings.RefreshIntervalEnabled
? (_settings.RefreshIntervalUnit == "Minutes" ? TimeSpan.FromMinutes(amount) : TimeSpan.FromSeconds(amount))
: TimeSpan.FromSeconds(1);
_refreshTimer.Start();
}
private void ApplyAutoOptimizeSchedule()
{
_autoOptimizeTimer.Stop();
_autoOptimizeTimer.Tick -= AutoOptimizeTimer_Tick;
if (!_settings.AutoOptimizeEnabled) return;
int amount = Math.Max(1, _settings.AutoOptimizeIntervalMinutes);
_autoOptimizeTimer.Interval = _settings.AutoOptimizeIntervalUnit == "Minutes"
? TimeSpan.FromMinutes(amount)
: TimeSpan.FromSeconds(amount);
_autoOptimizeTimer.Tick += AutoOptimizeTimer_Tick;
_autoOptimizeTimer.Start();
}
private async void AutoOptimizeTimer_Tick(object sender, EventArgs e)
{
if (_settings.SkipAutoOptimizeWhenFullscreen && FullscreenDetector.IsForegroundFullscreen())
return; // don't interrupt a fullscreen app (game, video, presentation) with a trim
await RunOptimizeAsync(isAutomatic: true);
}
// ===================== Optimize =====================
private async void Optimize_Click(object sender, RoutedEventArgs e) => await RunOptimizeAsync();
private async Task RunOptimizeAsync(bool isAutomatic = false)
{
if (_isOptimizing) return;
_isOptimizing = true;
OptimizeButton.IsEnabled = false;
_recoveredHideTimer.Stop();
RecoveredPanel.BeginAnimation(OpacityProperty, null); // cancel any fade-out in progress
RecoveredPanel.Opacity = 1;
RecoveredPanel.Visibility = Visibility.Collapsed;
LastOptimizedText.BeginAnimation(OpacityProperty, null);
LastOptimizedText.Opacity = 1;
LastOptimizedText.Visibility = Visibility.Collapsed;
try
{
var excluded = _settings.ExcludeProcessesEnabled ? _settings.ExcludedProcesses : null;
ulong recovered = await Task.Run(() => MemoryOptimizer.Optimize(excluded));
RecoveredText.Text = $"{MemoryStatus.FormatBytes(recovered)} recovered.";
// Capture the PREVIOUS optimize time before overwriting it - the footer text
// describes the gap between that run and this one, not "how long since this click".
DateTime? previousOptimizedAt = _lastOptimizedAt;
_lastOptimizedAt = DateTime.Now;
LastOptimizedText.Text = FormatTimeSincePreviousOptimize(previousOptimizedAt, _lastOptimizedAt.Value);
RefreshMemoryDisplay();
// Only for unattended runs (scheduled/threshold) while the window is hidden in the
// tray - a manual click already has its own visible feedback in the footer.
if (isAutomatic && !IsVisible && _settings.ShowNotificationToast)
{
_trayIcon?.ShowBalloonTip(3000, "RAM Cleaner",
$"Auto-optimize freed {MemoryStatus.FormatBytes(recovered)}.", Forms.ToolTipIcon.Info);
}
}
catch
{
// A transient failure to read/trim memory (e.g. GlobalMemoryStatusEx failing under
// memory pressure - exactly when this app is most likely to be used) shouldn't crash
// the app or leave Optimize permanently disabled. Tell the person and let them retry.
RecoveredText.Text = "Optimize failed - please try again.";
}
finally
{
// Always runs, even on failure above - this is what guarantees the button and the
// reentrancy guard never get stuck.
RecoveredPanel.Visibility = Visibility.Visible;
LastOptimizedText.Visibility = Visibility.Visible;
_recoveredHideTimer.Start();
OptimizeButton.IsEnabled = true;
_isOptimizing = false;
}
}
// Fades both the "X recovered" and "Last optimized" lines out together, 5 seconds after
// they last appeared - between optimizes, the footer goes back to blank until the next run.
private void RecoveredHideTimer_Tick(object sender, EventArgs e)
{
_recoveredHideTimer.Stop();
var fadeRecovered = new DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(400));
fadeRecovered.Completed += (s, args) => RecoveredPanel.Visibility = Visibility.Collapsed;
RecoveredPanel.BeginAnimation(OpacityProperty, fadeRecovered);
var fadeLastOptimized = new DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(400));
fadeLastOptimized.Completed += (s, args) => LastOptimizedText.Visibility = Visibility.Collapsed;
LastOptimizedText.BeginAnimation(OpacityProperty, fadeLastOptimized);
}
// ===================== Window chrome =====================
private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.ButtonState == MouseButtonState.Pressed)
DragMove();
}
// Blocks the Tab key from cycling focus between the buttons - this window has no
// form fields to tab through, so Tab is disabled entirely rather than just reordered.
private void Window_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Tab)
e.Handled = true;
}
private void Minimize_Click(object sender, RoutedEventArgs e) => WindowState = WindowState.Minimized;
private void Close_Click(object sender, RoutedEventArgs e) => Close();
private void Window_StateChanged(object sender, EventArgs e)
{
if (WindowState == WindowState.Minimized && _settings.MinimizeToTrayOnClose)
{
Hide();
ShowInTaskbar = false;
}
}
private void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
if (_isExiting || !_settings.MinimizeToTrayOnClose)
{
_trayIcon?.Dispose();
_trayIconImage?.Dispose();
return;
}
// Close button sends the app to the tray instead of exiting outright, matching the
// typical behavior of always-on-standby memory cleaners.
e.Cancel = true;
WindowState = WindowState.Minimized;
}
// ===================== Tray icon =====================
private void SetupTrayIcon()
{
var menu = new Forms.ContextMenuStrip();
menu.Items.Add("Open", null, (s, e) => RestoreFromTray());
menu.Items.Add("Optimize now", null, async (s, e) => await RunOptimizeAsync());
menu.Items.Add(new Forms.ToolStripSeparator());
menu.Items.Add("Exit", null, (s, e) => ExitApplication());
_trayIconImage = LoadAppIcon();
_trayIcon = new Forms.NotifyIcon
{
Icon = _trayIconImage,
Visible = true,
Text = "RAM Cleaner",
ContextMenuStrip = menu
};
_trayIcon.DoubleClick += (s, e) => RestoreFromTray();
}
// Reads icon.ico from the app's own embedded resources (the same file used for the window
// and titlebar icons) so the tray icon matches instead of showing a generic system icon.
// Falls back to the system default if it can't be read for any reason.
private static Drawing.Icon LoadAppIcon()
{
try
{
var info = Application.GetResourceStream(new Uri("pack://application:,,,/icon.ico"));
if (info != null)
{
using (info.Stream)
return new Drawing.Icon(info.Stream);
}
}
catch
{
// Fall through to the system default below.
}
return Drawing.SystemIcons.Application;
}
private void RestoreFromTray()
{
Show();
ShowInTaskbar = true;
WindowState = WindowState.Normal;
Activate();
}
private void Exit_Click(object sender, RoutedEventArgs e) => ExitApplication();
private void ExitApplication()
{
_isExiting = true;
_trayIcon?.Dispose();
_trayIconImage?.Dispose();
Application.Current.Shutdown();
}
// ===================== Options / About =====================
private void Options_Click(object sender, RoutedEventArgs e)
{
var optionsWindow = new OptionsWindow(_settings) { Owner = this };
if (optionsWindow.ShowDialog() == true)
{
_settings = optionsWindow.Settings;
ApplyAutoOptimizeSchedule();
ApplyRefreshSchedule();
ApplyDisplayMode();
}
}
private void About_Click(object sender, RoutedEventArgs e)
{
new AboutWindow { Owner = this }.ShowDialog();
}
}
}