Skip to content
Merged
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 @@ -37,7 +37,46 @@ derived from, any tab tracked by `Shell`.
by the "Expose Targets" tab's add-target picker, drawn from
`Shell.CurrentWorkspace.Workspace.Declarations`, excluding stdlib names and
node kinds that would fail `ViewDefinitionModel` validation (views,
viewpoints, imports, metadata, transitions, connections).
viewpoints, imports, metadata, transitions, connections). Remains the
master, unfiltered, sorted list; `DisplayedExposeTargets` (below) is what the
picker `ListBox` actually binds to.

**AvailableExposeTargetTypeLabels**: `IReadOnlyList<string>` — every distinct
human-readable "type label" present among the current `AvailableExposeTargets`
candidates, sorted ordinally. Each candidate's label is computed from its
underlying `SysmlNode` subtype: a `SysmlDefinitionNode` yields its
`DefinitionKeyword` (e.g. `"part def"`), a `SysmlFeatureNode` yields its
`FeatureKeyword` (e.g. `"part"`), and `SysmlPackageNode`/`SysmlDependencyNode`/
`SysmlSatisfyNode` yield the fixed literals `"package"`/`"dependency"`/
`"satisfy"` respectively (none of these three expose a keyword-like property
of their own). Any other node kind not already excluded from the picker falls
back to a defensively derived label (its runtime type name with a leading
`Sysml` and/or trailing `Node` stripped, lowercased), so an unrecognized
future node kind is labeled reasonably rather than crashing or silently
vanishing from the picker.

