Skip to content

Commit e07b8e1

Browse files
committed
CalendarView keyboard focus behavior
1 parent 96b1018 commit e07b8e1

File tree

2 files changed

+229
-6
lines changed

2 files changed

+229
-6
lines changed

source/iNKORE.UI.WPF.Modern/Controls/Helpers/CalendarHelper.cs

Lines changed: 211 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
using System.Windows;
1+
using System;
2+
using System.Globalization;
3+
using System.Reflection;
4+
using System.Windows;
25
using System.Windows.Controls;
36
using System.Windows.Controls.Primitives;
7+
using System.Windows.Data;
48
using System.Windows.Input;
9+
using Calendar = System.Windows.Controls.Calendar;
510

611
namespace iNKORE.UI.WPF.Modern.Controls.Helpers
712
{
@@ -52,5 +57,210 @@ private static void OnCalendarGotMouseCapture(object sender, MouseEventArgs e)
5257
}
5358
}
5459
}
60+
61+
#region CalendarKeyboardBehaviorOverride
62+
63+
public static bool GetKeyboardBehaviorOverride(Calendar calendar) =>
64+
(bool)calendar.GetValue(KeyboardBehaviorOverrideProperty);
65+
66+
public static void SetKeyboardBehaviorOverride(Calendar calendar, bool value) =>
67+
calendar.SetValue(KeyboardBehaviorOverrideProperty, value);
68+
69+
public static readonly DependencyProperty KeyboardBehaviorOverrideProperty =
70+
DependencyProperty.RegisterAttached(
71+
"KeyboardBehaviorOverride",
72+
typeof(bool),
73+
typeof(CalendarHelper),
74+
new PropertyMetadata(OnKeyboardBehaviorOverrideChanged));
75+
76+
private static void OnKeyboardBehaviorOverrideChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
77+
{
78+
if (d is not Calendar calendar)
79+
{
80+
return;
81+
}
82+
83+
if (e.NewValue is true)
84+
{
85+
calendar.PreviewKeyDown += OnHandleChangeFocusByKeys;
86+
}
87+
else
88+
{
89+
calendar.PreviewKeyDown -= OnHandleChangeFocusByKeys;
90+
}
91+
}
92+
93+
private static void OnHandleChangeFocusByKeys(object sender, KeyEventArgs e)
94+
{
95+
if (sender is not Calendar calendar ||
96+
(Keyboard.Modifiers & ModifierKeys.Control) is ModifierKeys.Control)
97+
{
98+
return;
99+
}
100+
101+
if ((Keyboard.Modifiers & ModifierKeys.Shift) is ModifierKeys.Shift)
102+
{
103+
e.Handled = true;
104+
return;
105+
}
106+
107+
DateTime? focusTarget;
108+
switch (calendar.DisplayMode)
109+
{
110+
case CalendarMode.Month:
111+
int? change = e.Key switch
112+
{
113+
Key.Up => -7,
114+
Key.Down => 7,
115+
Key.Left => -1,
116+
Key.Right => 1,
117+
_ => null
118+
};
119+
120+
if (change is null)
121+
{
122+
return;
123+
}
124+
125+
var currentDate = typeof(Calendar).GetProperty("CurrentDate",
126+
BindingFlags.NonPublic | BindingFlags.Instance)!
127+
.GetValue(calendar)
128+
as DateTime?;
129+
130+
focusTarget = GetNonBlackoutTarget(calendar, currentDate?.AddDays(change.Value));
131+
132+
break;
133+
134+
case CalendarMode.Year:
135+
if (GetChangeForYearOrDecadeView(e.Key) is not { } monthChange)
136+
{
137+
return;
138+
}
139+
140+
focusTarget = calendar.DisplayDate.AddMonths(monthChange);
141+
break;
142+
case CalendarMode.Decade:
143+
if (GetChangeForYearOrDecadeView(e.Key) is not { } yearChange)
144+
{
145+
return;
146+
}
147+
148+
focusTarget = calendar.DisplayDate.AddYears(yearChange);
149+
break;
150+
default:
151+
return;
152+
}
153+
154+
typeof(Calendar).GetMethod("MoveDisplayTo", BindingFlags.NonPublic | BindingFlags.Instance)!
155+
.Invoke(calendar,
156+
[focusTarget]);
157+
158+
e.Handled = true;
159+
160+
static int? GetChangeForYearOrDecadeView(Key key) => key switch
161+
{
162+
Key.Up => -4,
163+
Key.Down => 4,
164+
Key.Left => -1,
165+
Key.Right => 1,
166+
_ => null
167+
};
168+
169+
static DateTime? GetNonBlackoutTarget(Calendar calendar, DateTime? targetFocus)
170+
{
171+
var blackoutDates =
172+
typeof(Calendar).GetField("_blackoutDates", BindingFlags.NonPublic | BindingFlags.Instance)!
173+
.GetValue(calendar)
174+
as CalendarBlackoutDatesCollection;
175+
176+
var toSelectDate =
177+
typeof(CalendarBlackoutDatesCollection).GetMethod("GetNonBlackoutDate",
178+
BindingFlags.NonPublic | BindingFlags.Instance)!
179+
.Invoke(blackoutDates, [targetFocus, -1]) as DateTime?;
180+
return toSelectDate;
181+
}
182+
}
183+
184+
private static DateTime? GetCurrentFocusedDate() =>
185+
(Keyboard.FocusedElement as FrameworkElement)?.DataContext as DateTime?;
186+
187+
#endregion
188+
189+
#region ContainsToday
190+
191+
public static CalendarMode? GetCalendarDisplayMode(DependencyObject obj) => (CalendarMode?)obj.GetValue(CalendarDisplayModeProperty);
192+
193+
public static void SetCalendarDisplayMode(DependencyObject obj, CalendarMode? value) => obj.SetValue(CalendarDisplayModeProperty, value);
194+
195+
public static readonly DependencyProperty CalendarDisplayModeProperty =
196+
DependencyProperty.RegisterAttached(
197+
"CalendarDisplayMode",
198+
typeof(CalendarMode?),
199+
typeof(CalendarHelper),
200+
new PropertyMetadata(null, OnContainsTodayNeedsUpdate));
201+
202+
public static object GetContextDate(DependencyObject obj) => obj.GetValue(ContextDateProperty);
203+
204+
public static void SetContextDate(DependencyObject obj, object value) => obj.SetValue(ContextDateProperty, value);
205+
206+
public static readonly DependencyProperty ContextDateProperty =
207+
DependencyProperty.RegisterAttached(
208+
"ContextDate",
209+
typeof(object),
210+
typeof(CalendarHelper),
211+
new PropertyMetadata(null, OnContainsTodayNeedsUpdate));
212+
213+
public static bool GetIsContainsTodayActive(DependencyObject obj) => (bool)obj.GetValue(IsContainsTodayActiveProperty);
214+
215+
public static void SetIsContainsTodayActive(DependencyObject obj, bool value) => obj.SetValue(IsContainsTodayActiveProperty, value);
216+
217+
public static readonly DependencyProperty IsContainsTodayActiveProperty =
218+
DependencyProperty.RegisterAttached(
219+
"IsContainsTodayActive",
220+
typeof(bool),
221+
typeof(CalendarHelper),
222+
new PropertyMetadata(true, OnContainsTodayNeedsUpdate));
223+
224+
private static void OnContainsTodayNeedsUpdate(DependencyObject d, DependencyPropertyChangedEventArgs e)
225+
{
226+
UpdateContainsTodayActive(d);
227+
}
228+
229+
private static void UpdateContainsTodayActive(DependencyObject d)
230+
{
231+
var mode = GetCalendarDisplayMode(d);
232+
var contextDate = GetContextDate(d) as DateTime?;
233+
234+
if (!mode.HasValue || !contextDate.HasValue)
235+
{
236+
SetContainsToday(d, false);
237+
return;
238+
}
239+
240+
var containsTodayActive = mode.Value switch
241+
{
242+
CalendarMode.Year => contextDate.Value.Year == DateTime.Today.Year &&
243+
contextDate.Value.Month == DateTime.Today.Month,
244+
CalendarMode.Decade => contextDate.Value.Year == DateTime.Today.Year,
245+
_ => false
246+
};
247+
248+
SetContainsToday(d, containsTodayActive);
249+
}
250+
251+
public static bool GetContainsToday(DependencyObject obj) =>
252+
(bool)obj.GetValue(ContainsTodayProperty);
253+
254+
public static void SetContainsToday(DependencyObject obj, bool value) =>
255+
obj.SetValue(ContainsTodayProperty, value);
256+
257+
public static readonly DependencyProperty ContainsTodayProperty =
258+
DependencyProperty.RegisterAttached(
259+
"ContainsToday",
260+
typeof(bool),
261+
typeof(CalendarHelper),
262+
new PropertyMetadata(false));
263+
264+
#endregion
55265
}
56266
}

