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
8 changes: 8 additions & 0 deletions docs/design/ots/avalonia.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ platform-specific UI stacks.
UI.
- **Pointer and keyboard input** — supports diagram pan and zoom plus workspace
and view interactions.
- **`ContextMenu`/`MenuItem`** — every diagram tab's right-click "Copy as
SysML" action (the app's first `ContextMenu` usage; see
`docs/design/ots/dock.md`'s "Diagram Tab 'Copy as SysML' Context Menu"
section).
- **`TopLevel.Clipboard`** — writes the generated SysML snippet to the OS
clipboard, resolved from the diagram view's own control via
`TopLevel.GetTopLevel(control)`; the same API the custom view builder's own
"Copy as SysML" button already used.

### Integration Pattern

Expand Down
57 changes: 57 additions & 0 deletions docs/design/ots/dock.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,60 @@ with no restore path - closing a diagram tab is always a safe operation
(zero tabs open is a supported, first-class state), and reopening one is one
click away via the catalog or the "+ New Diagram Tab" button, so no
restore mechanism is needed.

### Diagram Tab "Copy as SysML" Context Menu

Every `DiagramDocumentView` - one per open `WorkbenchTab`, and therefore
covering all six predefined view kinds (General, Interconnection, State
Transition, Action Flow, Sequence, Grid) plus a custom-view-builder preview
tab, since all of them are rendered through this one shared view/view-model
pair - hosts a single shared `ContextMenu` on its diagram `Border`, with one
`MenuItem` ("Copy as SysML"). This is the app's first `ContextMenu` usage.

Clicking the item copies that tab's whole-diagram SysML `view { ... }` text
(view kind, every `expose` clause, and any `filter` expression) to the OS
clipboard, reusing the existing `SysmlSnippetGenerator` - the same unit the
custom view builder's own "Copy as SysML" button already used - so there is
exactly one snippet-generation code path regardless of which tab kind
triggered it. `MainWindowShell.WorkbenchTab.SourceDefinition` (a
`ViewDefinitionModel?`) carries the concrete definition each tab's diagram
was rendered from: for a custom-view-preview tab it is the definition passed
to `PreviewCustomView`; for a predefined-view tab it is derived by
`ViewCatalogPresenter.BuildViewDefinition`, since a predefined view's
`SysmlViewNode` in the loaded workspace carries the same kind/expose/filter
shape but had previously only been surfaced as a `ViewDescriptor` (display
metadata, not a definition).

The menu item's `IsEnabled` is bound to `DiagramDocumentViewModel.CanCopyAsSysml`,
which mirrors `MainWindowShell.CanExportTabAsSysml(TabId)` - `false` (and so
the item is disabled, never crashes) when a tab has no derivable definition,
which happens in two disclosed cases: a brand-new custom-preview tab that
has not rendered anything yet, and an unscoped predefined view (zero
`expose` members - a real, valid "expose everything" SysML v2 view with no
finite expose list for `SysmlSnippetGenerator` to serialize). Both cases are
logged at `Info` level via the existing `RollingFileLogger` rather than
thrown, consistent with every other expected-but-unavailable shell state.

Clicking the item invokes `DiagramDocumentViewModel.CopyAsSysmlAsync()`,
which asks `MainWindowShell.ExportTabAsSysmlSnippet` for the snippet text and
writes it via an injected `IClipboardService` - a small seam (mirroring the
existing `IUiDispatcher` seam pattern) so the generate-and-copy
orchestration can be unit tested with a fake clipboard rather than needing a
live UI. `DiagramDocumentView`'s code-behind constructs the real
`AvaloniaClipboardService` (wrapping `TopLevel.GetTopLevel(this)?.Clipboard`)
once it has a view model, and assigns it to the view model's
`ClipboardService` property; production code never touches
`TopLevel.Clipboard` directly from the view model, keeping the view model
UI-framework-agnostic aside from that one injected seam.

Adding this `ContextMenu` exposed a pre-existing pan-gesture bug: the same
`Border`'s pointer handlers previously started a pan on *any* pointer button,
including the right button. Since opening a `ContextMenu` on right-button
release consumes that release before it reaches the `Border`'s own
`PointerReleased` handler, panning was left permanently "stuck on" after
every right-click - the very next pointer move (even a plain mouse move with
no button held) dragged the diagram. `DiagramDocumentView` now only starts a
pan on `PointerPressed` when `PointerPoint.Properties.IsLeftButtonPressed` is
true, so the right button is reserved exclusively for the context menu, and
a `PointerCaptureLost` handler resets the panning flag defensively in case
capture is ever stolen mid-drag for some other reason.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ own focus-change signal via `NotifyActiveDiagramTab`.
WorkspacePanel to build its tree without owning its own `WorkspaceSourceSet`
instance.

