зеркало из https://github.com/DeGsoft/maui-linux.git
430 строки
11 KiB
C#
430 строки
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Specialized;
|
|
using System.ComponentModel;
|
|
using global::Windows.UI.Input;
|
|
using global::Windows.UI.Xaml;
|
|
using global::Windows.UI.Xaml.Automation.Peers;
|
|
using global::Windows.UI.Xaml.Controls;
|
|
using global::Windows.UI.Xaml.Controls.Primitives;
|
|
using global::Windows.UI.Xaml.Input;
|
|
using global::Windows.UI.Xaml.Media;
|
|
using global::Windows.UI.Xaml.Media.Animation;
|
|
using System.Maui.Internals;
|
|
|
|
namespace System.Maui.Platform.UWP
|
|
{
|
|
public class CellControl : ContentControl
|
|
{
|
|
public static readonly DependencyProperty CellProperty = DependencyProperty.Register("Cell", typeof(object), typeof(CellControl),
|
|
new PropertyMetadata(null, (o, e) => ((CellControl)o).SetSource((Cell)e.OldValue, (Cell)e.NewValue)));
|
|
|
|
public static readonly DependencyProperty IsGroupHeaderProperty = DependencyProperty.Register("IsGroupHeader", typeof(bool), typeof(CellControl), null);
|
|
|
|
internal static readonly BindableProperty MeasuredEstimateProperty = BindableProperty.Create("MeasuredEstimate", typeof(double), typeof(ListView), -1d);
|
|
readonly Lazy<ListView> _listView;
|
|
readonly PropertyChangedEventHandler _propertyChangedHandler;
|
|
Brush _defaultOnColor;
|
|
|
|
IList<MenuItem> _contextActions;
|
|
global::Windows.UI.Xaml.DataTemplate _currentTemplate;
|
|
bool _isListViewRealized;
|
|
object _newValue;
|
|
|
|
public CellControl()
|
|
{
|
|
_listView = new Lazy<ListView>(GetListView);
|
|
|
|
DataContextChanged += OnDataContextChanged;
|
|
|
|
Unloaded += (sender, args) =>
|
|
{
|
|
Cell?.SendDisappearing();
|
|
};
|
|
|
|
_propertyChangedHandler = OnCellPropertyChanged;
|
|
}
|
|
|
|
public Cell Cell
|
|
{
|
|
get { return (Cell)GetValue(CellProperty); }
|
|
set { SetValue(CellProperty, value); }
|
|
}
|
|
|
|
public bool IsGroupHeader
|
|
{
|
|
get { return (bool)GetValue(IsGroupHeaderProperty); }
|
|
set { SetValue(IsGroupHeaderProperty, value); }
|
|
}
|
|
|
|
protected FrameworkElement CellContent
|
|
{
|
|
get { return (FrameworkElement)Content; }
|
|
}
|
|
|
|
protected override global::Windows.Foundation.Size MeasureOverride(global::Windows.Foundation.Size availableSize)
|
|
{
|
|
ListView lv = _listView.Value;
|
|
|
|
// set the Cell now that we have a reference to the ListView, since it will have been skipped
|
|
// on DataContextChanged.
|
|
if (_newValue != null)
|
|
{
|
|
SetCell(_newValue);
|
|
_newValue = null;
|
|
}
|
|
|
|
if (Content == null)
|
|
{
|
|
if (lv != null)
|
|
{
|
|
if (lv.HasUnevenRows)
|
|
{
|
|
var estimate = (double)lv.GetValue(MeasuredEstimateProperty);
|
|
if (estimate > -1)
|
|
return new global::Windows.Foundation.Size(availableSize.Width, estimate);
|
|
}
|
|
else
|
|
{
|
|
double rowHeight = lv.RowHeight;
|
|
if (rowHeight > -1)
|
|
return new global::Windows.Foundation.Size(availableSize.Width, rowHeight);
|
|
}
|
|
}
|
|
|
|
// This needs to return a size with a non-zero height;
|
|
// otherwise, it kills virtualization.
|
|
return new global::Windows.Foundation.Size(0, Cell.DefaultCellHeight);
|
|
}
|
|
|
|
// Children still need measure called on them
|
|
global::Windows.Foundation.Size result = base.MeasureOverride(availableSize);
|
|
|
|
if (lv != null)
|
|
{
|
|
lv.SetValue(MeasuredEstimateProperty, result.Height);
|
|
}
|
|
|
|
SetDefaultSwitchColor();
|
|
|
|
return result;
|
|
}
|
|
|
|
ListView GetListView()
|
|
{
|
|
DependencyObject parent = VisualTreeHelper.GetParent(this);
|
|
while (parent != null)
|
|
{
|
|
var lv = parent as ListViewRenderer;
|
|
if (lv != null)
|
|
{
|
|
_isListViewRealized = true;
|
|
return lv.Element;
|
|
}
|
|
|
|
parent = VisualTreeHelper.GetParent(parent);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
global::Windows.UI.Xaml.DataTemplate GetTemplate(Cell cell)
|
|
{
|
|
var renderer = Registrar.Registered.GetHandlerForObject<ICellRenderer>(cell);
|
|
return renderer.GetTemplate(cell);
|
|
}
|
|
|
|
void OnCellPropertyChanged(object sender, PropertyChangedEventArgs e)
|
|
{
|
|
if (e.PropertyName == "HasContextActions")
|
|
{
|
|
SetupContextMenu();
|
|
}
|
|
else if (e.PropertyName == VisualElement.FlowDirectionProperty.PropertyName)
|
|
UpdateFlowDirection(Cell);
|
|
else if (e.PropertyName == SwitchCell.OnProperty.PropertyName ||
|
|
e.PropertyName == SwitchCell.OnColorProperty.PropertyName)
|
|
{
|
|
UpdateOnColor();
|
|
}
|
|
}
|
|
|
|
void UpdateOnColor()
|
|
{
|
|
if (!(Cell is SwitchCell switchCell))
|
|
return;
|
|
|
|
var color = switchCell.OnColor == Color.Default
|
|
? _defaultOnColor
|
|
: new SolidColorBrush(switchCell.OnColor.ToWindowsColor());
|
|
|
|
var nativeSwitch = FrameworkElementExtensions.GetFirstDescendant<ToggleSwitch>(this);
|
|
|
|
// change fill color in switch rectangle
|
|
var rects = nativeSwitch.GetDescendantsByName<global::Windows.UI.Xaml.Shapes.Rectangle>("SwitchKnobBounds");
|
|
foreach (var rect in rects)
|
|
rect.Fill = color;
|
|
|
|
// change color in animation on PointerOver
|
|
var grid = nativeSwitch.GetFirstDescendant<global::Windows.UI.Xaml.Controls.Grid>();
|
|
var gridVisualStateGroups = global::Windows.UI.Xaml.VisualStateManager.GetVisualStateGroups(grid);
|
|
global::Windows.UI.Xaml.VisualStateGroup vsGroup = null;
|
|
foreach (var visualGroup in gridVisualStateGroups)
|
|
{
|
|
if (visualGroup.Name == "CommonStates")
|
|
{
|
|
vsGroup = visualGroup;
|
|
break;
|
|
}
|
|
}
|
|
if (vsGroup == null)
|
|
return;
|
|
|
|
global::Windows.UI.Xaml.VisualState vState = null;
|
|
foreach (var visualState in vsGroup.States)
|
|
{
|
|
if (visualState.Name == "PointerOver")
|
|
{
|
|
vState = visualState;
|
|
break;
|
|
}
|
|
}
|
|
if (vState == null)
|
|
return;
|
|
|
|
var visualStates = vState.Storyboard.Children;
|
|
foreach (ObjectAnimationUsingKeyFrames item in visualStates)
|
|
{
|
|
if ((string)item.GetValue(Storyboard.TargetNameProperty) == "SwitchKnobBounds")
|
|
{
|
|
item.KeyFrames[0].Value = color;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void SetDefaultSwitchColor()
|
|
{
|
|
if (_defaultOnColor == null && Cell is SwitchCell)
|
|
{
|
|
var nativeSwitch = FrameworkElementExtensions.GetFirstDescendant<ToggleSwitch>(this);
|
|
var rects = nativeSwitch.GetDescendantsByName<global::Windows.UI.Xaml.Shapes.Rectangle>("SwitchKnobBounds");
|
|
foreach (var rect in rects)
|
|
_defaultOnColor = rect.Fill;
|
|
UpdateOnColor();
|
|
}
|
|
}
|
|
|
|
void OnClick(object sender, PointerRoutedEventArgs e)
|
|
{
|
|
PointerPoint point = e.GetCurrentPoint(CellContent);
|
|
if (point.Properties.PointerUpdateKind != PointerUpdateKind.RightButtonReleased)
|
|
return;
|
|
|
|
OpenContextMenu();
|
|
}
|
|
|
|
void OnContextActionsChanged(object sender, NotifyCollectionChangedEventArgs e)
|
|
{
|
|
var flyout = GetAttachedFlyout();
|
|
if (flyout != null)
|
|
{
|
|
flyout.Items.Clear();
|
|
SetupMenuItems(flyout);
|
|
}
|
|
}
|
|
|
|
void OnDataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
|
|
{
|
|
if (args.NewValue == null)
|
|
return;
|
|
|
|
// We don't want to set the Cell until the ListView is realized, just in case the
|
|
// Cell has an ItemTemplate. Instead, we'll store the new data item, and it will be
|
|
// set on MeasureOverrideDelegate. However, if the parent is a TableView, we'll already
|
|
// have a complete Cell object to work with, so we can move ahead.
|
|
if (_isListViewRealized || args.NewValue is Cell)
|
|
SetCell(args.NewValue);
|
|
else if (args.NewValue != null)
|
|
_newValue = args.NewValue;
|
|
}
|
|
|
|
void OnLongTap(object sender, HoldingRoutedEventArgs e)
|
|
{
|
|
if (e.HoldingState == HoldingState.Started)
|
|
OpenContextMenu();
|
|
}
|
|
|
|
/// <summary>
|
|
/// To check the context, not just the text.
|
|
/// </summary>
|
|
MenuFlyout GetAttachedFlyout()
|
|
{
|
|
if (FlyoutBase.GetAttachedFlyout(CellContent) is MenuFlyout flyout)
|
|
{
|
|
var actions = Cell.ContextActions;
|
|
if (flyout.Items.Count != actions.Count)
|
|
return null;
|
|
|
|
for (int i = 0; i < flyout.Items.Count; i++)
|
|
{
|
|
if (flyout.Items[i].DataContext != actions[i])
|
|
return null;
|
|
}
|
|
return flyout;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
void OpenContextMenu()
|
|
{
|
|
if (GetAttachedFlyout() == null)
|
|
{
|
|
var flyout = new MenuFlyout();
|
|
SetupMenuItems(flyout);
|
|
|
|
((INotifyCollectionChanged)Cell.ContextActions).CollectionChanged += OnContextActionsChanged;
|
|
|
|
_contextActions = Cell.ContextActions;
|
|
FlyoutBase.SetAttachedFlyout(CellContent, flyout);
|
|
}
|
|
|
|
FlyoutBase.ShowAttachedFlyout(CellContent);
|
|
}
|
|
|
|
void SetCell(object newContext)
|
|
{
|
|
var cell = newContext as Cell;
|
|
|
|
if (cell != null)
|
|
{
|
|
Cell = cell;
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(Cell?.BindingContext, newContext))
|
|
return;
|
|
|
|
// If there is a ListView, load the Cell content from the ItemTemplate.
|
|
// Otherwise, the given Cell is already a templated Cell from a TableView.
|
|
ListView lv = _listView.Value;
|
|
if (lv != null)
|
|
{
|
|
bool isGroupHeader = IsGroupHeader;
|
|
DataTemplate template = isGroupHeader ? lv.GroupHeaderTemplate : lv.ItemTemplate;
|
|
object bindingContext = newContext;
|
|
|
|
if (template is DataTemplateSelector)
|
|
{
|
|
template = ((DataTemplateSelector)template).SelectTemplate(bindingContext, lv);
|
|
}
|
|
|
|
if (template != null)
|
|
{
|
|
cell = template.CreateContent() as Cell;
|
|
}
|
|
else
|
|
{
|
|
if (isGroupHeader)
|
|
bindingContext = lv.GetDisplayTextFromGroup(bindingContext);
|
|
|
|
cell = lv.CreateDefaultCell(bindingContext);
|
|
}
|
|
|
|
// A TableView cell should already have its parent,
|
|
// but we need to set the parent for a ListView cell.
|
|
cell.Parent = lv;
|
|
|
|
// Set inherited BindingContext after setting the Parent so it won't be wiped out
|
|
BindableObject.SetInheritedBindingContext(cell, bindingContext);
|
|
|
|
// This provides the Group Header styling (e.g., larger font, etc.) when the
|
|
// template is loaded later.
|
|
cell.SetIsGroupHeader<ItemsView<Cell>, Cell>(isGroupHeader);
|
|
}
|
|
|
|
Cell = cell;
|
|
}
|
|
|
|
void SetSource(Cell oldCell, Cell newCell)
|
|
{
|
|
if (oldCell != null)
|
|
{
|
|
oldCell.PropertyChanged -= _propertyChangedHandler;
|
|
oldCell.SendDisappearing();
|
|
}
|
|
|
|
if (newCell != null)
|
|
{
|
|
newCell.SendAppearing();
|
|
|
|
UpdateContent(newCell);
|
|
UpdateFlowDirection(newCell);
|
|
SetupContextMenu();
|
|
|
|
newCell.PropertyChanged += _propertyChangedHandler;
|
|
}
|
|
}
|
|
|
|
void SetupContextMenu()
|
|
{
|
|
if (CellContent == null || Cell == null)
|
|
return;
|
|
|
|
if (!Cell.HasContextActions)
|
|
{
|
|
CellContent.Holding -= OnLongTap;
|
|
CellContent.PointerReleased -= OnClick;
|
|
if (_contextActions != null)
|
|
{
|
|
((INotifyCollectionChanged)_contextActions).CollectionChanged -= OnContextActionsChanged;
|
|
_contextActions = null;
|
|
}
|
|
|
|
FlyoutBase.SetAttachedFlyout(CellContent, null);
|
|
return;
|
|
}
|
|
|
|
CellContent.PointerReleased += OnClick;
|
|
CellContent.Holding += OnLongTap;
|
|
}
|
|
|
|
void SetupMenuItems(MenuFlyout flyout)
|
|
{
|
|
foreach (MenuItem item in Cell.ContextActions)
|
|
{
|
|
var flyoutItem = new MenuFlyoutItem();
|
|
flyoutItem.SetBinding(MenuFlyoutItem.TextProperty, "Text");
|
|
flyoutItem.Command = new MenuItemCommand(item);
|
|
flyoutItem.DataContext = item;
|
|
|
|
flyout.Items.Add(flyoutItem);
|
|
}
|
|
}
|
|
|
|
void UpdateContent(Cell newCell)
|
|
{
|
|
global::Windows.UI.Xaml.DataTemplate dt = GetTemplate(newCell);
|
|
if (dt != _currentTemplate || Content == null)
|
|
{
|
|
_currentTemplate = dt;
|
|
Content = dt.LoadContent();
|
|
}
|
|
|
|
((FrameworkElement)Content).DataContext = newCell;
|
|
}
|
|
|
|
protected override AutomationPeer OnCreateAutomationPeer()
|
|
{
|
|
return new FrameworkElementAutomationPeer(this);
|
|
}
|
|
|
|
void UpdateFlowDirection(Cell newCell)
|
|
{
|
|
if (newCell is ViewCell)
|
|
return;
|
|
|
|
this.UpdateFlowDirection(newCell.Parent as VisualElement);
|
|
}
|
|
}
|
|
} |