Skip to content

Commit d0d503b

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

File tree

9 files changed

+200
-58
lines changed

9 files changed

+200
-58
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
}

src/EventLogExpert/wwwroot/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
<script src="_framework/blazor.webview.js" autostart="false"></script>
2626
<script src="_content/Fluxor.Blazor.Web/scripts/index.js"></script>
27+
<script src="js/app.js"></script>
2728
<script src="js/context.js"></script>
2829
<script src="js/details_pane.js"></script>
2930
<script src="js/dropdowns.js"></script>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// App-global keydown handlers. Loaded once via index.html and intentionally
2+
// not tied to any component lifecycle, since the behavior they suppress is
3+
// WebView-wide (F5/Ctrl+R reload) or applies to the always-present
4+
// TableColumnMenu in MainLayout (Space-key default scroll).
5+
6+
// Prevent F5 and Ctrl+R from refreshing the WebView
7+
document.addEventListener("keydown",
8+
function(e) {
9+
if (e.key === "F5" || (e.ctrlKey && (e.key === "r" || e.key === "R"))) {
10+
e.preventDefault();
11+
}
12+
},
13+
true);
14+
15+
// Prevent the browser's default Space-key scroll when activating a
16+
// role="button"/menuitem inside the column menu (via keyboard). This runs
17+
// natively alongside Blazor's @onkeydown so Tab navigation still works.
18+
document.addEventListener("keydown",
19+
function(e) {
20+
if (e.key !== " ") {
21+
return;
22+
}
23+
24+
const target = e.target;
25+
if (target && target.closest &&
26+
target.closest('#table-column-menu [role="button"], #table-column-menu [role="menuitem"]')) {
27+
e.preventDefault();
28+
}
29+
});
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 & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,4 @@
11
(() => {
2-
// Prevent F5 and Ctrl+R from refreshing the WebView
3-
document.addEventListener("keydown",
4-
function(e) {
5-
if (e.key === "F5" || (e.ctrlKey && (e.key === "r" || e.key === "R"))) {
6-
e.preventDefault();
7-
}
8-
},
9-
true);
10-
11-
// Prevent the browser's default Space-key scroll when activating a
12-
// role="button"/menuitem inside the column menu (via keyboard). This runs
13-
// natively alongside Blazor's @onkeydown so Tab navigation still works.
14-
document.addEventListener("keydown",
15-
function(e) {
16-
if (e.key !== " ") {
17-
return;
18-
}
19-
20-
const target = e.target;
21-
if (target && target.closest &&
22-
target.closest('#table-column-menu [role="button"], #table-column-menu [role="menuitem"]')) {
23-
e.preventDefault();
24-
}
25-
});
26-
272
let activeDocumentListeners = [];
283
let controller = null;
294
let dotNetRef = null;
@@ -150,7 +125,9 @@
150125
const newWidth = parseInt(window.getComputedStyle(column).width, 10);
151126

152127
if (dotNetRef && colName) {
153-
dotNetRef.invokeMethodAsync("OnColumnResized", colName, newWidth);
128+
// Catch rejection in case the .NET object was disposed mid-drag
129+
// (component teardown, column set change, etc.).
130+
dotNetRef.invokeMethodAsync("OnColumnResized", colName, newWidth).catch(() => { });
154131
}
155132

156133
// Rebuild dividers after resize
@@ -181,6 +158,11 @@
181158
return;
182159
}
183160

161+
// Suppress the WebView's default arrow-key scroll while a focused
162+
// divider is being keyboard-resized.
163+
e.preventDefault();
164+
e.stopPropagation();
165+
184166
// Debounce keyboard resize persistence
185167
if (keyboardResizeTimer) {
186168
clearTimeout(keyboardResizeTimer);
@@ -191,7 +173,7 @@
191173
const newWidth = parseInt(window.getComputedStyle(column).width, 10);
192174

193175
if (dotNetRef && colName) {
194-
dotNetRef.invokeMethodAsync("OnColumnResized", colName, newWidth);
176+
dotNetRef.invokeMethodAsync("OnColumnResized", colName, newWidth).catch(() => { });
195177
}
196178

197179
keyboardResizeTimer = null;
@@ -278,7 +260,11 @@
278260
dragIndicator.style.left = `${drop.indicatorX}px`;
279261

280262
document.body.appendChild(dragIndicator);
281-
} else if (drop === false) {
263+
} else {
264+
// Either explicit cancel (drop === false, cursor over
265+
// source) or no header under cursor (drop === null,
266+
// e.g. dragged into the table body). Clear any stale
267+
// pendingTarget/indicator so mouseup doesn't act on it.
282268
removeIndicator();
283269
pendingTarget = null;
284270
}
@@ -298,7 +284,9 @@
298284
const sourceColName = getColumnName(dragSource);
299285

300286
if (dotNetRef && sourceColName) {
301-
dotNetRef.invokeMethodAsync("OnColumnReordered", sourceColName, pendingTarget, pendingInsertAfter);
287+
// Catch rejection in case the .NET object was disposed
288+
// mid-drag (component teardown, column set change, etc.).
289+
dotNetRef.invokeMethodAsync("OnColumnReordered", sourceColName, pendingTarget, pendingInsertAfter).catch(() => { });
302290
}
303291
}
304292

0 commit comments

Comments
 (0)