**ActiveExposeTypeFilters**: `ObservableCollection<string>` — the type-label
chips currently applied over the picker, combined with OR semantics (an item
is shown when its type label is any one of these); empty means no type
restriction (every candidate's type is shown). `RefreshFromWorkspace`
pre-populates this with just `"part"` when that label is present in the
current workspace, since narrowing to part usages is the most common
starting point; otherwise it starts empty. Mutated only through
`AddExposeTypeFilter`/`RemoveExposeTypeFilter` (or, defensively, any other
direct mutation, which is also observed) rather than replaced wholesale, so
the view's chip-row `ItemsControl` can bind to this instance directly.

**ExposeTargetSearchText**: `string?` — the picker's free-text search box
value, two-way bound from the view. A non-empty value narrows
`DisplayedExposeTargets` to qualified names containing it as a
case-insensitive substring, applied with AND semantics against whatever the
active type filter already narrowed to.

**DisplayedExposeTargets**: `IReadOnlyList<string>` — the picker `ListBox`'s
actual data source: `AvailableExposeTargets`, narrowed first by
`ActiveExposeTypeFilters` (OR semantics) and then by `ExposeTargetSearchText`
(AND semantics), preserving the master list's ordinal sort order. Recomputed
whenever any of the three inputs changes.

**StatusMessage**: `string?` — set when `RenderPreview` or `TryCommit` fails,
so the view can surface the failure inline without a modal message box.
Expand All @@ -48,15 +87,27 @@ unusable, target-less picker.

#### Key Methods

**RefreshFromWorkspace**: Refreshes `AvailableExposeTargets` and
`IsWorkspaceEmpty` from the shell's current workspace state.
**RefreshFromWorkspace**: Refreshes `AvailableExposeTargets`,
`AvailableExposeTargetTypeLabels`, `ActiveExposeTypeFilters`,
`DisplayedExposeTargets`, and `IsWorkspaceEmpty` from the shell's current
workspace state.

- *Parameters*: none.
- *Returns*: `void` — both properties update in place.
- *Returns*: `void` — all listed properties update in place.
- *Postconditions*: Called once at construction; the dialog is short-lived
(opened, edited, closed) so it does not itself subscribe to
`MainWindowShell.SourcesChanged` the way the old long-lived panel view
model did.
model did. Re-applies the `"part"`-default chip rule fresh each call.

**AddExposeTypeFilter** / **RemoveExposeTypeFilter**: Add or remove a type
label from `ActiveExposeTypeFilters` and recompute `DisplayedExposeTargets`.

- *Parameters*: the type label to add or remove.
- *Returns*: `void` — `ActiveExposeTypeFilters` and `DisplayedExposeTargets`
update in place.
- *Postconditions*: `AddExposeTypeFilter` is dedupe-safe (adding an
already-active label is a no-op beyond the recompute); `RemoveExposeTypeFilter`
no-ops when the label is not currently active, rather than throwing.

**AddExposeTarget** / **RemoveExposeTarget** / **SetExposeRecursionKind** /
**SetExposeBracketFilter**: Port the deleted `CustomViewBuilderToolViewModel`'s
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,25 @@ sections:
composition rather than resuming a previous, possibly-cancelled session.
tests:
- 'Construction_FreshInstance_DoesNotCarryOverPriorInstanceSelections'

- id: 'SysML2Workbench-AppShellSubsystem-ViewBuilderDialog-FilterExposeTargetsByTypeAndText'
title: 'The ViewBuilderDialog shall let the user narrow the expose-target picker by one or more
type-label chips (OR semantics, defaulting to a single "part" chip when available, or no
restriction when empty) combined with a case-insensitive name-substring search (AND semantics),
computing each candidate''s type label from its underlying SysmlNode subtype.'
justification: |
A workspace can contain far more candidate elements than fit comfortably in the picker
ListBox; letting the user narrow by kind (part def, part, package, etc.) and/or name text
makes finding the right expose target practical without scrolling through the entire
workspace, while the "part" default keeps the picker usefully narrowed out of the box.
tests:
- 'RefreshFromWorkspace_ComputesTypeLabelsPerNodeKind'
- 'RefreshFromWorkspace_AvailableExposeTargetTypeLabels_IsDistinctAndSorted'
- 'Construction_PartLabelPresent_DefaultsActiveExposeTypeFiltersToPart'
- 'Construction_PartLabelAbsent_ActiveExposeTypeFiltersStartsEmpty'
- 'DisplayedExposeTargets_EmptyChips_ShowsAllTypes'
- 'DisplayedExposeTargets_MultipleChips_AppliesOrSemantics'
- 'DisplayedExposeTargets_TypeFilterAndTextSearch_CombineWithAndSemantics'
- 'AddExposeTypeFilter_DuplicateLabel_DoesNotAddSecondChip'
- 'RemoveExposeTypeFilter_RemovesChipAndNoOpsWhenAbsent'
- 'ExposeTargetSearchTextChanged_RecomputesDisplayedExposeTargets'
29 changes: 19 additions & 10 deletions docs/user_guide/getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,16 +103,25 @@ to it.

1. On the **View Kind** tab, pick a kind (General, Interconnection, State
Transition, Action Flow, Sequence, or Grid).
2. On the **Expose Targets** tab, pick an element or package from the loaded
workspace and click **Add** to add it to the exposed-targets list. Repeat
for every element or package you want exposed - custom views support
multiple exposed elements, just like a hand-written SysML view. For each
row, choose its **recursion kind** - *This element only*, *This element +
everything below (::\*\*)* (the default), *Direct children only (::\*)*,
or *All descendants, not itself (::\*::\*\*)* - and optionally enter a
**bracket-filter expression** (enabled only for the two recursive kinds)
to narrow that target's exposed membership. Click **Remove** to drop a
row entirely.
2. On the **Expose Targets** tab, narrow the picker if needed, then pick an
element or package and click **Add** to add it to the exposed-targets
list. The picker starts filtered to a single **part** type chip (the most
common starting point); click **✕** on a chip to remove it, or click **+**
to open a flyout listing every other type label present in the workspace
(for example *part def*, *package*, *dependency*) and pick one to add
another chip - active chips combine so an element matching any one of
them is shown, and removing every chip lifts the type restriction
entirely. Use the **Filter by name...** search box alongside the chips to
further narrow the list to qualified names containing the typed text,
case-insensitively; the type-chip and name-search filters combine
together. Repeat **Add** for every element or package you want exposed -
custom views support multiple exposed elements, just like a hand-written
SysML view. For each row, choose its **recursion kind** - *This element
only*, *This element + everything below (::\*\*)* (the default), *Direct
children only (::\*)*, or *All descendants, not itself (::\*::\*\*)* - and
optionally enter a **bracket-filter expression** (enabled only for the two
recursive kinds) to narrow that target's exposed membership. Click
**Remove** to drop a row entirely.
3. On the **Filter & Name** tab, optionally enter a **Filter Expression** to
narrow what is included, and optionally enter a **View Name** - otherwise
a default name is used.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,51 @@ filter expression, display name) that is never committed leaves the shell's `Ope
over the same shell right after a prior instance was edited, starts from a completely clean `Definition` - no
view kind, expose targets, filter expression, or display name carried over. Verified by
`ViewBuilderDialogViewModelTests.Construction_FreshInstance_DoesNotCarryOverPriorInstanceSelections`.