Each `WorkbenchTab` also carries a `SourceDefinition: ViewDefinitionModel?` —
the definition that produced the tab's currently rendered diagram, or `null`
when none could be derived (an unscoped predefined view with zero expose
members, or a brand-new custom-preview tab that has not rendered anything
yet). This backs the tab's "Copy as SysML" context-menu action.

#### Key Methods

**AddFileSource**: Adds a single file as a new workspace source.
Expand Down Expand Up @@ -129,6 +135,26 @@ UI focus.
when Dock reports a focus change onto a diagram document - never by
`MainWindowShell` itself, which stays Dock-agnostic.

**CanExportTabAsSysml**: Reports whether an open diagram tab has a derivable
source definition and can export its diagram as a SysML snippet.

- *Parameters*: `string tabId` — identifier of an open tab.
- *Returns*: `bool` — `true` when the tab exists and its `SourceDefinition` is
ready to export.
- *Postconditions*: None (read-only). Backs the enabled/disabled state of
every diagram tab's "Copy as SysML" context-menu item.

**ExportTabAsSysmlSnippet**: Generates copy-pasteable SysML `view` text for
the diagram currently rendered in an open tab.

- *Parameters*: `string tabId` — identifier of an open tab.
- *Returns*: `string?` — the SysML snippet, or `null` when
`CanExportTabAsSysml` would report `false` for that tab.
- *Postconditions*: A `null` result is logged at `Info` level with the reason
(an expected, valid outcome, not a failure) rather than thrown; a
successful export is also logged at `Info` level, mirroring
`ExportCustomViewSnippet`'s existing style.

#### Error Handling

MainWindowShell handles user-cancelled operations, empty workspace selections,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,23 @@ a named snippet.
only four possible recursion kinds, at most four selections can ever exist
for one qualified name; once all four are taken, this remains a safe no-op.

**AddExposeTarget (selection overload)**: Adds a fully-specified
`ExposeTargetSelection` - qualified name, recursion kind, and optional
bracket-filter expression - directly, in a single call.

- *Parameters*: `ExposeTargetSelection selection` — fully-specified target
selection.
- *Returns*: `void` — state is updated in place.
- *Preconditions*: `selection` is not null.
- *Postconditions*: Deduplicates by the exact (`QualifiedName`,
`RecursionKind`) pair, mirroring the string overload's own dedupe contract:
a matching existing selection is preserved (including its own bracket
filter) rather than overwritten. Lets a caller (for example
`ViewCatalogPresenter.BuildViewDefinition`) reconstruct an arbitrary real
`ExposeMember` - any of the four recursion kinds plus an optional bracket
filter - without the multi-call add-then-retarget-then-filter sequence the
string overload would otherwise require.

**RemoveExposeTarget**: Removes a qualified name/recursion kind pair from the
`expose` target set.

Expand Down Expand Up @@ -131,3 +148,4 @@ the two recursive expose forms.
- **MainWindowShell**
- **SysmlSnippetGenerator**
- **LayoutInvoker**
- **ViewCatalogPresenter**
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,27 @@ underlying workspace snapshot has changed and the catalog must be rebuilt.
- *Postconditions*: The returned descriptor matches the current catalog
revision.

**BuildViewDefinition**: Derives a `ViewDefinitionModel` that faithfully
reconstructs a predefined view's real `view` declaration from the loaded
workspace.

