Skip to content

Commit d4e1e94

Browse files
Merge pull request #7 from TheEightBot/feature/updated-tapping
2 parents 259f55e + b7a877d commit d4e1e94

6 files changed

Lines changed: 329 additions & 39 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,5 @@ artifacts/
3838
# OS generated files
3939
.DS_Store
4040
Thumbs.db
41+
42+
*.lscache

samples/SampleApp.Maui/MvvmActionsPage.xaml

Lines changed: 88 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,140 @@
1-
<?xml version="1.0" encoding="utf-8" ?>
1+
<?xml version="1.0" encoding="utf-8" ?>
22
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
33
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
44
xmlns:dg="clr-namespace:KumikoUI.Maui;assembly=KumikoUI.Maui"
55
xmlns:core="clr-namespace:KumikoUI.Core.Models;assembly=KumikoUI.Core"
66
xmlns:render="clr-namespace:KumikoUI.Core.Rendering;assembly=KumikoUI.Core"
7+
xmlns:editing="clr-namespace:KumikoUI.Core.Editing;assembly=KumikoUI.Core"
78
x:Class="SampleApp.Maui.MvvmActionsPage"
8-
Title="MVVM Row Actions">
9+
Title="MVVM + Column EditTriggers">
910

10-
<Grid RowDefinitions="Auto,*" Padding="0">
11-
<Label Text="MVVM Row Actions — Tap Delete or Details on any row"
11+
<Grid RowDefinitions="Auto,Auto,*" Padding="0">
12+
13+
<Label Text="Column-Level EditTriggers"
1214
FontSize="18"
1315
FontAttributes="Bold"
14-
Padding="16,12"
16+
Padding="16,12,16,2"
1517
Grid.Row="0" />
1618

19+
<!-- Per-column trigger legend -->
20+
<VerticalStackLayout Grid.Row="1" Padding="16,0,16,8" Spacing="2">
21+
<Label FontSize="12" TextColor="Gray">
22+
<Label.FormattedText>
23+
<FormattedString>
24+
<Span Text="Name " FontAttributes="Bold" />
25+
<Span Text="Single-tap, F2, or type to edit" FontAttributes="Italic" />
26+
<Span Text=" " FontAttributes="Bold" />
27+
<Span Text="Department " FontAttributes="Bold" />
28+
<Span Text="Double-tap or F2 only" FontAttributes="Italic" />
29+
</FormattedString>
30+
</Label.FormattedText>
31+
</Label>
32+
<Label FontSize="12" TextColor="Gray">
33+
<Label.FormattedText>
34+
<FormattedString>
35+
<Span Text="Level " FontAttributes="Bold" />
36+
<Span Text="Double-tap only" FontAttributes="Italic" />
37+
<Span Text=" " FontAttributes="Bold" />
38+
<Span Text="Salary " FontAttributes="Bold" />
39+
<Span Text="F2 key only (most protected)" FontAttributes="Italic" />
40+
<Span Text=" " FontAttributes="Bold" />
41+
<Span Text="Actions " FontAttributes="Bold" />
42+
<Span Text="Single-tap (column override)" FontAttributes="Italic" />
43+
</FormattedString>
44+
</Label.FormattedText>
45+
</Label>
46+
</VerticalStackLayout>
47+
1748
<!--
18-
x:Name="actionsGrid" is referenced below by the command bindings.
19-
actionsGrid.BindingContext = MvvmActionsViewModel (set in code-behind).
49+
Grid-level EditTriggers = Default (DoubleTap | F2Key | Typing).
50+
Each column below overrides this with its own EditTriggers value,
51+
demonstrating the full range of per-column trigger configurations.
52+
53+
x:Name="actionsGrid" is referenced by the command bindings below.
54+
BindingContext = MvvmActionsViewModel (set in code-behind).
2055
-->
2156
<dg:DataGridView x:Name="actionsGrid"
22-
Grid.Row="1"
57+
Grid.Row="2"
2358
ItemsSource="{Binding Employees}"
24-
EditTriggers="SingleTap,F2Key"
59+
EditTriggers="DoubleTap, F2Key, Typing"
2560
RowHeight="44"
2661
HeaderHeight="40">
2762
<dg:DataGridView.Columns>
63+
64+
<!-- Id \u2014 read-only; no EditTriggers needed -->
2865
<core:DataGridColumn Header="Id"
2966
PropertyName="Id"
3067
ColumnType="Numeric"
3168
Width="52"
3269
IsReadOnly="True"
3370
AllowTabStop="False" />
3471

