Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 1 addition & 130 deletions src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Media;
using Avalonia.Threading;

namespace Avalonia.Controls
{
/// <summary>
/// Presents a color for user editing using a spectrum, palette and component sliders.
/// </summary>
[TemplatePart("PART_HexTextBox", typeof(TextBox))]
[TemplatePart("PART_TabControl", typeof(TabControl))]
public partial class ColorView : TemplatedControl
{
/// <summary>
Expand All @@ -22,7 +20,6 @@ public partial class ColorView : TemplatedControl

// XAML template parts
private TextBox? _hexTextBox;
private TabControl? _tabControl;

protected bool _ignorePropertyChanged = false;

Expand Down Expand Up @@ -68,110 +65,7 @@ private void SetColorToHexTextBox()
includeSymbol: false);
}
}

/// <summary>
/// Validates the tab/panel/page selection taking into account the visibility of each item
/// as well as the current selection.
/// </summary>
/// <remarks>
/// Derived controls may re-implement this based on their default style / control template
/// and any specialized selection needs.
/// </remarks>
protected virtual void ValidateSelection()
{
if (_tabControl != null &&
_tabControl.Items != null)
{
// Determine the number of visible tab items
int numVisibleItems = 0;
foreach (var item in _tabControl.Items)
{
if (item is Control control &&
control.IsVisible)
{
numVisibleItems++;
}
}

// Verify the selection
if (numVisibleItems > 0)
{
object? selectedItem = null;

if (_tabControl.SelectedItem == null &&
_tabControl.ItemCount > 0)
{
// As a failsafe, forcefully select the first item
foreach (var item in _tabControl.Items)
{
selectedItem = item;
break;
}
}
else
{
selectedItem = _tabControl.SelectedItem;
}

if (selectedItem is Control selectedControl &&
selectedControl.IsVisible == false)
{
// Select the first visible item instead
foreach (var item in _tabControl.Items)
{
if (item is Control control &&
control.IsVisible)
{
selectedItem = item;
break;
}
}
}

_tabControl.SelectedItem = selectedItem;
_tabControl.IsVisible = true;
}
else
{
// Special case when all items are hidden
// If TabControl ever properly supports no selected item /
// all items hidden this can be removed
_tabControl.SelectedItem = null;
_tabControl.IsVisible = false;
}

// Hide the "tab strip" if there is only one tab
// This allows, for example, to view only the palette
/*
var itemsPresenter = _tabControl.FindDescendantOfType<ItemsPresenter>();
if (itemsPresenter != null)
{
if (numVisibleItems == 1)
{
itemsPresenter.IsVisible = false;
}
else
{
itemsPresenter.IsVisible = true;
}
}
*/

// Note that if externally the SelectedIndex is set to 4 or something
// outside the valid range, the TabControl will ignore it and replace it
// with a valid SelectedIndex. This however is not propagated back through
// the TwoWay binding in the control template so the SelectedIndex and
// SelectedIndex become out of sync.
//
// The work-around for this is done here where SelectedIndex is forcefully
// synchronized with whatever the TabControl property value is. This is
// possible since selection validation is already done by this method.
SetCurrentValue(SelectedIndexProperty, _tabControl.SelectedIndex);
}

return;
}


/// <inheritdoc/>
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
Expand All @@ -182,7 +76,6 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
}

_hexTextBox = e.NameScope.Find<TextBox>("PART_HexTextBox");
_tabControl = e.NameScope.Find<TabControl>("PART_TabControl");

SetColorToHexTextBox();

Expand All @@ -193,7 +86,6 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
}

base.OnApplyTemplate(e);
ValidateSelection();
}

/// <inheritdoc/>
Expand Down Expand Up @@ -260,27 +152,6 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
// (Color will be coerced automatically if HsvColor changes)
SetCurrentValue(HsvColorProperty, OnCoerceHsvColor(HsvColor));
}
else if (change.Property == IsColorComponentsVisibleProperty ||
change.Property == IsColorPaletteVisibleProperty ||
change.Property == IsColorSpectrumVisibleProperty)
{
// When the property changed notification is received here the visibility
// of individual tab items has not yet been updated through the bindings.
// Therefore, the validation is delayed until after bindings update.
Dispatcher.UIThread.Post(() =>
{
ValidateSelection();
}, DispatcherPriority.Background);
}
else if (change.Property == SelectedIndexProperty)
{
// Again, it is necessary to wait for the SelectedIndex value to
// be applied to the TabControl through binding before validation occurs.
Dispatcher.UIThread.Post(() =>
{
ValidateSelection();
}, DispatcherPriority.Background);
}