**RefreshFromWorkspace_ComputesTypeLabelsPerNodeKind**: Constructing the view model over a workspace containing a
`part def`, a `part` usage, and a bare `package` computes the correct type label for each - `"part def"`,
`"part"`, and `"package"` respectively - in `AvailableExposeTargetTypeLabels`. Verified by
`ViewBuilderDialogViewModelTests.RefreshFromWorkspace_ComputesTypeLabelsPerNodeKind`.

**RefreshFromWorkspace_AvailableExposeTargetTypeLabels_IsDistinctAndSorted**: `AvailableExposeTargetTypeLabels`
contains no duplicate labels and is sorted ordinally. Verified by
`ViewBuilderDialogViewModelTests.RefreshFromWorkspace_AvailableExposeTargetTypeLabels_IsDistinctAndSorted`.

**Construction_PartLabelPresent_DefaultsActiveExposeTypeFiltersToPart**: Constructing the view model over a
workspace where `"part"` is an available type label pre-populates `ActiveExposeTypeFilters` with exactly that one
chip. Verified by
`ViewBuilderDialogViewModelTests.Construction_PartLabelPresent_DefaultsActiveExposeTypeFiltersToPart`.

**Construction_PartLabelAbsent_ActiveExposeTypeFiltersStartsEmpty**: Constructing the view model over a workspace
with no `"part"`-labeled elements leaves `ActiveExposeTypeFilters` empty, applying no type restriction by default.
Verified by
`ViewBuilderDialogViewModelTests.Construction_PartLabelAbsent_ActiveExposeTypeFiltersStartsEmpty`.

**DisplayedExposeTargets_EmptyChips_ShowsAllTypes**: Removing the only active type-filter chip leaves
`DisplayedExposeTargets` equal to the full `AvailableExposeTargets` list, confirming empty chips means no type
restriction. Verified by
`ViewBuilderDialogViewModelTests.DisplayedExposeTargets_EmptyChips_ShowsAllTypes`.

**DisplayedExposeTargets_MultipleChips_AppliesOrSemantics**: Activating two type-filter chips (`"part def"` and
`"package"`) shows candidates matching either label while excluding a candidate whose label matches neither,
confirming OR semantics across chips. Verified by
`ViewBuilderDialogViewModelTests.DisplayedExposeTargets_MultipleChips_AppliesOrSemantics`.

**DisplayedExposeTargets_TypeFilterAndTextSearch_CombineWithAndSemantics**: With two type-filter chips active,
setting `ExposeTargetSearchText` to a case-insensitive substring further narrows `DisplayedExposeTargets` to only
the candidates matching both the type filter and the text search, confirming AND semantics between the two
filters. Verified by
`ViewBuilderDialogViewModelTests.DisplayedExposeTargets_TypeFilterAndTextSearch_CombineWithAndSemantics`.

**AddExposeTypeFilter_DuplicateLabel_DoesNotAddSecondChip**: Calling `AddExposeTypeFilter` twice with the same
label results in exactly one chip in `ActiveExposeTypeFilters`, confirming dedupe-safe behavior. Verified by
`ViewBuilderDialogViewModelTests.AddExposeTypeFilter_DuplicateLabel_DoesNotAddSecondChip`.

**RemoveExposeTypeFilter_RemovesChipAndNoOpsWhenAbsent**: Removing the default `"part"` chip clears
`ActiveExposeTypeFilters`, and a subsequent removal of a label that is not currently active is a no-op rather
than a throw. Verified by
`ViewBuilderDialogViewModelTests.RemoveExposeTypeFilter_RemovesChipAndNoOpsWhenAbsent`.