72+
<!--
73+
Name \u2014 single-tap, F2, OR typing starts editing.
74+
Most accessible column; overrides the grid-level Default.
75+
-->
3576
<core:DataGridColumn Header="Name"
3677
PropertyName="Name"
3778
Width="160"
38-
IsReadOnly="True" />
79+
EditTriggers="SingleTap, F2Key, Typing" />
3980

81+
<!--
82+
Department \u2014 double-tap or F2 only.
83+
Typing excluded to prevent accidental edits on a ComboBox.
84+
-->
4085
<core:DataGridColumn Header="Department"
4186
PropertyName="Department"
87+
ColumnType="ComboBox"
4288
Width="130"
43-
IsReadOnly="True" />
89+
EditorItemsString="Engineering,Marketing,Finance,HR,Design,Product,Sales"
90+
EditTriggers="DoubleTap, F2Key" />
4491

92+
<!--
93+
Level \u2014 double-tap ONLY; no F2, no typing.
94+
Picker columns benefit from explicit intent before opening a scroll wheel.
95+
-->
4596
<core:DataGridColumn Header="Level"
4697
PropertyName="Level"
98+
ColumnType="Picker"
4799
Width="90"
48-
IsReadOnly="True" />
100+
EditorItemsString="Junior,Mid,Senior,Lead,Principal"
101+
EditTriggers="DoubleTap" />
49102

103+
<!--
104+
Salary \u2014 F2 key ONLY.
105+
Protects financial data: pointer gestures cannot open the editor;
106+
the user must deliberately press F2 on a keyboard-attached device.
107+
-->
50108
<core:DataGridColumn Header="Salary"
51109
PropertyName="Salary"
52110
ColumnType="Numeric"
53111
Format="C0"
54112
Width="110"
55-
IsReadOnly="True"
56-
TextAlignment="Right" />
113+
TextAlignment="Right"
114+
EditTriggers="F2Key" />
57115

116+
<!--
117+
Active \u2014 Boolean; toggles on single-tap regardless of EditTriggers.
118+
No EditTriggers override needed; inherits grid default (unused for booleans).
119+
-->
58120
<core:DataGridColumn Header="Active"
59121
PropertyName="IsActive"
60122
ColumnType="Boolean"
61123
Width="60"
62-
IsReadOnly="True"
63124
AllowTabStop="False" />
64125

65126
<!--
66-
Actions column.
67-
PropertyName="" short-circuits DataGridSource.BuildAccessor to
68-
return item => item, so both the renderer and the editor factory
69-
receive the full Employee object as their 'value' argument.
127+
Actions column \u2014 Template with inline action buttons.
128+
Column-level EditTriggers="SingleTap" ensures buttons are always
129+
activatable with a single tap even though the grid default is DoubleTap.
130+
This is the primary use-case for the column-level override feature.
70131
71-
CustomCellRenderer is fully declared here in XAML.
72-
MauiActionButtonDefinition is a BindableObject, so Command accepts
73-
{Binding} expressions. MauiActionButtonDefinition is NOT in the
74-
visual tree, so BindingContext is NOT inherited automatically —
75-
use Source={x:Reference actionsGrid} to resolve the ViewModel.
132+
PropertyName="" short-circuits DataGridSource.BuildAccessor to
133+
return item => item, so both the renderer and editor factory
134+
receive the full Employee object as their value argument.
76135
77-
The editor factory (in code-behind) reuses this same Buttons list,
78-
so commands are already bound when a row is tapped.
136+
MauiActionButtonDefinition is NOT in the visual tree so BindingContext
137+
is not inherited \u2014 use Source={x:Reference actionsGrid} to resolve the VM.
79138
-->
80139
<core:DataGridColumn x:Name="actionsColumn"
81140
Header="Actions"
@@ -87,7 +146,8 @@
87146
AllowFiltering="False"
88147
AllowTabStop="False"
89148
SizeMode="Star"
90-
StarWeight="1">
149+
StarWeight="1"
150+
EditTriggers="SingleTap">
91151
<core:DataGridColumn.CustomCellRenderer>
92152
<render:ActionButtonsCellRenderer>
93153
<render:ActionButtonsCellRenderer.Buttons>
@@ -105,6 +165,7 @@
105165
</render:ActionButtonsCellRenderer>
106166
</core:DataGridColumn.CustomCellRenderer>
107167
</core:DataGridColumn>
168+
108169
</dg:DataGridView.Columns>
109170
</dg:DataGridView>
110171
</Grid>