base.OnPropertyChanged(change);
}
Expand Down
73 changes: 69 additions & 4 deletions src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ private protected override void OnItemsViewCollectionChanged(object? sender, Not

if (AlwaysSelected && SelectedIndex == -1 && ItemCount > 0)
{
SelectedIndex = 0;
SelectedIndex = GetFirstVisibleAndEnabledIndex();
}
}

Expand Down Expand Up @@ -534,6 +534,11 @@ protected internal override void ContainerForItemPreparedOverride(Control contai

if (Selection.AnchorIndex == index)
KeyboardNavigation.SetTabOnceActiveElement(this, container);

if (AlwaysSelected && index == SelectedIndex && (!container.IsVisible || !container.IsEnabled))
{
MoveSelectionToFirstVisibleAndEnabledItem();
}
}

/// <inheritdoc />
Expand Down Expand Up @@ -616,6 +621,13 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
{
AutoScrollToSelectedItemIfNecessary(GetAnchorIndex());
}
else if (change.Property == IsVisibleProperty)
{
if (change.GetNewValue<bool>())
{
AutoScrollToSelectedItemIfNecessary(GetAnchorIndex());
}
}
else if (change.Property == SelectionModeProperty && _selection is object)
{
var newValue = change.GetNewValue<SelectionMode>();
Expand Down Expand Up @@ -1035,7 +1047,7 @@ private void OnSelectionModelLostSelection(object? sender, EventArgs e)
{
if (AlwaysSelected && ItemsView.Count > 0)
{
SelectedIndex = 0;
SelectedIndex = GetFirstVisibleAndEnabledIndex();
}
}

Expand Down Expand Up @@ -1153,6 +1165,11 @@ Presenter is object &&
anchorIndex >= 0 &&
IsAttachedToVisualTree)
{
if (!IsEffectivelyVisible)
{
return;
}

Dispatcher.UIThread.Post(state =>
{
ScrollIntoView((int)state!);
Expand Down Expand Up @@ -1205,6 +1222,54 @@ private void MarkContainerSelected(Control container, bool selected)
}
}

/// <summary>
/// Finds the first visible and enabled index in the ItemsSource.
/// </summary>
/// <returns>the index of the first visible and enabled item, or -1 if none found</returns>
private int GetFirstVisibleAndEnabledIndex()
Copy link
Copy Markdown
Contributor

@robloo robloo Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll place general comments here. Without testing, my understanding of WPF is:

  1. Enabled=false tabs are still visible but can't be clicked. If there are NO other tabs the Enabled=False tab should be the one visible but with all it's content disabled.
  2. Visible=false tabs are completely hidden and removed from the control

Part 1 likely doesn't function the same since you are treating Enabled/Visible as equivalent -- they have slight differences.

Visible=false

  • Never visible in the tab strip, so can't be selected by the user
  • Never visible in the tab content (this is the needed fix)

Enabled=false

  • Always visible in the tab strip.... but with the disabled styling. Cannot be selected by the user, but can be selected as the default.
  • MAY BE visible in the tab content if there are NO other tab pages

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tabs can be selected programatically. I am not sure how to deal with that if really all tabs are either disabled or hidden. We should review this in the team before making an decision.

{
var count = ItemCount;
if (count == 0)
return -1;

for (var i = 0; i < count; i++)
{
var container = ContainerFromIndex(i);
if (container is not null)
{
if (container is { IsVisible: true, IsEnabled: true })
return i;
}
else
{
var item = ItemsView[i];
if (item is Visual v)
{
if (v.IsVisible && (v is not Control c || c.IsEnabled))
return i;
}
else if (item is not null)
{
return i;
}
}
}

return -1;
}

/// <summary>
/// this method moves selection to first visible and enabled item.
/// </summary>
private void MoveSelectionToFirstVisibleAndEnabledItem()
{
var index = GetFirstVisibleAndEnabledIndex();
if (index != -1 && index != SelectedIndex)
{
SelectedIndex = index;
}
}

private void UpdateContainerSelection()
{
if (Presenter?.Panel is { } panel)
Expand Down Expand Up @@ -1251,7 +1316,7 @@ private void InitializeSelectionModel(ISelectionModel model)

if (_updateState is null && AlwaysSelected && model.Count == 0)
{
model.SelectedIndex = 0;
model.SelectedIndex = GetFirstVisibleAndEnabledIndex();
}

UpdateContainerSelection();
Expand Down Expand Up @@ -1358,7 +1423,7 @@ private void EndUpdating()

if (AlwaysSelected && SelectedIndex == -1 && ItemCount > 0)
{
SelectedIndex = 0;
SelectedIndex = GetFirstVisibleAndEnabledIndex();
}
}
}
Expand Down
Loading
Loading