- *Parameters*: `SemanticWorkspace workspace` — loaded model content;
`string viewId` — qualified name of the predefined view, as published in
`AvailableViews`.
- *Returns*: `ViewDefinitionModel?` — a populated definition (view kind, every
expose member with its own recursion kind and optional bracket-filter
expression, filter expression, display name), or `null` when `viewId` is
not in the current catalog, does not resolve to a view node, its render
target does not map to a supported `ViewKind`, or it declares zero expose
members.
- *Preconditions*: `RefreshCatalog` has been called against `workspace`.
- *Postconditions*: The zero-expose-members case ("expose everything, no
scoping" - valid SysML v2) intentionally yields `null` rather than an
empty-but-technically-valid definition, since `SysmlSnippetGenerator` has
no finite expose list to serialize for it. Used by `MainWindowShell` to
populate a predefined-view diagram tab's `SourceDefinition` for its
"Copy as SysML" context-menu action.

#### Error Handling

ViewCatalogPresenter handles invalid or disappearing selections locally by
Expand All @@ -63,6 +84,9 @@ resolved before exposing the catalog.
- **WorkspaceModel** — provides the semantic workspace snapshot used for
discovery.
- **LayoutInvoker** — consumes the selected descriptor to generate a layout.
- **ViewDefinitionModel** — populated by `BuildViewDefinition` for consumers
that need a concrete, reusable view definition rather than just a
`ViewDescriptor`.
- **SysML2Tools** — supplies the view usage concepts and semantic model types.
- **AppShellSubsystem** — hosts the UI surface that binds the catalog output.

Expand Down
7 changes: 7 additions & 0 deletions docs/reqstream/ots/avalonia.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,10 @@ sections:
The workbench depends on Avalonia to compose the major UI regions around the rendered SVG viewer.
tests:
- 'MainWindow_HostsDiagramAndDiagnosticsPanels'

- id: 'Avalonia-CopyDiagramAsSysmlViaContextMenu'
title: 'The SysML2Workbench shall use Avalonia''s ContextMenu and TopLevel.Clipboard APIs to let a user right-click any open diagram tab and copy that diagram''s SysML view definition to the OS clipboard.'
justification: |
Every rendered diagram tab needs a discoverable, in-place way to copy its underlying SysML view text without navigating away to the custom view builder panel, and Avalonia's real ContextMenu/Clipboard integration is the genuine end-to-end behavior being qualified rather than a mocked UI harness.
tests:
- 'DiagramContextMenu_CopyAsSysml_CopiesSnippetToClipboard'
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,14 @@ sections:
- 'Construction_EstablishesValidEmptySnapshot'
- 'RemoveSourceAsync_DownToZeroSources_ProducesEmptySnapshotAndUnwatchesEverything'
- 'SelectPredefinedView_NoWorkspaceOpened_ThrowsInvalidOperationException'

- id: 'SysML2Workbench-AppShellSubsystem-MainWindowShell-ExportTabAsSysml'
title: 'The MainWindowShell shall derive and export a SysML view snippet for any open diagram tab whose originating view definition can be determined, and shall report that a tab has no derivable definition rather than throwing.'
justification: |
Every diagram tab - predefined view or custom-view-builder preview - supports a "Copy as SysML" context-menu action that reuses the existing SysmlSnippetGenerator, and a tab whose definition cannot be derived (for example an unscoped predefined view with zero expose members, or an unrendered custom-preview tab) must disable that action gracefully instead of crashing.
tests:
- 'ExportTabAsSysmlSnippet_PredefinedViewTab_ReturnsSnippet'
- 'ExportTabAsSysmlSnippet_CustomPreviewTab_ReturnsSnippet'
- 'ExportTabAsSysmlSnippet_UnknownTabId_ReturnsNull'
- 'ExportTabAsSysmlSnippet_PredefinedViewWithNoExposeMembers_ReturnsNull'
- 'CanExportTabAsSysml_MirrorsExportability'
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,11 @@ sections:
The shell and snippet generator need to know when a user's current selections form a usable custom view definition.
tests:
- 'DefinitionState_ReportsRenderAndExportReadiness'

- id: 'SysML2Workbench-ViewBuilderSubsystem-ViewDefinitionModel-AddExposeTargetSelectionDirectly'
title: 'The ViewDefinitionModel shall accept a fully-specified ExposeTargetSelection (qualified name, recursion kind, and optional bracket-filter expression) in a single call, deduplicated by the exact (qualified name, recursion kind) pair.'
justification: |
Reconstructing an arbitrary real expose member - any of the four recursion kinds plus an optional bracket filter - from a workspace's predefined view requires a single-call add that does not risk colliding with an already-present sibling selection, unlike the string overload's fixed default recursion kind plus a separate retarget/filter call sequence.
tests:
- 'AddExposeTarget_SelectionOverload_AppendsAndDedupesByQualifiedNameAndRecursionKind'
- 'AddExposeTarget_SelectionOverload_NullSelection_ThrowsArgumentNullException'
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,12 @@ sections:
Rendering begins from a single active catalog selection, so the presenter must expose that selection change.
tests:
- 'SelectView_PublishesCurrentSelection'

- id: 'SysML2Workbench-ViewCatalogSubsystem-ViewCatalogPresenter-BuildViewDefinitionFromWorkspaceView'
title: 'The ViewCatalogPresenter shall derive a ViewDefinitionModel that faithfully reconstructs a predefined view''s view kind, every expose member (recursion kind and bracket filter included), filter expression, and display name from the loaded workspace, and shall return null when no finite expose list can be determined.'
justification: |
Predefined-view diagram tabs need a concrete, reusable view definition - not just a display descriptor - so the shared "Copy as SysML" export path can generate a snippet for them the same way it does for custom-view-builder previews.
tests:
- 'BuildViewDefinition_ResolvesKindExposeAndFilter'
- 'BuildViewDefinition_UnknownViewId_ReturnsNull'
- 'BuildViewDefinition_NoExposeMembers_ReturnsNull'
10 changes: 10 additions & 0 deletions docs/user_guide/getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ The rendered diagram appears as an interactive SVG in its tab's canvas:
Each diagram tab has its own independent pan/zoom state, so switching
between tabs never disturbs another tab's view of its diagram.

Every open diagram tab - whether it renders a predefined view or a custom
view preview - supports right-click > **Copy as SysML** to copy that tab's
`view { ... }` SysML v2 text to the clipboard. The option is disabled when
no concrete view definition can be derived for the tab, for example an
unscoped predefined view or a custom-view preview tab that has not yet been
rendered.

## Building a Custom View

The **Custom View Builder** panel (right side, dockable) lets you construct an
Expand Down Expand Up @@ -113,6 +120,9 @@ ad-hoc view without writing SysML syntax:
regardless of whether a custom view name was supplied. Paste this into a
`.sysml` file to promote the
ephemeral preview into a permanent, version-controlled view definition.
The same right-click **Copy as SysML** context menu described in
"Browsing Predefined Views" is also available on this preview's diagram
tab, not just the panel's own button.

Custom views built in the GUI are **session-only** - they are not saved to
disk by SysML2Workbench itself. The "Copy as SysML" snippet is the only way
Expand Down
2 changes: 2 additions & 0 deletions docs/verification/ots/avalonia.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ OTS integration tests in `test/OtsSoftwareTests/AvaloniaTests.cs` verify the rea
**Startup_HostsDesktopShellControls**: A real `MainWindowView` is constructed over a fully composed `MainWindowShell` and shown under the headless platform. The dependency proves it hosts the menu, predefined-views list, and custom-view builder controls (ComboBox, multi-select ListBox, buttons) as real, discoverable Avalonia controls attached to the window's visual tree.

**MainWindow_HostsDiagramAndDiagnosticsPanels**: The same window's diagram `Image` control and diagnostics `ListBox` are confirmed present before any workspace is opened; after opening a workspace and selecting a predefined view through the shell, the shared canvas state backing the diagram control reports loaded content, proving Avalonia genuinely hosts the interactive diagram surface alongside the diagnostics panel.

**DiagramContextMenu_CopyAsSysml_CopiesSnippetToClipboard**: After opening a workspace and rendering a predefined view, the real diagram tab's `ContextMenu` is opened on its `Border`, and the "Copy as SysML" `MenuItem` is clicked, both discovered as genuine Avalonia controls in the visual tree rather than invoked programmatically. The headless platform's own `TopLevel.Clipboard` is then read back via `TryGetTextAsync`, and its text is asserted to match the snippet `MainWindowShell.ExportTabAsSysmlSnippet` independently generates for the same tab - proving the whole click-to-clipboard path (context menu, view-model command, `IClipboardService` seam, and the real Avalonia clipboard) works end-to-end through the actual framework, not a mock.
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,23 @@ source's file watcher. Verified by
**SelectPredefinedView_NoWorkspaceOpened_ThrowsInvalidOperationException**: Selecting a predefined view while zero
workspace sources are open throws `InvalidOperationException` rather than rendering against an empty workspace.
Verified by `MainWindowShellTests.SelectPredefinedView_NoWorkspaceOpened_ThrowsInvalidOperationException`.

**ExportTabAsSysmlSnippet_PredefinedViewTab_ReturnsSnippet**: A rendered predefined-view diagram tab exports a
SysML snippet reflecting its derived view definition, and `CanExportTabAsSysml` reports it as exportable. Verified
by `MainWindowShellTests.ExportTabAsSysmlSnippet_PredefinedViewTab_ReturnsSnippet`.

**ExportTabAsSysmlSnippet_CustomPreviewTab_ReturnsSnippet**: A rendered custom-view-preview diagram tab exports a
SysML snippet reflecting the definition it was previewed from. Verified by
`MainWindowShellTests.ExportTabAsSysmlSnippet_CustomPreviewTab_ReturnsSnippet`.

**ExportTabAsSysmlSnippet_UnknownTabId_ReturnsNull**: An unknown tab id is reported as not exportable and returns
`null` rather than throwing. Verified by `MainWindowShellTests.ExportTabAsSysmlSnippet_UnknownTabId_ReturnsNull`.

**ExportTabAsSysmlSnippet_PredefinedViewWithNoExposeMembers_ReturnsNull**: A predefined view with zero expose
members (a valid, unscoped "expose everything" view) cannot be exported, since there is no finite expose list to
serialize - it is reported gracefully rather than throwing. Verified by
`MainWindowShellTests.ExportTabAsSysmlSnippet_PredefinedViewWithNoExposeMembers_ReturnsNull`.

**CanExportTabAsSysml_MirrorsExportability**: `CanExportTabAsSysml` mirrors the same true/false outcomes as
`ExportTabAsSysmlSnippet` across the exportable and unknown-tab cases. Verified by
`MainWindowShellTests.CanExportTabAsSysml_MirrorsExportability`.
Loading
Loading