source/iNKORE.UI.WPF.Modern/Themes/Controls/Calendar.xaml

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,9 @@
818818
<Setter Property="HorizontalContentAlignment" Value="Center" />
819819
<Setter Property="VerticalContentAlignment" Value="Center" />
820820
<Setter Property="FocusVisualStyle" Value="{StaticResource EllipticalFocusStyle}"/>
821+
<Setter Property="chelper:CalendarHelper.CalendarDisplayMode" Value="{Binding RelativeSource={RelativeSource AncestorType=Calendar}, Path=DisplayMode, Mode=OneWay}" />
822+
<Setter Property="chelper:CalendarHelper.IsContainsTodayActive" Value="{Binding RelativeSource={RelativeSource AncestorType=Calendar}, Path=IsTodayHighlighted, Mode=OneWay}" />
823+
<Setter Property="chelper:CalendarHelper.ContextDate" Value="{Binding }" />
821824
<Setter Property="chelper:FocusVisualHelper.UseSystemFocusVisuals" Value="True" />
822825
<Setter Property="chelper:FocusVisualHelper.FocusVisualMargin" Value="-4,-3.5,-3.5,-3" />
823826
<Setter Property="Template">
@@ -868,30 +871,39 @@
868871
<Setter TargetName="NormalText" Property="Foreground" Value="{DynamicResource CalendarViewDisabledForeground}" />
869872
</Trigger>
870873
<!-- Selected -->
871-
<Trigger Property="HasSelectedDays" Value="True">
874+
<Trigger Property="chelper:CalendarHelper.ContainsToday" Value="True">
872875
<Setter Property="IsTabStop" Value="True" />
873876
<Setter Property="TabIndex" Value="4" />
877+
</Trigger>
878+
<MultiTrigger >
879+
<MultiTrigger.Conditions>
880+
<Condition Property="chelper:CalendarHelper.ContainsToday" Value="True"/>
881+
<Condition Property="chelper:CalendarHelper.IsContainsTodayActive" Value="True"/>
882+
</MultiTrigger.Conditions>
874883
<Setter TargetName="Background" Property="Fill" Value="{DynamicResource CalendarViewTodayBackground}" />
875884
<Setter TargetName="NormalText" Property="FontWeight" Value="{DynamicResource CalendarViewTodayFontWeight}" />
876885
<Setter TargetName="NormalText" Property="Foreground" Value="{DynamicResource CalendarViewTodayForeground}" />
877-
</Trigger>
886+
</MultiTrigger>
878887
<MultiTrigger>
879888
<MultiTrigger.Conditions>
880-
<Condition Property="HasSelectedDays" Value="True" />
889+
<Condition Property="chelper:CalendarHelper.ContainsToday" Value="True"/>
890+
<Condition Property="chelper:CalendarHelper.IsContainsTodayActive" Value="True"/>
881891
<Condition Property="IsMouseOver" Value="True" />
882892
</MultiTrigger.Conditions>
883893
<Setter TargetName="Background" Property="Fill" Value="{DynamicResource CalendarViewTodayHoverBackground}" />
884894
</MultiTrigger>
885895
<MultiTrigger>
886896
<MultiTrigger.Conditions>
887-
<Condition Property="HasSelectedDays" Value="True" />
897+
<Condition Property="chelper:CalendarHelper.ContainsToday" Value="True"/>
898+
<Condition Property="chelper:CalendarHelper.IsContainsTodayActive" Value="True"/>
888899
<Condition Property="IsPressed" Value="True" />
889900
</MultiTrigger.Conditions>
890901
<Setter TargetName="Background" Property="Fill" Value="{DynamicResource CalendarViewTodayPressedBackground}" />
891902
</MultiTrigger>
892903
<MultiTrigger>
893904
<MultiTrigger.Conditions>
894-
<Condition Property="HasSelectedDays" Value="True" />
905+
<Condition Property="chelper:CalendarHelper.ContainsToday" Value="True"/>
906+
<Condition Property="chelper:CalendarHelper.IsContainsTodayActive" Value="True"/>
895907
<Condition Property="IsEnabled" Value="False" />
896908
</MultiTrigger.Conditions>
897909
<Setter TargetName="Background" Property="Fill" Value="{DynamicResource CalendarViewTodayDisabledBackground}" />
@@ -937,6 +949,7 @@
937949
<Setter Property="IsTabStop" Value="False" />
938950
<Setter Property="chelper:ControlHelper.CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
939951
<Setter Property="chelper:CalendarHelper.AutoReleaseMouseCapture" Value="True" />
952+
<Setter Property="chelper:CalendarHelper.KeyboardBehaviorOverride" Value="True" />
940953
<Setter Property="Template">
941954
<Setter.Value>
942955
<ControlTemplate TargetType="Calendar">

0 commit comments

Comments
 (0)