diff --git a/Avalonia.sln.DotSettings b/Avalonia.sln.DotSettings index 2c0a6b9dc8c7..b0692905e7b7 100644 --- a/Avalonia.sln.DotSettings +++ b/Avalonia.sln.DotSettings @@ -1,5 +1,4 @@  - True ExplicitlyExcluded ExplicitlyExcluded ExplicitlyExcluded @@ -39,4 +38,4 @@ <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> True True - True \ No newline at end of file + True diff --git a/Documentation/build.md b/Documentation/build.md index a7d68eb59907..9f5436e68e91 100644 --- a/Documentation/build.md +++ b/Documentation/build.md @@ -6,6 +6,7 @@ Avalonia requires at least Visual Studio 2019 and .NET Core SDK 3.1 to build on ``` git clone https://github.com/AvaloniaUI/Avalonia.git +cd Avalonia git submodule update --init ``` diff --git a/azure-pipelines.yml b/azure-pipelines.yml index fbd85071931d..11ef36d43ffb 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,7 +1,7 @@ jobs: - job: Linux pool: - vmImage: 'ubuntu-16.04' + vmImage: 'ubuntu-20.04' steps: - task: CmdLine@2 displayName: 'Install Nuke' diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 14fe60ab0b9d..7a6e7dc72fe5 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -641,6 +641,7 @@ virtual void OnResized () [Window setCanBecomeKeyAndMain]; [Window disableCursorRects]; [Window setTabbingMode:NSWindowTabbingModeDisallowed]; + [Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; } void HideOrShowTrafficLights () @@ -1091,14 +1092,7 @@ void EnterFullScreenMode () { _fullScreenActive = true; - [Window setHasShadow:YES]; - [Window setTitleVisibility:NSWindowTitleVisible]; - [Window setTitlebarAppearsTransparent:NO]; [Window setTitle:_lastTitle]; - - Window.styleMask = Window.styleMask | NSWindowStyleMaskTitled | NSWindowStyleMaskResizable; - Window.styleMask = Window.styleMask & ~NSWindowStyleMaskFullSizeContentView; - [Window toggleFullScreen:nullptr]; } @@ -1672,6 +1666,7 @@ - (void)otherMouseDown:(NSEvent *)event switch(event.buttonNumber) { + case 2: case 3: _isMiddlePressed = true; [self mouseEvent:event withType:MiddleButtonDown]; @@ -1704,6 +1699,7 @@ - (void)otherMouseUp:(NSEvent *)event { switch(event.buttonNumber) { + case 2: case 3: _isMiddlePressed = false; [self mouseEvent:event withType:MiddleButtonUp]; diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets index 45a7f1aa4492..de3830ffea62 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.targets +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -42,12 +42,24 @@ - $(BuildAvaloniaResourcesDependsOn);AddAvaloniaResources;ResolveReferences + $(BuildAvaloniaResourcesDependsOn);AddAvaloniaResources;ResolveReferences;_GenerateAvaloniaResourcesDependencyCache + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/TextBoxPage.xaml b/samples/ControlCatalog/Pages/TextBoxPage.xaml index 1ac447ea6937..f631c40eb1b7 100644 --- a/samples/ControlCatalog/Pages/TextBoxPage.xaml +++ b/samples/ControlCatalog/Pages/TextBoxPage.xaml @@ -18,6 +18,7 @@ Watermark="Floating Watermark" UseFloatingWatermark="True" Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit."/> + diff --git a/src/Avalonia.Base/Collections/AvaloniaList.cs b/src/Avalonia.Base/Collections/AvaloniaList.cs index 2c7f34c5be1b..2f1cb2888ecf 100644 --- a/src/Avalonia.Base/Collections/AvaloniaList.cs +++ b/src/Avalonia.Base/Collections/AvaloniaList.cs @@ -280,8 +280,8 @@ public Enumerator GetEnumerator() /// /// Gets a range of items from the collection. /// - /// The first index to remove. - /// The number of items to remove. + /// The zero-based index at which the range starts. + /// The number of elements in the range. public IEnumerable GetRange(int index, int count) { return _inner.GetRange(index, count); @@ -455,7 +455,7 @@ public void MoveRange(int oldIndex, int count, int newIndex) } /// - /// Ensures that the capacity of the list is at least . + /// Ensures that the capacity of the list is at least . /// /// The capacity. public void EnsureCapacity(int capacity) diff --git a/src/Avalonia.Base/Data/Converters/FuncValueConverter.cs b/src/Avalonia.Base/Data/Converters/FuncValueConverter.cs index 9ec600d2bc95..2385d4981c4a 100644 --- a/src/Avalonia.Base/Data/Converters/FuncValueConverter.cs +++ b/src/Avalonia.Base/Data/Converters/FuncValueConverter.cs @@ -26,7 +26,7 @@ public FuncValueConverter(Func convert) /// public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - if (value is TIn || (value == null && TypeUtilities.AcceptsNull(typeof(TIn)))) + if (TypeUtilities.CanCast(value)) { return _convert((TIn)value); } diff --git a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs index 1a7879217366..326d1a3f5350 100644 --- a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs +++ b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs @@ -39,7 +39,7 @@ public override void Send(SendOrPostCallback d, object state) if (Dispatcher.UIThread.CheckAccess()) d(state); else - Dispatcher.UIThread.InvokeAsync(() => d(state), DispatcherPriority.Send).Wait(); + Dispatcher.UIThread.InvokeAsync(() => d(state), DispatcherPriority.Send).GetAwaiter().GetResult(); } diff --git a/src/Avalonia.Base/Utilities/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs index 179ded35495d..0978308ef6cc 100644 --- a/src/Avalonia.Base/Utilities/TypeUtilities.cs +++ b/src/Avalonia.Base/Utilities/TypeUtilities.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; namespace Avalonia.Utilities { @@ -93,6 +94,17 @@ public static bool AcceptsNull(Type type) return !type.IsValueType || IsNullableType(type); } + /// + /// Returns a value indicating whether null can be assigned to the specified type. + /// + /// The type + /// True if the type accepts null values; otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool AcceptsNull() + { + return default(T) is null; + } + /// /// Returns a value indicating whether value can be casted to the specified type. /// If value is null, checks if instances of that type can be null. @@ -102,7 +114,7 @@ public static bool AcceptsNull(Type type) /// True if the cast is possible, otherwise false. public static bool CanCast(object value) { - return value is T || (value is null && AcceptsNull(typeof(T))); + return value is T || (value is null && AcceptsNull()); } /// diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs index 9d8ebbfac144..587dd228a3a4 100644 --- a/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs +++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs @@ -83,8 +83,9 @@ object GetKey(object o) if (key == null) key = item; - if (_valueConverter != null) - key = _valueConverter.Convert(key, typeof(object), level, culture); + var valueConverter = ValueConverter; + if (valueConverter != null) + key = valueConverter.Convert(key, typeof(object), level, culture); return key; } @@ -99,6 +100,8 @@ public override bool KeysMatch(object groupKey, object itemKey) } public override string PropertyName => _propertyPath; + public IValueConverter ValueConverter { get => _valueConverter; set => _valueConverter = value; } + private Type GetPropertyType(object o) { return o.GetType().GetNestedPropertyType(_propertyPath); diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index ab1aff9220f1..fea02dabf46f 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -3039,6 +3039,12 @@ internal void UpdateStateOnCurrentChanged(object currentItem, int currentPositio } } + //TODO: Ensure right button is checked for + internal bool UpdateStateOnMouseRightButtonDown(PointerPressedEventArgs pointerPressedEventArgs, int columnIndex, int slot, bool allowEdit) + { + KeyboardHelper.GetMetaKeyState(pointerPressedEventArgs.KeyModifiers, out bool ctrl, out bool shift); + return UpdateStateOnMouseRightButtonDown(pointerPressedEventArgs, columnIndex, slot, allowEdit, shift, ctrl); + } //TODO: Ensure left button is checked for internal bool UpdateStateOnMouseLeftButtonDown(PointerPressedEventArgs pointerPressedEventArgs, int columnIndex, int slot, bool allowEdit) { @@ -4489,17 +4495,27 @@ private void PopulateCellContent(bool isCellEdited, element = dataGridColumn.GenerateEditingElementInternal(dataGridCell, dataGridRow.DataContext); if (element != null) { - // Subscribe to the new element's events - element.Initialized += EditingElement_Initialized; + + dataGridCell.Content = element; + if (element.IsInitialized) + { + PreparingCellForEditPrivate(element as Control); + } + else + { + // Subscribe to the new element's events + element.Initialized += EditingElement_Initialized; + } } } else { // Generate Element and apply column style if available element = dataGridColumn.GenerateElementInternal(dataGridCell, dataGridRow.DataContext); + dataGridCell.Content = element; } - dataGridCell.Content = element; + } private void PreparingCellForEditPrivate(Control editingElement) @@ -5711,6 +5727,35 @@ private void VerticalScrollBar_Scroll(object sender, ScrollEventArgs e) VerticalScroll?.Invoke(sender, e); } + //TODO: Ensure right button is checked for + private bool UpdateStateOnMouseRightButtonDown(PointerPressedEventArgs pointerPressedEventArgs, int columnIndex, int slot, bool allowEdit, bool shift, bool ctrl) + { + Debug.Assert(slot >= 0); + + if (shift || ctrl) + { + return true; + } + if (IsSlotOutOfBounds(slot)) + { + return true; + } + if (GetRowSelection(slot)) + { + return true; + } + // Unselect everything except the row that was clicked on + try + { + UpdateSelectionAndCurrency(columnIndex, slot, DataGridSelectionAction.SelectCurrent, scrollIntoView: false); + } + finally + { + NoSelectionChangeCount--; + } + return true; + } + //TODO: Ensure left button is checked for private bool UpdateStateOnMouseLeftButtonDown(PointerPressedEventArgs pointerPressedEventArgs, int columnIndex, int slot, bool allowEdit, bool shift, bool ctrl) { diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs index 7dda93631711..e3f150f5c477 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs @@ -161,29 +161,42 @@ protected override void OnPointerLeave(PointerEventArgs e) private void DataGridCell_PointerPressed(PointerPressedEventArgs e) { // OwningGrid is null for TopLeftHeaderCell and TopRightHeaderCell because they have no OwningRow - if (OwningGrid != null) + if (OwningGrid == null) { - OwningGrid.OnCellPointerPressed(new DataGridCellPointerPressedEventArgs(this, OwningRow, OwningColumn, e)); - if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + return; + } + OwningGrid.OnCellPointerPressed(new DataGridCellPointerPressedEventArgs(this, OwningRow, OwningColumn, e)); + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + { + if (!e.Handled) + //if (!e.Handled && OwningGrid.IsTabStop) + { + OwningGrid.Focus(); + } + if (OwningRow != null) { - if (!e.Handled) - //if (!e.Handled && OwningGrid.IsTabStop) + var handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled); + + // Do not handle PointerPressed with touch, + // so we can start scroll gesture on the same event. + if (e.Pointer.Type != PointerType.Touch) { - OwningGrid.Focus(); + e.Handled = handled; } - if (OwningRow != null) - { - var handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled); - - // Do not handle PointerPressed with touch, - // so we can start scroll gesture on the same event. - if (e.Pointer.Type != PointerType.Touch) - { - e.Handled = handled; - } - OwningGrid.UpdatedStateOnMouseLeftButtonDown = true; - } + OwningGrid.UpdatedStateOnMouseLeftButtonDown = true; + } + } + else if (e.GetCurrentPoint(this).Properties.IsRightButtonPressed) + { + if (!e.Handled) + //if (!e.Handled && OwningGrid.IsTabStop) + { + OwningGrid.Focus(); + } + if (OwningRow != null) + { + e.Handled = OwningGrid.UpdateStateOnMouseRightButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled); } } } diff --git a/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs b/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs index a94acdec57d9..fade597ca137 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs @@ -233,7 +233,7 @@ public bool BeginEdit(object dataItem) else { editableCollectionView.EditItem(dataItem); - return editableCollectionView.IsEditingItem; + return editableCollectionView.IsEditingItem || editableCollectionView.IsAddingNew; } } @@ -314,7 +314,14 @@ public bool EndEdit(object dataItem) CommittingEdit = true; try { - editableCollectionView.CommitEdit(); + if (editableCollectionView.IsAddingNew) + { + editableCollectionView.CommitNew(); + } + else + { + editableCollectionView.CommitEdit(); + } } finally { diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs index 754697049896..1efce7c0b872 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs @@ -378,13 +378,13 @@ internal int? MouseOverColumnIndex } } } - } + } internal Panel RootElement { get; private set; - } + } internal int Slot { @@ -638,7 +638,7 @@ internal void UpdatePseudoClasses() PseudoClasses.Set(":editing", IsEditing); PseudoClasses.Set(":invalid", !IsValid); ApplyHeaderStatus(); - } + } } //TODO Animation @@ -896,7 +896,7 @@ internal void EnsureDetailsContentHeight() _detailsElement.ContentHeight = _detailsDesiredHeight; } } - } + } // Makes sure the _detailsDesiredHeight is initialized. We need to measure it to know what // height we want to animate to. Subsequently, we just update that height in response to SizeChanged @@ -919,7 +919,7 @@ private void EnsureDetailsDesiredHeight() //TODO Cleanup double? _previousDetailsHeight = null; - + //TODO Animation private void DetailsContent_HeightChanged(double newValue) { @@ -1022,7 +1022,7 @@ internal void SetDetailsVisibilityInternal(bool isVisible, bool raiseNotificatio } } } - + internal void ApplyDetailsTemplate(bool initializeDetailsPreferredHeight) { if (_detailsElement != null && AreDetailsVisible) @@ -1066,7 +1066,7 @@ internal void ApplyDetailsTemplate(bool initializeDetailsPreferredHeight) .Subscribe(DetailsContent_MarginChanged); } - + _detailsElement.Children.Add(_detailsContent); } } @@ -1090,6 +1090,28 @@ internal void ApplyDetailsTemplate(bool initializeDetailsPreferredHeight) } } } + + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (change.Property == DataContextProperty) + { + var owner = OwningGrid; + if (owner != null && this.IsRecycled) + { + var columns = owner.ColumnsItemsInternal; + var nc = columns.Count; + for (int ci = 0; ci < nc; ci++) + { + if (columns[ci] is DataGridTemplateColumn column) + { + column.RefreshCellContent((Control)this.Cells[column.Index].Content, nameof(DataGridTemplateColumn.CellTemplate)); + } + } + } + } + base.OnPropertyChanged(change); + } } diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs index 1e03b134b1bb..49ca23d34c3a 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs @@ -283,7 +283,11 @@ internal void ClearFrozenStates() //TODO TabStop private void DataGridRowGroupHeader_PointerPressed(PointerPressedEventArgs e) { - if (OwningGrid != null && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + if (OwningGrid == null) + { + return; + } + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { if (OwningGrid.IsDoubleClickRecordsClickOnCall(this) && !e.Handled) { @@ -300,6 +304,15 @@ private void DataGridRowGroupHeader_PointerPressed(PointerPressedEventArgs e) e.Handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, OwningGrid.CurrentColumnIndex, RowGroupInfo.Slot, allowEdit: false); } } + else if (e.GetCurrentPoint(this).Properties.IsRightButtonPressed) + { + if (!e.Handled) + { + OwningGrid.Focus(); + } + e.Handled = OwningGrid.UpdateStateOnMouseRightButtonDown(e, OwningGrid.CurrentColumnIndex, RowGroupInfo.Slot, allowEdit: false); + } + } private void EnsureChildClip(Visual child, double frozenLeftEdge) diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs index 0cd3589a5768..510072174fe9 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs @@ -179,12 +179,12 @@ protected override void OnPointerLeave(PointerEventArgs e) //TODO TabStop private void DataGridRowHeader_PointerPressed(object sender, PointerPressedEventArgs e) { - if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + if (OwningGrid == null) { return; } - if (OwningGrid != null) + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { if (!e.Handled) //if (!e.Handled && OwningGrid.IsTabStop) @@ -199,6 +199,19 @@ private void DataGridRowHeader_PointerPressed(object sender, PointerPressedEvent OwningGrid.UpdatedStateOnMouseLeftButtonDown = true; } } + else if (e.GetCurrentPoint(this).Properties.IsRightButtonPressed) + { + if (!e.Handled) + { + OwningGrid.Focus(); + } + if (OwningRow != null) + { + Debug.Assert(sender is DataGridRowHeader); + Debug.Assert(sender == this); + e.Handled = OwningGrid.UpdateStateOnMouseRightButtonDown(e, -1, Slot, false); + } + } } } diff --git a/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs b/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs index 6ac77fbb9993..1d1a595ccf7e 100644 --- a/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs +++ b/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs @@ -1,10 +1,8 @@ using Avalonia.Data; using Avalonia.Reactive; using System; -using System.ComponentModel.DataAnnotations; using System.Collections.Generic; using System.Reactive.Subjects; -using System.Text; namespace Avalonia.Controls.Utils { @@ -67,11 +65,14 @@ public SubjectWrapper(ISubject bindingSourceSubject, CellEditBinding edi private void SetSourceValue(object value) { - _settingSourceValue = true; + if (!_settingSourceValue) + { + _settingSourceValue = true; - _sourceSubject.OnNext(value); + _sourceSubject.OnNext(value); - _settingSourceValue = false; + _settingSourceValue = false; + } } private void SetControlValue(object value) { @@ -157,4 +158,4 @@ public void CommitEdit() } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 5a6e78f4413d..0e946126ea4f 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -2094,7 +2094,21 @@ private void RefreshView() bool inResults = !(stringFiltering || objectFiltering); if (!inResults) { - inResults = stringFiltering ? TextFilter(text, FormatValue(item)) : ItemFilter(text, item); + if (stringFiltering) + { + inResults = TextFilter(text, FormatValue(item)); + } + else + { + if (ItemFilter is null) + { + throw new Exception("ItemFilter property can not be null when FilterMode has value AutoCompleteFilterMode.Custom"); + } + else + { + inResults = ItemFilter(text, item); + } + } } if (view_count > view_index && inResults && _view[view_index] == item) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 57c07916dbb7..274696d50109 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -1,6 +1,8 @@ using System; using System.Linq; +using System.Reactive.Disposables; using Avalonia.Controls.Generators; +using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; @@ -80,7 +82,7 @@ public class ComboBox : SelectingItemsControl private bool _isDropDownOpen; private Popup _popup; private object _selectionBoxItem; - private IDisposable _subscriptionsOnOpen; + private readonly CompositeDisposable _subscriptionsOnOpen = new CompositeDisposable(); /// /// Initializes static members of the class. @@ -291,6 +293,7 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) _popup = e.NameScope.Get("PART_Popup"); _popup.Opened += PopupOpened; + _popup.Closed += PopupClosed; } internal void ItemFocused(ComboBoxItem dropDownItem) @@ -303,8 +306,7 @@ internal void ItemFocused(ComboBoxItem dropDownItem) private void PopupClosed(object sender, EventArgs e) { - _subscriptionsOnOpen?.Dispose(); - _subscriptionsOnOpen = null; + _subscriptionsOnOpen.Clear(); if (CanFocus(this)) { @@ -316,20 +318,34 @@ private void PopupOpened(object sender, EventArgs e) { TryFocusSelectedItem(); - _subscriptionsOnOpen?.Dispose(); - _subscriptionsOnOpen = null; + _subscriptionsOnOpen.Clear(); var toplevel = this.GetVisualRoot() as TopLevel; if (toplevel != null) { - _subscriptionsOnOpen = toplevel.AddDisposableHandler(PointerWheelChangedEvent, (s, ev) => + toplevel.AddDisposableHandler(PointerWheelChangedEvent, (s, ev) => { //eat wheel scroll event outside dropdown popup while it's open if (IsDropDownOpen && (ev.Source as IVisual).GetVisualRoot() == toplevel) { ev.Handled = true; } - }, Interactivity.RoutingStrategies.Tunnel); + }, Interactivity.RoutingStrategies.Tunnel).DisposeWith(_subscriptionsOnOpen); + } + + this.GetObservable(IsVisibleProperty).Subscribe(IsVisibleChanged).DisposeWith(_subscriptionsOnOpen); + + foreach (var parent in this.GetVisualAncestors().OfType()) + { + parent.GetObservable(IsVisibleProperty).Subscribe(IsVisibleChanged).DisposeWith(_subscriptionsOnOpen); + } + } + + private void IsVisibleChanged(bool isVisible) + { + if (!isVisible && IsDropDownOpen) + { + IsDropDownOpen = false; } } diff --git a/src/Avalonia.Controls/Design.cs b/src/Avalonia.Controls/Design.cs index 0d05e19e53da..07d2918a8856 100644 --- a/src/Avalonia.Controls/Design.cs +++ b/src/Avalonia.Controls/Design.cs @@ -60,6 +60,19 @@ public static Control GetPreviewWith(AvaloniaObject target) return target.GetValue(PreviewWithProperty); } + public static readonly AttachedProperty DesignStyleProperty = AvaloniaProperty + .RegisterAttached("DesignStyle", typeof(Design)); + + public static void SetDesignStyle(Control control, IStyle value) + { + control.SetValue(DesignStyleProperty, value); + } + + public static IStyle GetDesignStyle(Control control) + { + return control.GetValue(DesignStyleProperty); + } + public static void ApplyDesignModeProperties(Control target, Control source) { if (source.IsSet(WidthProperty)) @@ -68,6 +81,8 @@ public static void ApplyDesignModeProperties(Control target, Control source) target.Height = source.GetValue(HeightProperty); if (source.IsSet(DataContextProperty)) target.DataContext = source.GetValue(DataContextProperty); + if (source.IsSet(DesignStyleProperty)) + target.Styles.Add(source.GetValue(DesignStyleProperty)); } } } diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 230b4954fe29..4b903d056c8e 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -215,11 +215,6 @@ protected virtual bool ShowAtCore(Control placementTarget, bool showAtPointer = } } - if (CancelOpening()) - { - return false; - } - if (Popup.Parent != null && Popup.Parent != placementTarget) { ((ISetLogicalParent)Popup).SetParent(null); @@ -236,6 +231,11 @@ protected virtual bool ShowAtCore(Control placementTarget, bool showAtPointer = Popup.Child = CreatePresenter(); } + if (CancelOpening()) + { + return false; + } + PositionPopup(showAtPointer); IsOpen = Popup.IsOpen = true; OnOpened(); diff --git a/src/Avalonia.Controls/MaskedTextBox.cs b/src/Avalonia.Controls/MaskedTextBox.cs new file mode 100644 index 000000000000..a72c617f054e --- /dev/null +++ b/src/Avalonia.Controls/MaskedTextBox.cs @@ -0,0 +1,433 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using Avalonia.Input; +using Avalonia.Input.Platform; +using Avalonia.Interactivity; +using Avalonia.Styling; + +#nullable enable + +namespace Avalonia.Controls +{ + public class MaskedTextBox : TextBox, IStyleable + { + public static readonly StyledProperty AsciiOnlyProperty = + AvaloniaProperty.Register(nameof(AsciiOnly)); + + public static readonly DirectProperty CultureProperty = + AvaloniaProperty.RegisterDirect(nameof(Culture), o => o.Culture, + (o, v) => o.Culture = v, CultureInfo.CurrentCulture); + + public static readonly StyledProperty HidePromptOnLeaveProperty = + AvaloniaProperty.Register(nameof(HidePromptOnLeave)); + + public static readonly DirectProperty MaskCompletedProperty = + AvaloniaProperty.RegisterDirect(nameof(MaskCompleted), o => o.MaskCompleted); + + public static readonly DirectProperty MaskFullProperty = + AvaloniaProperty.RegisterDirect(nameof(MaskFull), o => o.MaskFull); + + public static readonly StyledProperty MaskProperty = + AvaloniaProperty.Register(nameof(Mask), string.Empty); + + public static new readonly StyledProperty PasswordCharProperty = + AvaloniaProperty.Register(nameof(PasswordChar), '\0'); + + public static readonly StyledProperty PromptCharProperty = + AvaloniaProperty.Register(nameof(PromptChar), '_'); + + public static readonly DirectProperty ResetOnPromptProperty = + AvaloniaProperty.RegisterDirect(nameof(ResetOnPrompt), o => o.ResetOnPrompt, (o, v) => o.ResetOnPrompt = v); + + public static readonly DirectProperty ResetOnSpaceProperty = + AvaloniaProperty.RegisterDirect(nameof(ResetOnSpace), o => o.ResetOnSpace, (o, v) => o.ResetOnSpace = v); + + private CultureInfo? _culture; + + private bool _resetOnPrompt = true; + + private bool _ignoreTextChanges; + + private bool _resetOnSpace = true; + + public MaskedTextBox() { } + + /// + /// Constructs the MaskedTextBox with the specified MaskedTextProvider object. + /// + public MaskedTextBox(MaskedTextProvider maskedTextProvider) + { + if (maskedTextProvider == null) + { + throw new ArgumentNullException(nameof(maskedTextProvider)); + } + AsciiOnly = maskedTextProvider.AsciiOnly; + Culture = maskedTextProvider.Culture; + Mask = maskedTextProvider.Mask; + PasswordChar = maskedTextProvider.PasswordChar; + PromptChar = maskedTextProvider.PromptChar; + } + + /// + /// Gets or sets a value indicating if the masked text box is restricted to accept only ASCII characters. + /// Default value is false. + /// + public bool AsciiOnly + { + get => GetValue(AsciiOnlyProperty); + set => SetValue(AsciiOnlyProperty, value); + } + + /// + /// Gets or sets the culture information associated with the masked text box. + /// + public CultureInfo? Culture + { + get => _culture; + set => SetAndRaise(CultureProperty, ref _culture, value); + } + + /// + /// Gets or sets a value indicating if the prompt character is hidden when the masked text box loses focus. + /// + public bool HidePromptOnLeave + { + get => GetValue(HidePromptOnLeaveProperty); + set => SetValue(HidePromptOnLeaveProperty, value); + } + + /// + /// Gets or sets the mask to apply to the TextBox. + /// + public string? Mask + { + get => GetValue(MaskProperty); + set => SetValue(MaskProperty, value); + } + + /// + /// Specifies whether the test string required input positions, as specified by the mask, have + /// all been assigned. + /// + public bool? MaskCompleted + { + get => MaskProvider?.MaskCompleted; + } + + /// + /// Specifies whether all inputs (required and optional) have been provided into the mask successfully. + /// + public bool? MaskFull + { + get => MaskProvider?.MaskFull; + } + + /// + /// Gets the MaskTextProvider for the specified Mask. + /// + public MaskedTextProvider? MaskProvider { get; private set; } + + /// + /// Gets or sets the character to be displayed in substitute for user input. + /// + public new char PasswordChar + { + get => GetValue(PasswordCharProperty); + set => SetValue(PasswordCharProperty, value); + } + + /// + /// Gets or sets the character used to represent the absence of user input in MaskedTextBox. + /// + public char PromptChar + { + get => GetValue(PromptCharProperty); + set => SetValue(PromptCharProperty, value); + } + + /// + /// Gets or sets a value indicating if selected characters should be reset when the prompt character is pressed. + /// + public bool ResetOnPrompt + { + get => _resetOnPrompt; + set + { + SetAndRaise(ResetOnPromptProperty, ref _resetOnPrompt, value); + if (MaskProvider != null) + { + MaskProvider.ResetOnPrompt = value; + } + + } + } + + /// + /// Gets or sets a value indicating if selected characters should be reset when the space character is pressed. + /// + public bool ResetOnSpace + { + get => _resetOnSpace; + set + { + SetAndRaise(ResetOnSpaceProperty, ref _resetOnSpace, value); + if (MaskProvider != null) + { + MaskProvider.ResetOnSpace = value; + } + + } + + + } + + Type IStyleable.StyleKey => typeof(TextBox); + + protected override void OnGotFocus(GotFocusEventArgs e) + { + if (HidePromptOnLeave == true && MaskProvider != null) + { + Text = MaskProvider.ToDisplayString(); + } + base.OnGotFocus(e); + } + + protected override async void OnKeyDown(KeyEventArgs e) + { + if (MaskProvider == null) + { + base.OnKeyDown(e); + return; + } + + var keymap = AvaloniaLocator.Current.GetService(); + + bool Match(List gestures) => gestures.Any(g => g.Matches(e)); + + if (Match(keymap.Paste)) + { + var text = await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))).GetTextAsync(); + + if (text == null) + return; + + foreach (var item in text) + { + var index = GetNextCharacterPosition(CaretIndex); + if (MaskProvider.InsertAt(item, index)) + { + CaretIndex = ++index; + } + } + + Text = MaskProvider.ToDisplayString(); + e.Handled = true; + return; + } + + if (e.Key != Key.Back) + { + base.OnKeyDown(e); + } + + switch (e.Key) + { + case Key.Delete: + if (CaretIndex < Text.Length) + { + if (MaskProvider.RemoveAt(CaretIndex)) + { + RefreshText(MaskProvider, CaretIndex); + } + + e.Handled = true; + } + break; + case Key.Space: + if (!MaskProvider.ResetOnSpace || string.IsNullOrEmpty(SelectedText)) + { + if (MaskProvider.InsertAt(" ", CaretIndex)) + { + RefreshText(MaskProvider, CaretIndex); + } + } + + e.Handled = true; + break; + case Key.Back: + if (CaretIndex > 0) + { + MaskProvider.RemoveAt(CaretIndex - 1); + } + RefreshText(MaskProvider, CaretIndex - 1); + e.Handled = true; + break; + } + } + + protected override void OnLostFocus(RoutedEventArgs e) + { + if (HidePromptOnLeave == true && MaskProvider != null) + { + Text = MaskProvider.ToString(!HidePromptOnLeave, true); + } + base.OnLostFocus(e); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + void UpdateMaskProvider() + { + MaskProvider = new MaskedTextProvider(Mask, Culture, true, PromptChar, PasswordChar, AsciiOnly) { ResetOnSpace = ResetOnSpace, ResetOnPrompt = ResetOnPrompt }; + if (Text != null) + { + MaskProvider.Set(Text); + } + RefreshText(MaskProvider, 0); + } + if (change.Property == TextProperty && MaskProvider != null && _ignoreTextChanges == false) + { + if (string.IsNullOrEmpty(Text)) + { + MaskProvider.Clear(); + RefreshText(MaskProvider, CaretIndex); + base.OnPropertyChanged(change); + return; + } + + MaskProvider.Set(Text); + RefreshText(MaskProvider, CaretIndex); + } + else if (change.Property == MaskProperty) + { + UpdateMaskProvider(); + + if (!string.IsNullOrEmpty(Mask)) + { + foreach (var c in Mask!) + { + if (!MaskedTextProvider.IsValidMaskChar(c)) + { + throw new ArgumentException("Specified mask contains characters that are not valid."); + } + } + } + } + else if (change.Property == PasswordCharProperty) + { + if (!MaskedTextProvider.IsValidPasswordChar(PasswordChar)) + { + throw new ArgumentException("Specified character value is not allowed for this property.", nameof(PasswordChar)); + } + if (MaskProvider != null && PasswordChar == MaskProvider.PromptChar) + { + // Prompt and password chars must be different. + throw new InvalidOperationException("PasswordChar and PromptChar values cannot be the same."); + } + if (MaskProvider != null && MaskProvider.PasswordChar != PasswordChar) + { + UpdateMaskProvider(); + } + } + else if (change.Property == PromptCharProperty) + { + if (!MaskedTextProvider.IsValidInputChar(PromptChar)) + { + throw new ArgumentException("Specified character value is not allowed for this property."); + } + if (PromptChar == PasswordChar) + { + throw new InvalidOperationException("PasswordChar and PromptChar values cannot be the same."); + } + if (MaskProvider != null && MaskProvider.PromptChar != PromptChar) + { + UpdateMaskProvider(); + } + } + else if (change.Property == AsciiOnlyProperty && MaskProvider != null && MaskProvider.AsciiOnly != AsciiOnly + || change.Property == CultureProperty && MaskProvider != null && !MaskProvider.Culture.Equals(Culture)) + { + UpdateMaskProvider(); + } + base.OnPropertyChanged(change); + } + protected override void OnTextInput(TextInputEventArgs e) + { + _ignoreTextChanges = true; + try + { + if (IsReadOnly) + { + e.Handled = true; + base.OnTextInput(e); + return; + } + if (MaskProvider == null) + { + base.OnTextInput(e); + return; + } + if ((MaskProvider.ResetOnSpace && e.Text == " " || MaskProvider.ResetOnPrompt && e.Text == MaskProvider.PromptChar.ToString()) && !string.IsNullOrEmpty(SelectedText)) + { + if (SelectionStart > SelectionEnd ? MaskProvider.RemoveAt(SelectionEnd, SelectionStart - 1) : MaskProvider.RemoveAt(SelectionStart, SelectionEnd - 1)) + { + SelectedText = string.Empty; + } + } + + if (CaretIndex < Text.Length) + { + CaretIndex = GetNextCharacterPosition(CaretIndex); + + if (MaskProvider.InsertAt(e.Text, CaretIndex)) + { + CaretIndex++; + } + var nextPos = GetNextCharacterPosition(CaretIndex); + if (nextPos != 0 && CaretIndex != Text.Length) + { + CaretIndex = nextPos; + } + } + + RefreshText(MaskProvider, CaretIndex); + + + e.Handled = true; + + base.OnTextInput(e); + } + finally + { + _ignoreTextChanges = false; + } + + } + + private int GetNextCharacterPosition(int startPosition) + { + if (MaskProvider != null) + { + var position = MaskProvider.FindEditPositionFrom(startPosition, true); + if (CaretIndex != -1) + { + return position; + } + } + return startPosition; + } + + private void RefreshText(MaskedTextProvider provider, int position) + { + if (provider != null) + { + Text = provider.ToDisplayString(); + CaretIndex = position; + } + } + + } +} diff --git a/src/Avalonia.Controls/Mixins/DisposableMixin.cs b/src/Avalonia.Controls/Mixins/DisposableMixin.cs new file mode 100644 index 000000000000..9b30b4ba4c9e --- /dev/null +++ b/src/Avalonia.Controls/Mixins/DisposableMixin.cs @@ -0,0 +1,38 @@ +using System; +using System.Reactive.Disposables; + +namespace Avalonia.Controls.Mixins +{ + /// + /// Extension methods associated with the IDisposable interface. + /// + public static class DisposableMixin + { + /// + /// Ensures the provided disposable is disposed with the specified . + /// + /// + /// The type of the disposable. + /// + /// + /// The disposable we are going to want to be disposed by the CompositeDisposable. + /// + /// + /// The to which will be added. + /// + /// + /// The disposable. + /// + public static T DisposeWith(this T item, CompositeDisposable compositeDisposable) + where T : IDisposable + { + if (compositeDisposable is null) + { + throw new ArgumentNullException(nameof(compositeDisposable)); + } + + compositeDisposable.Add(item); + return item; + } + } +} diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 984faa4d60c7..e361e7b736a7 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -115,7 +115,7 @@ public virtual void Detach(IMenu menu) protected IMenu? Menu { get; private set; } - protected static TimeSpan MenuShowDelay { get; } = TimeSpan.FromMilliseconds(400); + public static TimeSpan MenuShowDelay { get; set; } = TimeSpan.FromMilliseconds(400); protected internal virtual void GotFocus(object sender, GotFocusEventArgs e) { @@ -275,7 +275,7 @@ protected internal virtual void AccessKeyPressed(object sender, RoutedEventArgs return; } - if (item.HasSubMenu) + if (item.HasSubMenu && item.IsEffectivelyEnabled) { Open(item, true); } @@ -303,7 +303,8 @@ protected internal virtual void PointerEnter(object sender, PointerEventArgs e) { item.Parent.SelectedItem.Close(); SelectItemAndAncestors(item); - Open(item, false); + if (item.HasSubMenu) + Open(item, false); } else { diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index e3783febddf6..b0b52812b973 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -511,8 +511,8 @@ private void UpdateFromScrollable(ILogicalScrollable scrollable) else if (scrollable.IsLogicalScrollEnabled) { Viewport = scrollable.Viewport; - Offset = scrollable.Offset; Extent = scrollable.Extent; + Offset = scrollable.Offset; } } diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index d5fb69a67291..a5cdeefb0e5e 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Linq; using System.Reactive.Disposables; +using Avalonia.Controls.Mixins; using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives.PopupPositioning; @@ -52,6 +53,7 @@ public class Popup : Control, IVisualTreeHost, IPopupHostProvider AvaloniaProperty.Register( nameof(PlacementConstraintAdjustment), PopupPositionerConstraintAdjustment.FlipX | PopupPositionerConstraintAdjustment.FlipY | + PopupPositionerConstraintAdjustment.SlideX | PopupPositionerConstraintAdjustment.SlideY | PopupPositionerConstraintAdjustment.ResizeX | PopupPositionerConstraintAdjustment.ResizeY); /// @@ -144,7 +146,9 @@ static Popup() { IsHitTestVisibleProperty.OverrideDefaultValue(false); ChildProperty.Changed.AddClassHandler((x, e) => x.ChildChanged(e)); - IsOpenProperty.Changed.AddClassHandler((x, e) => x.IsOpenChanged((AvaloniaPropertyChangedEventArgs)e)); + IsOpenProperty.Changed.AddClassHandler((x, e) => x.IsOpenChanged((AvaloniaPropertyChangedEventArgs)e)); + VerticalOffsetProperty.Changed.AddClassHandler((x, _) => x.HandlePositionChange()); + HorizontalOffsetProperty.Changed.AddClassHandler((x, _) => x.HandlePositionChange()); } /// @@ -393,18 +397,8 @@ public void Open() var handlerCleanup = new CompositeDisposable(5); - void DeferCleanup(IDisposable? disposable) - { - if (disposable is null) - { - return; - } - - handlerCleanup.Add(disposable); - } - - DeferCleanup(popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty, - HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty)); + popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty, + HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty).DisposeWith(handlerCleanup); popupHost.SetChild(Child); ((ISetLogicalParent)popupHost).SetParent(this); @@ -418,19 +412,19 @@ void DeferCleanup(IDisposable? disposable) PlacementConstraintAdjustment, PlacementRect); - DeferCleanup(SubscribeToEventHandler>(popupHost, RootTemplateApplied, + SubscribeToEventHandler>(popupHost, RootTemplateApplied, (x, handler) => x.TemplateApplied += handler, - (x, handler) => x.TemplateApplied -= handler)); + (x, handler) => x.TemplateApplied -= handler).DisposeWith(handlerCleanup); if (topLevel is Window window) { - DeferCleanup(SubscribeToEventHandler(window, WindowDeactivated, + SubscribeToEventHandler(window, WindowDeactivated, (x, handler) => x.Deactivated += handler, - (x, handler) => x.Deactivated -= handler)); + (x, handler) => x.Deactivated -= handler).DisposeWith(handlerCleanup); - DeferCleanup(SubscribeToEventHandler(window.PlatformImpl, WindowLostFocus, + SubscribeToEventHandler(window.PlatformImpl, WindowLostFocus, (x, handler) => x.LostFocus += handler, - (x, handler) => x.LostFocus -= handler)); + (x, handler) => x.LostFocus -= handler).DisposeWith(handlerCleanup); } else { @@ -438,13 +432,13 @@ void DeferCleanup(IDisposable? disposable) if (parentPopupRoot?.Parent is Popup popup) { - DeferCleanup(SubscribeToEventHandler>(popup, ParentClosed, + SubscribeToEventHandler>(popup, ParentClosed, (x, handler) => x.Closed += handler, - (x, handler) => x.Closed -= handler)); + (x, handler) => x.Closed -= handler).DisposeWith(handlerCleanup); } } - DeferCleanup(InputManager.Instance?.Process.Subscribe(ListenForNonClientClick)); + InputManager.Instance?.Process.Subscribe(ListenForNonClientClick).DisposeWith(handlerCleanup); var cleanupPopup = Disposable.Create((popupHost, handlerCleanup), state => { @@ -466,17 +460,17 @@ void DeferCleanup(IDisposable? disposable) dismissLayer.IsVisible = true; dismissLayer.InputPassThroughElement = _overlayInputPassThroughElement; - DeferCleanup(Disposable.Create(() => + Disposable.Create(() => { dismissLayer.IsVisible = false; dismissLayer.InputPassThroughElement = null; - })); + }).DisposeWith(handlerCleanup); - DeferCleanup(SubscribeToEventHandler>( + SubscribeToEventHandler>( dismissLayer, PointerPressedDismissOverlay, (x, handler) => x.PointerPressed += handler, - (x, handler) => x.PointerPressed -= handler)); + (x, handler) => x.PointerPressed -= handler).DisposeWith(handlerCleanup); } } @@ -528,6 +522,24 @@ protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs base.OnDetachedFromLogicalTree(e); Close(); } + + private void HandlePositionChange() + { + if (_openState != null) + { + var placementTarget = PlacementTarget ?? this.FindLogicalAncestorOfType(); + if (placementTarget == null) + return; + _openState.PopupHost.ConfigurePosition( + placementTarget, + PlacementMode, + new Point(HorizontalOffset, VerticalOffset), + PlacementAnchor, + PlacementGravity, + PlacementConstraintAdjustment, + PlacementRect); + } + } private static IDisposable SubscribeToEventHandler(T target, TEventHandler handler, Action subscribe, Action unsubscribe) { diff --git a/src/Avalonia.Controls/SystemDialog.cs b/src/Avalonia.Controls/SystemDialog.cs index e74b950f2312..d81926ecc7ec 100644 --- a/src/Avalonia.Controls/SystemDialog.cs +++ b/src/Avalonia.Controls/SystemDialog.cs @@ -4,30 +4,65 @@ using System.Threading.Tasks; using Avalonia.Controls.Platform; +#nullable enable + namespace Avalonia.Controls { + /// + /// Base class for system file dialogs. + /// public abstract class FileDialog : FileSystemDialog { + /// + /// Gets or sets a collection of filters which determine the types of files displayed in an + /// or an . + /// public List Filters { get; set; } = new List(); - public string InitialFileName { get; set; } + + /// + /// Gets or sets initial file name that is displayed when the dialog is opened. + /// + public string? InitialFileName { get; set; } } + /// + /// Base class for system file and directory dialogs. + /// public abstract class FileSystemDialog : SystemDialog { [Obsolete("Use Directory")] - public string InitialDirectory + public string? InitialDirectory { get => Directory; set => Directory = value; } - public string Directory { get; set; } + + /// + /// Gets or sets the initial directory that will be displayed when the file system dialog + /// is opened. + /// + public string? Directory { get; set; } } + /// + /// Represents a system dialog that prompts the user to select a location for saving a file. + /// public class SaveFileDialog : FileDialog { - public string DefaultExtension { get; set; } + /// + /// Gets or sets the default extension to be used to save the file (including the period "."). + /// + public string? DefaultExtension { get; set; } - public async Task ShowAsync(Window parent) + /// + /// Shows the save file dialog. + /// + /// The parent window. + /// + /// A task that on completion contains the full path of the save location, or null if the + /// dialog was canceled. + /// + public async Task ShowAsync(Window parent) { if(parent == null) throw new ArgumentNullException(nameof(parent)); @@ -37,11 +72,25 @@ public async Task ShowAsync(Window parent) } } + /// + /// Represents a system dialog that allows the user to select one or more files to open. + /// public class OpenFileDialog : FileDialog { + /// + /// Gets or sets a value indicating whether the user can select multiple files. + /// public bool AllowMultiple { get; set; } - public Task ShowAsync(Window parent) + /// + /// Shows the open file dialog. + /// + /// The parent window. + /// + /// A task that on completion returns an array containing the full path to the selected + /// files, or null if the dialog was canceled. + /// + public Task ShowAsync(Window parent) { if(parent == null) throw new ArgumentNullException(nameof(parent)); @@ -49,15 +98,27 @@ public Task ShowAsync(Window parent) } } + /// + /// Represents a system dialog that allows the user to select a directory. + /// public class OpenFolderDialog : FileSystemDialog { [Obsolete("Use Directory")] - public string DefaultDirectory + public string? DefaultDirectory { get => Directory; set => Directory = value; } - public Task ShowAsync(Window parent) + + /// + /// Shows the open folder dialog. + /// + /// The parent window. + /// + /// A task that on completion returns the full path of the selected directory, or null if the + /// dialog was canceled. + /// + public Task ShowAsync(Window parent) { if(parent == null) throw new ArgumentNullException(nameof(parent)); @@ -65,14 +126,32 @@ public Task ShowAsync(Window parent) } } + + /// + /// Base class for system dialogs. + /// public abstract class SystemDialog { - public string Title { get; set; } + /// + /// Gets or sets the dialog title. + /// + public string? Title { get; set; } } + /// + /// Represents a filter in an or an . + /// public class FileDialogFilter { - public string Name { get; set; } + /// + /// Gets or sets the name of the filter, e.g. ("Text files (.txt)"). + /// + public string? Name { get; set; } + + /// + /// Gets or sets a list of file extensions matched by the filter (e.g. "txt" or "*" for all + /// files). + /// public List Extensions { get; set; } = new List(); } } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 0eade8d6df91..9eae928eeb22 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -145,6 +145,18 @@ public class TextBox : TemplatedControl, UndoRedoHelper.I (o, v) => o.UndoLimit = v, unsetValue: -1); + public static readonly RoutedEvent CopyingToClipboardEvent = + RoutedEvent.Register( + "CopyingToClipboard", RoutingStrategies.Bubble); + + public static readonly RoutedEvent CuttingToClipboardEvent = + RoutedEvent.Register( + "CuttingToClipboard", RoutingStrategies.Bubble); + + public static readonly RoutedEvent PastingFromClipboardEvent = + RoutedEvent.Register( + "PastingFromClipboard", RoutingStrategies.Bubble); + readonly struct UndoRedoState : IEquatable { public string Text { get; } @@ -500,6 +512,24 @@ public int UndoLimit } } + public event EventHandler CopyingToClipboard + { + add => AddHandler(CopyingToClipboardEvent, value); + remove => RemoveHandler(CopyingToClipboardEvent, value); + } + + public event EventHandler CuttingToClipboard + { + add => AddHandler(CuttingToClipboardEvent, value); + remove => RemoveHandler(CuttingToClipboardEvent, value); + } + + public event EventHandler PastingFromClipboard + { + add => AddHandler(PastingFromClipboardEvent, value); + remove => RemoveHandler(PastingFromClipboardEvent, value); + } + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { _presenter = e.NameScope.Get("PART_TextPresenter"); @@ -638,27 +668,54 @@ public string RemoveInvalidCharacters(string text) public async void Cut() { var text = GetSelection(); - if (text is null) return; + if (string.IsNullOrEmpty(text)) + { + return; + } - SnapshotUndoRedo(); - Copy(); - DeleteSelection(); + var eventArgs = new RoutedEventArgs(CuttingToClipboardEvent); + RaiseEvent(eventArgs); + if (!eventArgs.Handled) + { + SnapshotUndoRedo(); + await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))) + .SetTextAsync(text); + DeleteSelection(); + } } public async void Copy() { var text = GetSelection(); - if (text is null) return; + if (string.IsNullOrEmpty(text)) + { + return; + } - await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))) - .SetTextAsync(text); + var eventArgs = new RoutedEventArgs(CopyingToClipboardEvent); + RaiseEvent(eventArgs); + if (!eventArgs.Handled) + { + await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))) + .SetTextAsync(text); + } } public async void Paste() { + var eventArgs = new RoutedEventArgs(PastingFromClipboardEvent); + RaiseEvent(eventArgs); + if (eventArgs.Handled) + { + return; + } + var text = await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))).GetTextAsync(); - if (text is null) return; + if (string.IsNullOrEmpty(text)) + { + return; + } SnapshotUndoRedo(); HandleTextInput(text); diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index ea06c33e4d91..73d867bf107d 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -160,13 +160,19 @@ private void RawKeyDown(RawKeyEventArgs e) return; } + var root = Root; + if (root is null) + { + return; + } + switch (e.Modifiers) { case RawInputModifiers.Control | RawInputModifiers.Shift: { IControl? control = null; - foreach (var popupRoot in GetPopupRoots(Root)) + foreach (var popupRoot in GetPopupRoots(root)) { control = GetHoveredControl(popupRoot); @@ -176,7 +182,7 @@ private void RawKeyDown(RawKeyEventArgs e) } } - control ??= GetHoveredControl(Root); + control ??= GetHoveredControl(root); if (control != null) { @@ -190,7 +196,7 @@ private void RawKeyDown(RawKeyEventArgs e) { vm.FreezePopups = !vm.FreezePopups; - foreach (var popupRoot in GetPopupRoots(Root)) + foreach (var popupRoot in GetPopupRoots(root)) { if (popupRoot.Parent is Popup popup) { diff --git a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs index 55e30396e1bc..5d7619d18460 100644 --- a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs +++ b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs @@ -30,13 +30,13 @@ public static void OpenBrowser(string url) } else { - using (Process process = Process.Start(new ProcessStartInfo + using Process process = Process.Start(new ProcessStartInfo { FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? url : "open", Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? $"{url}" : "", CreateNoWindow = true, UseShellExecute = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - })); + }); } } diff --git a/src/Avalonia.Dialogs/ManagedFileChooser.cs b/src/Avalonia.Dialogs/ManagedFileChooser.cs index f9f38ac47489..9058c405a36f 100644 --- a/src/Avalonia.Dialogs/ManagedFileChooser.cs +++ b/src/Avalonia.Dialogs/ManagedFileChooser.cs @@ -1,13 +1,11 @@ using System; using System.Linq; using System.Threading.Tasks; -using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; -using Avalonia.Markup.Xaml; namespace Avalonia.Dialogs { @@ -35,7 +33,9 @@ private void OnPointerPressed(object sender, PointerPressedEventArgs e) if (_quickLinksRoot != null) { var isQuickLink = _quickLinksRoot.IsLogicalAncestorOf(e.Source as Control); +#pragma warning disable CS0618 // Type or member is obsolete if (e.ClickCount == 2 || isQuickLink) +#pragma warning restore CS0618 // Type or member is obsolete { if (model.ItemType == ManagedFileChooserItemType.File) { diff --git a/src/Avalonia.Dialogs/ManagedFileChooserSources.cs b/src/Avalonia.Dialogs/ManagedFileChooserSources.cs index 050d618ce164..a217a67bc634 100644 --- a/src/Avalonia.Dialogs/ManagedFileChooserSources.cs +++ b/src/Avalonia.Dialogs/ManagedFileChooserSources.cs @@ -67,7 +67,7 @@ public static ManagedFileChooserNavigationItem[] DefaultGetFileSystemRoots() { Directory.GetFiles(x.VolumePath); } - catch (Exception _) + catch (Exception) { return null; } diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 268171d46753..63cbfb2dbec6 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -47,6 +47,8 @@ public IGeometryImpl CreateRectangleGeometry(Rect rect) } public IStreamGeometryImpl CreateStreamGeometry() => new HeadlessStreamingGeometryStub(); + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => throw new NotImplementedException(); + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => throw new NotImplementedException(); public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => new HeadlessRenderTarget(); diff --git a/src/Avalonia.Input/AccessKeyHandler.cs b/src/Avalonia.Input/AccessKeyHandler.cs index 5c4af68d796c..5082265ea647 100644 --- a/src/Avalonia.Input/AccessKeyHandler.cs +++ b/src/Avalonia.Input/AccessKeyHandler.cs @@ -157,10 +157,9 @@ protected virtual void OnPreviewKeyDown(object sender, KeyEventArgs e) _restoreFocusElement?.Focus(); _restoreFocusElement = null; + + e.Handled = true; } - - // We always handle the Alt key. - e.Handled = true; } else if (_altIsDown) { diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index f2cc9e9072ad..8d7400130981 100644 --- a/src/Avalonia.Input/Gestures.cs +++ b/src/Avalonia.Input/Gestures.cs @@ -81,17 +81,21 @@ private static void PointerPressed(RoutedEventArgs ev) var e = (PointerPressedEventArgs)ev; var visual = (IVisual)ev.Source; - if (e.ClickCount <= 1) +#pragma warning disable CS0618 // Type or member is obsolete + var clickCount = e.ClickCount; +#pragma warning restore CS0618 // Type or member is obsolete + if (clickCount <= 1) { s_lastPress = new WeakReference(ev.Source); } - else if (s_lastPress != null && e.ClickCount == 2 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) + else if (s_lastPress != null && clickCount == 2 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) { if (s_lastPress.TryGetTarget(out var target) && target == e.Source) { e.Source.RaiseEvent(new TappedEventArgs(DoubleTappedEvent, e)); } } + } } diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index cfa3690daf58..401c6cb2ac78 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -75,7 +75,9 @@ public Point GetPosition(IVisual relativeTo) throw new InvalidOperationException("Control is not attached to visual tree."); } +#pragma warning disable CS0618 // Type or member is obsolete var rootPoint = relativeTo.VisualRoot.PointToClient(Position); +#pragma warning restore CS0618 // Type or member is obsolete var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo); return rootPoint * transform!.Value; } diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs index ed7df67bf221..c8290cb3b79e 100644 --- a/src/Avalonia.Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Input/Navigation/TabNavigation.cs @@ -234,7 +234,7 @@ internal static class TabNavigation // Return the first visible element. var uiElement = e as InputElement; - if (uiElement is null || uiElement.IsVisible) + if (uiElement is null || IsVisibleAndEnabled(uiElement)) { if (e is IVisual elementAsVisual) { @@ -245,7 +245,7 @@ internal static class TabNavigation { if (children[i] is InputElement ie) { - if (ie.IsVisible) + if (IsVisibleAndEnabled(ie)) return ie; else { @@ -270,7 +270,7 @@ internal static class TabNavigation // Return the last visible element. var uiElement = e as InputElement; - if (uiElement == null || uiElement.IsVisible) + if (uiElement == null || IsVisibleAndEnabled(uiElement)) { var elementAsVisual = e as IVisual; @@ -283,7 +283,7 @@ internal static class TabNavigation { if (children[i] is InputElement ie) { - if (ie.IsVisible) + if (IsVisibleAndEnabled(ie)) return ie; else { @@ -600,7 +600,7 @@ internal static class TabNavigation var vchild = children[i]; if (vchild == elementAsVisual) break; - if (vchild.IsVisible == true && vchild is IInputElement ie) + if (vchild is IInputElement ie && IsVisibleAndEnabled(ie)) prev = ie; } return prev; @@ -668,5 +668,6 @@ private static bool IsTabStop(IInputElement e) } private static bool IsTabStopOrGroup(IInputElement e) => IsTabStop(e) || IsGroup(e); + private static bool IsVisibleAndEnabled(IInputElement e) => e.IsVisible && e.IsEnabled; } } diff --git a/src/Avalonia.Themes.Default/Expander.xaml b/src/Avalonia.Themes.Default/Expander.xaml index 5e0958c54ce9..7df65677b6f6 100644 --- a/src/Avalonia.Themes.Default/Expander.xaml +++ b/src/Avalonia.Themes.Default/Expander.xaml @@ -101,6 +101,7 @@ Grid.Column="1" Background="Transparent" Content="{TemplateBinding Content}" + ContentTemplate="{Binding $parent[Expander].HeaderTemplate}" VerticalAlignment="Center" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" diff --git a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml index 39030969334e..72c25cea37d9 100644 --- a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml @@ -75,8 +75,6 @@ @@ -199,6 +197,8 @@