Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ public CommandPalettePageViewModelFactory(TaskScheduler scheduler, IContextMenuF
{
MainListPage listPage => new ListViewModel(listPage, _scheduler, host, providerContext, _contextMenuFactory) { IsRootPage = !nested, IsMainPage = true },
IListPage listPage => new ListViewModel(listPage, _scheduler, host, providerContext, _contextMenuFactory) { IsRootPage = !nested },

// ITabbedPage must be matched before IContentPage: the Toolkit
// TabbedPage base implements both, and only newer hosts render the
// tab strip (older hosts fall back to the IContentPage message).
ITabbedPage tabbedPage => new TabbedPageViewModel(tabbedPage, _scheduler, host, providerContext, this) { IsRootPage = !nested },
IContentPage contentPage => new CommandPaletteContentPageViewModel(contentPage, _scheduler, host, providerContext),
IParametersPage paramsPage => new ParametersPageViewModel(paramsPage, _scheduler, host, providerContext, _contextMenuFactory),
_ => null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ public PageViewModel CurrentPage
IsSearchBoxVisible = true;
}

// The search box and filters follow the active leaf page, which for a
// tabbed page is the active tab's child rather than the container.
OnPropertyChanged(nameof(SearchHostPage));

if (oldValue is IDisposable disposable)
{
try
Expand All @@ -88,8 +92,28 @@ private void CurrentPage_PropertyChanged(object? sender, PropertyChangedEventArg
{
IsSearchBoxVisible = CurrentPage.HasSearchBox;
}
else if (e.PropertyName == nameof(TabbedPageViewModel.ActiveChild))
{
// A tabbed page switched its active tab. Re-target the search box and
// filters at the new child so search and filtering operate on the tab
// that is now visible.
OnPropertyChanged(nameof(SearchHostPage));
}
}

/// <summary>
/// Gets the page that drives the shared search box and filters. For a tabbed
/// page this is the active tab's child page (a real list, content or
/// parameters page) so the search box renders and filters the active tab; for
/// every other page it is simply the current page. The SearchBar only renders
/// its text box for a concrete list page, so the container tabbed page must be
/// seen through to its child here.
/// </summary>
public PageViewModel? SearchHostPage =>
_currentPage is TabbedPageViewModel { ActiveChild: { } activeChild }
? activeChild
: _currentPage;

private IPage? _rootPage;

private bool _isNested;
Expand Down
137 changes: 137 additions & 0 deletions src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TabViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;

namespace Microsoft.CmdPal.UI.ViewModels;

/// <summary>
/// View model for a single tab within a <see cref="TabbedPageViewModel"/>. Wraps
/// an extension-provided <see cref="ITab"/> and surfaces its chrome (title, icon,
/// observable badge) so the tab strip can render before the hosted page is
/// initialized. The hosted <see cref="IPage"/> itself is exposed via
/// <see cref="Page"/> and only turned into a child view model lazily, the first
/// time the tab becomes active.
/// </summary>
public partial class TabViewModel : ExtensionObjectViewModel
{
private readonly ExtensionObject<ITab> _model;

/// <summary>
/// Gets the stable identity for this tab, used to preserve the active tab
/// across dynamic tab-set updates. This is the hosted page's <c>Id</c> when
/// available; otherwise it falls back to the tab title.
/// </summary>
public string TabId { get; private set; } = string.Empty;

public string Title { get; private set; } = string.Empty;

public string Badge { get; private set; } = string.Empty;

public bool HasBadge => !string.IsNullOrEmpty(Badge);

public IconInfoViewModel Icon { get; private set; } = new(null);

public bool HasIcon => Icon.IsSet;

/// <summary>
/// Gets the raw extension page hosted by this tab. The host turns this into
/// a child <see cref="PageViewModel"/> through the page view model factory
/// when the tab is first activated.
/// </summary>
public IPage? Page { get; private set; }

public TabViewModel(ITab tab, WeakReference<IPageContext> context)
: base(context)
{
_model = new(tab);
}

public override void InitializeProperties()
{
var tab = _model.Unsafe;
if (tab is null)
{
return;
}

Page = tab.Page;

var title = tab.Title;
if (string.IsNullOrEmpty(title))
{
// Fall back to the hosted page's own name/title so the strip is
// never blank when the extension didn't set a tab title.
title = Page?.Title;
if (string.IsNullOrEmpty(title))
{
title = Page?.Name;
}
}

Title = title ?? string.Empty;
Badge = tab.Badge ?? string.Empty;

var pageId = Page?.Id;
TabId = string.IsNullOrEmpty(pageId) ? Title : pageId;

Icon = new(tab.Icon);
Icon.InitializeProperties();

UpdateProperty(nameof(Title));
UpdateProperty(nameof(Badge));
UpdateProperty(nameof(HasBadge));
UpdateProperty(nameof(Icon));
UpdateProperty(nameof(HasIcon));

tab.PropChanged += Model_PropChanged;
}

private void Model_PropChanged(object sender, IPropChangedEventArgs args)
{
try
{
var tab = _model.Unsafe;
if (tab is null)
{
return;
}

switch (args.PropertyName)
{
case nameof(Badge):
Badge = tab.Badge ?? string.Empty;
UpdateProperty(nameof(Badge));
UpdateProperty(nameof(HasBadge));
break;
case nameof(Title):
Title = string.IsNullOrEmpty(tab.Title) ? Title : tab.Title;
UpdateProperty(nameof(Title));
break;
case nameof(Icon):
Icon = new(tab.Icon);
Icon.InitializeProperties();
UpdateProperty(nameof(Icon));
UpdateProperty(nameof(HasIcon));
break;
}
}
catch (Exception ex)
{
ShowException(ex);
}
}

protected override void UnsafeCleanup()
{
base.UnsafeCleanup();

var tab = _model.Unsafe;
if (tab is not null)
{
tab.PropChanged -= Model_PropChanged;
}
}
}
Loading