src/KumikoUI.Core/Input/GridInputController.cs

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -275,11 +275,17 @@ public void HandleKey(
275275
switch (e.Key)
276276
{
277277
case GridKey.F2:
278-
// F2 → begin editing the current cell
278+
// F2 → begin editing the current cell (honours column- and grid-level F2Key flag)
279279
if (_editSession != null && selection.CurrentCell.IsValid)
280280
{
281-
TryBeginEdit(selection, dataSource, scroll, style, null);
282-
e.Handled = true;
281+
int f2Col = selection.CurrentCell.Column;
282+
var f2Column = f2Col >= 0 && f2Col < dataSource.Columns.Count
283+
? dataSource.Columns[f2Col] : null;
284+
if ((GetEffectiveEditTriggers(f2Column) & EditTrigger.F2Key) != 0)
285+
{
286+
TryBeginEdit(selection, dataSource, scroll, style, null);
287+
e.Handled = true;
288+
}
283289
}
284290
break;
285291
case GridKey.Up:
@@ -370,14 +376,19 @@ public void HandleKey(
370376
default:
371377
// Typing trigger: printable character starts editing
372378
if (_editSession != null && e.Character.HasValue && selection.CurrentCell.IsValid &&
373-
!e.HasControl && !e.HasAlt &&
374-
(_editSession.EditTriggers & EditTrigger.Typing) != 0)
379+
!e.HasControl && !e.HasAlt)
375380
{
376-
char c = e.Character.Value;
377-
if (!char.IsControl(c))
381+
int typingCol = selection.CurrentCell.Column;
382+
var typingColumn = typingCol >= 0 && typingCol < dataSource.Columns.Count
383+
? dataSource.Columns[typingCol] : null;
384+
if ((GetEffectiveEditTriggers(typingColumn) & EditTrigger.Typing) != 0)
378385
{
379-
TryBeginEdit(selection, dataSource, scroll, style, c);
380-
e.Handled = true;
386+
char c = e.Character.Value;
387+
if (!char.IsControl(c))
388+
{
389+
TryBeginEdit(selection, dataSource, scroll, style, c);
390+
e.Handled = true;
391+
}
381392
}
382393
}
383394
break;
@@ -769,7 +780,7 @@ private void HandlePointerUp(
769780

770781
// Double-tap edit trigger
771782
if (_editSession != null && hit.Column != null &&
772-
(_editSession.EditTriggers & EditTrigger.DoubleTap) != 0)
783+
(GetEffectiveEditTriggers(hit.Column) & EditTrigger.DoubleTap) != 0)
773784
{
774785
// Boolean toggle on double-tap
775786
if (hit.Column.ColumnType == DataGridColumnType.Boolean)
@@ -793,7 +804,7 @@ private void HandlePointerUp(
793804
}
794805
// Single-tap edit trigger for non-boolean cells
795806
else if (_editSession != null &&
796-
(_editSession.EditTriggers & EditTrigger.SingleTap) != 0)
807+
(GetEffectiveEditTriggers(hit.Column) & EditTrigger.SingleTap) != 0)
797808
{
798809
TryBeginEdit(selection, dataSource, scroll, style, null, e);
799810
}
@@ -875,7 +886,7 @@ private void HandleLongPress(
875886
selection.CurrentCell = new CellPosition(hit.RowIndex, hit.ColumnIndex);
876887

877888
if (_editSession != null && hit.Column != null &&
878-
(_editSession.EditTriggers & EditTrigger.LongPress) != 0)
889+
(GetEffectiveEditTriggers(hit.Column) & EditTrigger.LongPress) != 0)
879890
{
880891
if (hit.Column.ColumnType == DataGridColumnType.Boolean)
881892
{
@@ -1196,6 +1207,15 @@ private void CloseFilterPopup()
11961207

11971208
// ── Edit helpers ──────────────────────────────────────────────
11981209

1210+
/// <summary>
1211+
/// Resolves the effective <see cref="EditTrigger"/> for <paramref name="column"/>.
1212+
/// When the column has its own <see cref="DataGridColumn.EditTriggers"/> override it is
1213+
/// returned directly; otherwise the grid-level <see cref="EditSession.EditTriggers"/> is
1214+
/// used, falling back to <see cref="EditTrigger.Default"/> if the session is null.
1215+
/// </summary>
1216+
private EditTrigger GetEffectiveEditTriggers(DataGridColumn? column)
1217+
=> column?.EditTriggers ?? _editSession?.EditTriggers ?? EditTrigger.Default;
1218+
11991219
/// <summary>
12001220
/// Try to begin editing the current cell.
12011221
/// </summary>

src/KumikoUI.Core/Models/DataGridColumn.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,18 @@ public string? EditorItemsString
232232
/// </summary>
233233
public Rendering.ICellRenderer? CustomCellRenderer { get; set; }
234234

235+
// ── Per-column editing behaviour ───────────────────────────────
236+
237+
/// <summary>
238+
/// Optional column-level edit trigger override.
239+
/// When set, this column uses its own <see cref="Editing.EditTrigger"/> flags instead of
240+
/// the grid-level <c>EditSession.EditTriggers</c> value.
241+
/// Set to <see cref="Editing.EditTrigger.None"/> to disable all trigger-based editing for
242+
/// this column without making it fully read-only via <see cref="IsReadOnly"/>.
243+
/// <c>null</c> (default) means inherit from the grid-level setting.
244+
/// </summary>
245+
public Editing.EditTrigger? EditTriggers { get; set; }
246+
235247
// ── Per-column styling ─────────────────────────────────────────
236248

237249
/// <summary>

tests/KumikoUI.Core.Tests/DataGridColumnTests.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,5 +159,57 @@ public void EditorItemsString_RemovesEmptyEntries()
159159
var items = col.EditorItems!.Cast<string>().ToList();
160160
Assert.Equal(3, items.Count);
161161
}
162+
163+
// ── EditTriggers ──────────────────────────────────────────────
164+
165+
[Fact]
166+
public void EditTriggers_Default_IsNull()
167+
{
168+
var col = new DataGridColumn();
169+
Assert.Null(col.EditTriggers);
170+
}
171+
172+
[Fact]
173+
public void EditTriggers_SetToNone_StoresNone()
174+
{
175+
var col = new DataGridColumn();
176+
col.EditTriggers = KumikoUI.Core.Editing.EditTrigger.None;
177+
Assert.Equal(KumikoUI.Core.Editing.EditTrigger.None, col.EditTriggers);
178+
}
179+
180+
[Fact]
181+
public void EditTriggers_SetToSingleTap_StoresSingleTap()
182+
{
183+
var col = new DataGridColumn();
184+
col.EditTriggers = KumikoUI.Core.Editing.EditTrigger.SingleTap;
185+
Assert.Equal(KumikoUI.Core.Editing.EditTrigger.SingleTap, col.EditTriggers);
186+
}
187+
188+
[Fact]
189+
public void EditTriggers_SetToCombinedFlags_StoresCombinedFlags()
190+
{
191+
var col = new DataGridColumn();
192+
col.EditTriggers = KumikoUI.Core.Editing.EditTrigger.DoubleTap | KumikoUI.Core.Editing.EditTrigger.F2Key;
193+
Assert.Equal(
194+
KumikoUI.Core.Editing.EditTrigger.DoubleTap | KumikoUI.Core.Editing.EditTrigger.F2Key,
195+
col.EditTriggers);
196+
}
197+
198+
[Fact]
199+
public void EditTriggers_SetThenClearedToNull_IsNull()
200+
{
201+
var col = new DataGridColumn { EditTriggers = KumikoUI.Core.Editing.EditTrigger.SingleTap };
202+
col.EditTriggers = null;
203+
Assert.Null(col.EditTriggers);
204+
}
205+
206+
[Fact]
207+
public void EditTriggers_TwoColumns_IndependentValues()
208+
{
209+
var col1 = new DataGridColumn { EditTriggers = KumikoUI.Core.Editing.EditTrigger.SingleTap };
210+
var col2 = new DataGridColumn { EditTriggers = KumikoUI.Core.Editing.EditTrigger.None };
211+
Assert.Equal(KumikoUI.Core.Editing.EditTrigger.SingleTap, col1.EditTriggers);
212+
Assert.Equal(KumikoUI.Core.Editing.EditTrigger.None, col2.EditTriggers);
213+
}
162214
}
163215

0 commit comments

Comments
 (0)