Skip to content

Commit ffdf4b5

Browse files
committed
Added details pane height preference
1 parent 23c883b commit ffdf4b5

File tree

7 files changed

+170
-33
lines changed

7 files changed

+170
-33
lines changed

src/EventLogExpert.UI.Tests/Store/EventTable/EventTableEffectsTests.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,12 @@ public async Task HandleResetColumnDefaults_ShouldResetAllColumnSettingsToDefaul
200200
_ = mockPreferencesProvider.Received(1).ColumnOrderPreference =
201201
Arg.Is<IEnumerable<ColumnName>>(o => !o.Any());
202202

203+
var expectedWidths = ColumnDefaults.Widths.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
204+
203205
mockDispatcher.Received(1).Dispatch(Arg.Is<EventTableAction.LoadColumnsCompleted>(action =>
204-
action.ColumnWidths.SequenceEqual(ColumnDefaults.Widths) &&
206+
action.ColumnWidths.Count == expectedWidths.Count &&
207+
action.ColumnWidths.All(kvp =>
208+
expectedWidths.ContainsKey(kvp.Key) && expectedWidths[kvp.Key] == kvp.Value) &&
205209
action.ColumnOrder.SequenceEqual(ColumnDefaults.Order) &&
206210
action.LoadedColumns[ColumnName.Level] == true &&
207211
action.LoadedColumns[ColumnName.DateAndTime] == true &&

src/EventLogExpert.UI/Interfaces/IPreferencesProvider.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ public interface IPreferencesProvider
1212

1313
IDictionary<ColumnName, int> ColumnWidthsPreference { get; set; }
1414

15+
int DetailsPaneHeightPreference { get; set; }
16+
1517
IEnumerable<string> DisabledDatabasesPreference { get; set; }
1618

1719
bool DisplayPaneSelectionPreference { get; set; }

src/EventLogExpert/Components/DetailsPane.razor.cs

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515

1616
namespace EventLogExpert.Components;
1717

18-
public sealed partial class DetailsPane : IDisposable
18+
public sealed partial class DetailsPane
1919
{
20+
private DotNetObjectReference<DetailsPane>? _dotNetRef;
2021
private bool _hasOpened = false;
2122
private bool _isVisible = false;
2223
private bool _isXmlVisible = false;
@@ -27,6 +28,8 @@ public sealed partial class DetailsPane : IDisposable
2728

2829
[Inject] private IJSRuntime JSRuntime { get; init; } = null!;
2930

31+
[Inject] private IPreferencesProvider PreferencesProvider { get; init; } = null!;
32+
3033
private DisplayEventModel? SelectedEvent { get; set; }
3134

3235
[Inject] private IStateSelection<EventLogState, ImmutableList<DisplayEventModel>> SelectedEventSelection { get; init; } = null!;
@@ -35,13 +38,42 @@ public sealed partial class DetailsPane : IDisposable
3538

3639
[Inject] private ITraceLogger TraceLogger { get; init; } = null!;
3740

38-
public void Dispose() => SelectedEventSelection.SelectedValueChanged -= OnSelectedEventChanged;
41+
[JSInvokable]
42+
public void OnDetailsPaneHeightChanged(int height)
43+
{
44+
if (height > 0)
45+
{
46+
PreferencesProvider.DetailsPaneHeightPreference = height;
47+
}
48+
}
49+
50+
protected override async ValueTask DisposeAsyncCore(bool disposing)
51+
{
52+
if (disposing)
53+
{
54+
SelectedEventSelection.SelectedValueChanged -= OnSelectedEventChanged;
55+
56+
try
57+
{
58+
await JSRuntime.InvokeVoidAsync("disposeDetailsPaneResizer");
59+
}
60+
catch (JSDisconnectedException) { }
61+
62+
_dotNetRef?.Dispose();
63+
}
64+
65+
await base.DisposeAsyncCore(disposing);
66+
}
3967

4068
protected override async Task OnAfterRenderAsync(bool firstRender)
4169
{
4270
if (firstRender)
4371
{
44-
await JSRuntime.InvokeVoidAsync("enableDetailsPaneResizer");
72+
_dotNetRef = DotNetObjectReference.Create(this);
73+
await JSRuntime.InvokeVoidAsync(
74+
"enableDetailsPaneResizer",
75+
_dotNetRef,
76+
PreferencesProvider.DetailsPaneHeightPreference);
4577
}
4678

4779
await base.OnAfterRenderAsync(firstRender);
@@ -60,6 +92,8 @@ protected override void OnInitialized()
6092

6193
private void HandleKeyDown(KeyboardEventArgs e)
6294
{
95+
if (e.Repeat) { return; }
96+
6397
if (e.Key is "Enter" or " ")
6498
{
6599
ToggleMenu();
@@ -68,6 +102,8 @@ private void HandleKeyDown(KeyboardEventArgs e)
68102

69103
private void HandleKeyDownXml(KeyboardEventArgs e)
70104
{
105+
if (e.Repeat) { return; }
106+
71107
if (e.Key is "Enter" or " ")
72108
{
73109
ToggleXml();

src/EventLogExpert/Services/PreferencesProvider.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public sealed class PreferencesProvider : IPreferencesProvider
1313
{
1414
private const string ColumnOrder = "column-order";
1515
private const string ColumnWidths = "column-widths";
16+
private const string DetailsPaneHeight = "details-pane-height";
1617
private const string DisabledDatabases = "disabled-databases";
1718
private const string DisplaySelectionEnabled = "display-selection-enabled";
1819
private const string EnabledEventTableColumns = "enabled-event-table-columns";
@@ -36,6 +37,12 @@ public IDictionary<ColumnName, int> ColumnWidthsPreference
3637
set => Preferences.Default.Set(ColumnWidths, JsonSerializer.Serialize(value));
3738
}
3839

40+
public int DetailsPaneHeightPreference
41+
{
42+
get => Preferences.Default.Get(DetailsPaneHeight, 0);
43+
set => Preferences.Default.Set(DetailsPaneHeight, value);
44+
}
45+
3946
public IEnumerable<string> DisabledDatabasesPreference
4047
{
4148
get => JsonSerializer.Deserialize<List<string>>(Preferences.Default.Get(DisabledDatabases, "[]")) ?? [];

src/EventLogExpert/Shared/Components/TableColumnMenu.razor.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ protected override void OnInitialized()
3030

3131
private static void HandleActivationKey(KeyboardEventArgs args, Action action)
3232
{
33+
// Ignore auto-repeat so holding Enter/Space doesn't dispatch the action
34+
// repeatedly while the key is held down.
35+
if (args.Repeat)
36+
{
37+
return;
38+
}
39+
3340
if (args.Key is "Enter" or " ")
3441
{
3542
action();
@@ -40,9 +47,6 @@ private static void HandleActivationKey(KeyboardEventArgs args, Action action)
4047

4148
private void ResetDefaults() => Dispatcher.Dispatch(new EventTableAction.ResetColumnDefaults());
4249

43-
private void ToggleColumn(ColumnName columnName)
44-
{
50+
private void ToggleColumn(ColumnName columnName) =>
4551
Dispatcher.Dispatch(new EventTableAction.ToggleColumn(columnName));
46-
Dispatcher.Dispatch(new EventTableAction.LoadColumns());
47-
}
4852
}
Lines changed: 92 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,102 @@
1-
window.enableDetailsPaneResizer = () => {
2-
const detailsPane = document.getElementById("details-pane");
3-
const resizer = document.getElementById("details-resizer");
1+
(() => {
2+
const detailsPaneState = {
3+
activeDocumentListeners: [],
4+
controller: null,
5+
dotNetRef: null
6+
};
47

5-
if (detailsPane == null || resizer == null) { return; }
8+
function trackDetailsDocumentListener(event, handler) {
9+
const entry = { event, handler };
10+
const options = detailsPaneState.controller ? { signal: detailsPaneState.controller.signal } : undefined;
11+
document.addEventListener(event, handler, options);
12+
detailsPaneState.activeDocumentListeners.push(entry);
613

7-
let y, h = 0;
14+
return () => {
15+
document.removeEventListener(event, handler);
16+
const i = detailsPaneState.activeDocumentListeners.indexOf(entry);
17+
if (i !== -1) {
18+
detailsPaneState.activeDocumentListeners.splice(i, 1);
19+
}
20+
};
21+
}
822

9-
const mouseMoveHandler = function(e) {
10-
const distance = e.clientY - y;
23+
window.enableDetailsPaneResizer = (dotNetRef, savedHeight) => {
24+
window.disposeDetailsPaneResizer();
1125

12-
detailsPane.style.height = `${h - distance}px`;
13-
};
26+
const detailsPane = document.getElementById("details-pane");
27+
const resizer = document.getElementById("details-resizer");
1428

15-
const mouseUpHandler = function() {
16-
document.removeEventListener("mousemove", mouseMoveHandler);
17-
document.removeEventListener("mouseup", mouseUpHandler);
18-
};
29+
if (detailsPane == null || resizer == null) {
30+
return;
31+
}
32+
33+
// Apply persisted height (if any) before user interaction. CSS supplies
34+
// the default height when no saved value exists. Clamp so a height saved
35+
// on a larger window can't overwhelm a smaller viewport on next launch.
36+
if (savedHeight && savedHeight > 0) {
37+
const maxHeight = Math.max(60, Math.floor(window.innerHeight * 0.8));
38+
detailsPane.style.height = `${Math.min(savedHeight, maxHeight)}px`;
39+
}
40+
41+
detailsPaneState.dotNetRef = dotNetRef;
42+
detailsPaneState.controller = new AbortController();
43+
const signal = detailsPaneState.controller.signal;
44+
45+
let y = 0;
46+
let h = 0;
47+
let untrackMove = null;
48+
let untrackUp = null;
49+
50+
const mouseMoveHandler = function(e) {
51+
const distance = e.clientY - y;
52+
// Match the column-resize minimum (avoids the pane vanishing entirely
53+
// and being un-grabbable). CSS min-height still applies on top.
54+
const newHeight = Math.max(30, h - distance);
55+
56+
detailsPane.style.height = `${newHeight}px`;
57+
};
1958

20-
const mouseDownHandler = function(e) {
21-
y = e.clientY;
59+
const mouseUpHandler = function() {
60+
if (untrackMove) { untrackMove(); untrackMove = null; }
61+
if (untrackUp) { untrackUp(); untrackUp = null; }
2262

23-
const styles = window.getComputedStyle(detailsPane);
24-
h = parseInt(styles.height, 10);
63+
const ref = detailsPaneState.dotNetRef;
2564

26-
document.addEventListener("mousemove", mouseMoveHandler);
27-
document.addEventListener("mouseup", mouseUpHandler);
65+
if (ref && detailsPane.isConnected) {
66+
const newHeight = parseInt(window.getComputedStyle(detailsPane).height, 10);
67+
// Catch rejection in case the .NET object was disposed mid-drag.
68+
ref.invokeMethodAsync("OnDetailsPaneHeightChanged", newHeight).catch(() => { });
69+
}
70+
};
71+
72+
const mouseDownHandler = function(e) {
73+
// Only respond to primary (left) button so right-click context menus
74+
// and middle-click are not intercepted.
75+
if (e.button !== 0) { return; }
76+
77+
y = e.clientY;
78+
79+
const styles = window.getComputedStyle(detailsPane);
80+
h = parseInt(styles.height, 10);
81+
82+
untrackMove = trackDetailsDocumentListener("mousemove", mouseMoveHandler);
83+
untrackUp = trackDetailsDocumentListener("mouseup", mouseUpHandler);
84+
};
85+
86+
resizer.addEventListener("mousedown", mouseDownHandler, { signal });
2887
};
2988

30-
resizer.addEventListener("mousedown", mouseDownHandler);
31-
};
89+
window.disposeDetailsPaneResizer = () => {
90+
if (detailsPaneState.controller) {
91+
detailsPaneState.controller.abort();
92+
detailsPaneState.controller = null;
93+
}
94+
95+
for (const { event, handler } of detailsPaneState.activeDocumentListeners) {
96+
document.removeEventListener(event, handler);
97+
}
98+
99+
detailsPaneState.activeDocumentListeners = [];
100+
detailsPaneState.dotNetRef = null;
101+
};
102+
})();

src/EventLogExpert/wwwroot/js/event_table.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,9 @@
150150
const newWidth = parseInt(window.getComputedStyle(column).width, 10);
151151

152152
if (dotNetRef && colName) {
153-
dotNetRef.invokeMethodAsync("OnColumnResized", colName, newWidth);
153+
// Catch rejection in case the .NET object was disposed mid-drag
154+
// (component teardown, column set change, etc.).
155+
dotNetRef.invokeMethodAsync("OnColumnResized", colName, newWidth).catch(() => { });
154156
}
155157

156158
// Rebuild dividers after resize
@@ -181,6 +183,11 @@
181183
return;
182184
}
183185

186+
// Suppress the WebView's default arrow-key scroll while a focused
187+
// divider is being keyboard-resized.
188+
e.preventDefault();
189+
e.stopPropagation();
190+
184191
// Debounce keyboard resize persistence
185192
if (keyboardResizeTimer) {
186193
clearTimeout(keyboardResizeTimer);
@@ -191,7 +198,7 @@
191198
const newWidth = parseInt(window.getComputedStyle(column).width, 10);
192199

193200
if (dotNetRef && colName) {
194-
dotNetRef.invokeMethodAsync("OnColumnResized", colName, newWidth);
201+
dotNetRef.invokeMethodAsync("OnColumnResized", colName, newWidth).catch(() => { });
195202
}
196203

197204
keyboardResizeTimer = null;
@@ -278,7 +285,11 @@
278285
dragIndicator.style.left = `${drop.indicatorX}px`;
279286

280287
document.body.appendChild(dragIndicator);
281-
} else if (drop === false) {
288+
} else {
289+
// Either explicit cancel (drop === false, cursor over
290+
// source) or no header under cursor (drop === null,
291+
// e.g. dragged into the table body). Clear any stale
292+
// pendingTarget/indicator so mouseup doesn't act on it.
282293
removeIndicator();
283294
pendingTarget = null;
284295
}
@@ -298,7 +309,9 @@
298309
const sourceColName = getColumnName(dragSource);
299310

300311
if (dotNetRef && sourceColName) {
301-
dotNetRef.invokeMethodAsync("OnColumnReordered", sourceColName, pendingTarget, pendingInsertAfter);
312+
// Catch rejection in case the .NET object was disposed
313+
// mid-drag (component teardown, column set change, etc.).
314+
dotNetRef.invokeMethodAsync("OnColumnReordered", sourceColName, pendingTarget, pendingInsertAfter).catch(() => { });
302315
}
303316
}
304317

0 commit comments

Comments
 (0)