**ExposeTargetSearchTextChanged_RecomputesDisplayedExposeTargets**: Setting `ExposeTargetSearchText` alone (with
no other method call) live-recomputes `DisplayedExposeTargets` to the matching subset. Verified by
`ViewBuilderDialogViewModelTests.ExposeTargetSearchTextChanged_RecomputesDisplayedExposeTargets`.
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,39 @@
<ScrollViewer>
<StackPanel Margin="8" Spacing="6">
<TextBlock Text="Available (select, then Add)" />
<ListBox Name="AvailableExposeTargetsListBox" ItemsSource="{Binding AvailableExposeTargets}" Height="120" />

<StackPanel Orientation="Horizontal" Spacing="4">
<ItemsControl Name="ActiveExposeTypeFiltersItemsControl" ItemsSource="{Binding ActiveExposeTypeFilters}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border CornerRadius="8" Padding="6,2" Margin="0,0,4,4" Background="#E0E0E0">
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock Text="{Binding}" VerticalAlignment="Center" />
<Button Content="✕" Tag="{Binding}" Click="OnRemoveExposeTypeFilterClick"
Padding="2,0" MinWidth="0" MinHeight="0" FontSize="10" />
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

<Button Name="AddExposeTypeFilterButton" Content="+" Padding="6,2" MinWidth="0">
<Button.Flyout>
<Flyout Placement="Bottom">
<ListBox Name="AddableExposeTypeFilterListBox" Width="160" MaxHeight="200" />
</Flyout>
</Button.Flyout>
</Button>
</StackPanel>

<TextBox Name="ExposeTargetSearchTextBox" PlaceholderText="Filter by name..." Text="{Binding ExposeTargetSearchText, Mode=TwoWay}" />

<ListBox Name="AvailableExposeTargetsListBox" ItemsSource="{Binding DisplayedExposeTargets}" Height="120" />
<Button Name="AddExposeTargetButton" Content="Add →" HorizontalAlignment="Stretch" />

<StackPanel Orientation="Horizontal" Spacing="4">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ public ViewBuilderDialogView(MainWindowShell shell)

AddExposeTargetButton.Click += OnAddExposeTargetClick;

AddExposeTypeFilterButton.Flyout!.Opened += OnAddExposeTypeFilterFlyoutOpening;
AddableExposeTypeFilterListBox.SelectionChanged += OnAddableExposeTypeFilterSelectionChanged;

FilterExpressionTextBox.LostFocus += OnFilterExpressionLostFocus;
DisplayNameTextBox.LostFocus += OnDisplayNameLostFocus;

Expand Down Expand Up @@ -106,6 +109,42 @@ private void OnPreviewChanged(object? sender, EventArgs e)
LoadPreviewImage();
}

/// <summary>
/// Populates <see cref="AddableExposeTypeFilterListBox" /> from the view model's currently addable type
/// labels each time the "+" button's flyout is about to open, so the list always reflects the latest
/// workspace/active-filter state rather than a stale snapshot from construction time.
/// </summary>
private void OnAddExposeTypeFilterFlyoutOpening(object? sender, EventArgs e)
{
AddableExposeTypeFilterListBox.ItemsSource = _viewModel.GetAddableExposeTargetTypeLabels();
}

/// <summary>
/// Adds the selected type label as a new active filter chip and closes the flyout.
/// </summary>
private void OnAddableExposeTypeFilterSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (AddableExposeTypeFilterListBox.SelectedItem is not string typeLabel)
{
return;
}

_viewModel.AddExposeTypeFilter(typeLabel);
AddableExposeTypeFilterListBox.SelectedItem = null;
AddExposeTypeFilterButton.Flyout?.Hide();
}

/// <summary>
/// Removes the clicked chip's type label from the active filters.
/// </summary>
private void OnRemoveExposeTypeFilterClick(object? sender, RoutedEventArgs e)
{
if (sender is Button { Tag: string typeLabel })
{
_viewModel.RemoveExposeTypeFilter(typeLabel);
}
}

/// <summary>
/// Loads this dialog's currently rendered preview SVG into the on-screen image control.
/// </summary>
Expand Down
Loading
Loading