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
1 change: 1 addition & 0 deletions .cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ words:
- WebDriver
- webdriver
- novawindows
- winapi
- atspi
- automatable
- startable
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -391,9 +391,13 @@ jobs:
node-version: 24.x

- name: Install Appium and the NovaWindows driver
# appium-novawindows-driver is pinned: 1.4.2 reintroduced an inverted return value in
# trySetForegroundWindow, so every session fails with "Failed to locate window of the app."
# 1.4.1 is the last good release. Keep this in step with build.ps1's pin. See
# docs/design/ots/appium.md; remove the pin once a fixed release ships.
run: |
npm install -g appium
appium driver install --source=npm appium-novawindows-driver
appium driver install --source=npm appium-novawindows-driver@1.4.1

# === BUILD AND PUBLISH THE DESKTOP APPLICATION ===
# This section builds the solution and publishes the self-contained Desktop executable that the
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ exported as copy-pasteable SysML `view ... expose ...` text to promote them
into a permanent model file, and any workspace `.sysml` file's raw source
text can be opened read-only, with syntax highlighting, by double-clicking
it in the workspace tree. A live-updating diagnostics panel surfaces parser
and reference-resolution problems across the whole workspace, and a local
rolling log file is written for bug-report attachments.
and reference-resolution problems across the whole workspace, a status bar
along the bottom of the window summarizes whichever document tab is active,
and a local rolling log file is written for bug-report attachments.

This is a **Phase 0, read-only** release: there is no git integration, no
text/structural editing, no telemetry, and custom views are session-only until
Expand Down
26 changes: 25 additions & 1 deletion build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,32 @@ if ($IntegrationTest) {
npm install -g appium
if ($LASTEXITCODE -ne 0) { $buildError = $true }

$driverName = $IsWindows ? "appium-novawindows-driver" : "mac2"
# appium-novawindows-driver is pinned: 1.4.2 reintroduced an inverted return value in
# trySetForegroundWindow, so every session fails with "Failed to locate window of the
# app." 1.4.1 is the last good release. See docs/design/ots/appium.md for the analysis;
# remove the pin once a fixed release ships.
$novaWindowsDriverVersion = "1.4.1"

$driverName = $IsWindows ? "appium-novawindows-driver@$novaWindowsDriverVersion" : "mac2"
$driverArgs = $IsWindows ? @("--source=npm", $driverName) : @($driverName)

# A driver already installed at a different version makes `driver install` fail with
# "already installed", which the check below deliberately tolerates. Uninstall a
# mismatched version first so that tolerance can never silently leave the pin unapplied.
if ($IsWindows) {
$installedVersion = $null
try {
$installedVersion = (appium driver list --installed --json 2>$null | ConvertFrom-Json).novawindows.version
} catch {
$installedVersion = $null
}

if ($installedVersion -and $installedVersion -ne $novaWindowsDriverVersion) {
Write-Host "Replacing the novawindows driver $installedVersion with the pinned $novaWindowsDriverVersion..."
appium driver uninstall novawindows
}
}

Write-Host "Installing the $driverName driver..."
$driverInstallOutput = appium driver install @driverArgs 2>&1 | Out-String
Write-Host $driverInstallOutput
Expand Down
24 changes: 24 additions & 0 deletions docs/design/ots/appium.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,27 @@ and fails fast with an actionable error instead of letting all tests fail
slowly, one at a time, with this cryptic stack trace. The remediation is
simply to **unlock the interactive console/RDP session** and re-run
`-IntegrationTest`; no code change can substitute for this.

The same error has a second, entirely different cause: a regression in
**`appium-novawindows-driver` 1.4.2**. Its `trySetForegroundWindow`
(`lib/winapi/user32.ts`) returns the result of the Win32 `EnumWindows` call
that locates the window, but `EnumWindows` returns FALSE precisely when its
callback halts enumeration - i.e. when the window *was* found and
foregrounded. The value is therefore inverted, so `attachToApplicationWindow`
takes its `focusElement` fallback branch on every successful launch;
`focusElement` issues UI Automation `SetFocus()` against a window that already
holds the foreground, that throws, and the launch loop swallows the error and
retries 20 times before reporting `Failed to locate window of the app.` The
inverted form is pre-1.4.1 code that 1.4.1's stability refactor incidentally
corrected and 1.4.2's revert of that refactor reintroduced.

Because that failure is indistinguishable from the locked-session one at the
top-level error message, and because an unpinned `appium driver install`
silently picks up new releases, **both `build.ps1 -IntegrationTest` and
`.github/workflows/build.yaml` pin the driver to 1.4.1** - the last good
release. `build.ps1` additionally uninstalls a mismatched version before
installing, because `appium driver install` refuses to overwrite an existing
driver and both scripts deliberately tolerate its "already installed" error;
without that uninstall, a developer's stale driver would silently defeat the
pin and let local results diverge from CI. Remove both pins once a fixed
release ships.
18 changes: 15 additions & 3 deletions docs/design/ots/dock.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,24 @@ the ones the Tool panels use:
- **`IFactory.FocusedDockableChanged`/`OnDockableClosed`** — `MainWindowView`
subscribes to `WorkbenchDockFactory`'s inherited `FocusedDockableChanged`
event to forward Dock's own tab-focus tracking to
`MainWindowShell.NotifyActiveDiagramTab` whenever focus lands on a diagram
document (focus changes onto a Tool panel are ignored), and
`MainWindowShell.NotifyActiveDocumentTab` whenever focus lands on a
document, whether a diagram or a read-only source-text document (focus
changes onto a Tool panel are ignored), and
`WorkbenchDockFactory` overrides `OnDockableClosed` to raise a
`DiagramTabClosed` event when a diagram document closes through Dock's own
chrome, which `MainWindowView` uses to call
`MainWindowShell.CloseDiagramTab`.
`MainWindowShell.CloseDiagramTab`. Selecting a document tab drives Dock's
internal `TryActivateDockable`, which activates the dockable within its own
dock and *then* focuses it from the focusable root; the activation step
already raises the focus change synchronously (via `InitActiveDockable`), so
subscribing to `FocusedDockableChanged` alone tracks the selected tab.
Headless tests therefore reproduce a tab click by calling the factory's
`SetActiveDockable` followed by `SetFocusedDockable`, which is the same
activate-then-focus pair Dock's own tab-header click handler issues.
Calling `SetFocusedDockable` alone is not sufficient: it returns without
raising anything when the focusable root's focused dockable already matches
the requested one, so a test that only focuses cannot observe a transition
it did not first activate.

`Document`s and `Tool`s therefore have deliberately different close
semantics in this codebase: a closed `Tool` is hidden and restorable via the
Expand Down
18 changes: 17 additions & 1 deletion docs/design/sysml2-workbench/app-shell-subsystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,12 @@ environment.
state. The three Tool panels (workspace, predefined views, diagnostics)
are closable and restorable via a View menu, which reuses the same
long-lived panel view model instance so its in-progress state survives
the close/restore cycle. Custom-view composition is not one of these
the close/restore cycle. The workspace and predefined-views panels share
the left column, with Workspace as its leading tab and the one shown by
default at startup (matching the order the View menu lists them in), so a
freshly launched application presents the workspace tree first - the
panel a user needs before any view can be rendered. Custom-view
composition is not one of these
docked panels: it is `ViewBuilderDialog`, a modal `Window` shown via
`ShowDialog` (like `AboutDialog`), opened from the View menu and
discarded on close rather than living in the persistent Dock layout. The
Expand All @@ -86,3 +91,14 @@ environment.
diagnostics panel in particular distinguishes "no diagnostics because the
workspace is empty" from "no diagnostics because everything is clean,"
since those are different facts the user needs to tell apart.
8. A status area runs along the bottom edge of the main window, below every
docked panel, summarizing whatever document tab is currently active: the
file name and full path for a source-text tab, the view kind plus the
entities being viewed (elided to the first few with a trailing "+N more"
count) for a diagram tab, and a neutral idle message when no tab is open.
The summary text itself is computed by MainWindowShell
(`GetActiveTabStatusSummary`), so the logic stays Avalonia-free and
unit-testable and the UI layer only renders the resulting string,
refreshing it whenever the open-tab set or the focused document changes.
Because the shell is the only owner of that text, the status area never
duplicates tab-presentation rules the shell already applies.
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ content. Each `WorkbenchTab` owns its own `SvgCanvasHost` (unused/never
loaded for a source-text tab), so multiple open diagram tabs have fully
independent zoom/pan/content state.

**ActiveTabId**: `string?` — identifier of the diagram tab currently
active/focused, or `null` when no diagram tab is open. Updated both by shell
**ActiveTabId**: `string?` — identifier of the document tab currently
active/focused, or `null` when no document tab is open. Updated both by shell
operations that open or focus a tab, and by the UI layer forwarding Dock's
own focus-change signal via `NotifyActiveDiagramTab`.
own focus-change signal via `NotifyActiveDocumentTab`.

**CurrentSourceIdToFiles**: `IReadOnlyDictionary<string, IReadOnlyList<string>>`
— per-source file lists from the most recent workspace resolution, used by
Expand Down Expand Up @@ -150,10 +150,10 @@ and makes it active, without rendering anything into it.
tab, a neighboring tab becomes active, or `ActiveTabId` becomes `null` if no
tabs remain.

**NotifyActiveDiagramTab**: Notifies the shell that a diagram tab has gained
UI focus.
**NotifyActiveDocumentTab**: Notifies the shell that a document tab — a
rendered diagram or a read-only source-text document — has gained UI focus.

- *Parameters*: `string? tabId` — identifier of the newly focused diagram
- *Parameters*: `string? tabId` — identifier of the newly focused document
tab.
- *Returns*: `void` — `ActiveTabId` updates in place.
- *Postconditions*: `ActiveTabId` is updated, unless `tabId` does not refer to
Expand Down Expand Up @@ -224,6 +224,29 @@ the diagram currently rendered in an open tab.
successful export is also logged at `Info` level, mirroring
`ExportCustomViewSnippet`'s existing style.

**GetActiveTabStatusSummary**: Builds a short, single-line summary of
whatever the currently active tab presents, for the main window's bottom
status area.

- *Parameters*: none.
- *Returns*: `string` — never `null` or empty.
- *Preconditions*: None; every shell state, including "no tab open", is
valid input.
- *Postconditions*: None (read-only; no file is read and nothing is
re-rendered, so it is cheap enough to call on every tab or focus change).
The result is `Ready` when no tab is active; `{fileName} — {fullPath}` for
a `SourceText` tab (falling back to the tab's `Title` when it carries no
file path); and `{View Kind} view — {Entity1}, {Entity2}, {Entity3}
(+N more)` for a diagram tab, where the view kind is the definition's
friendly space-separated label (for example *State Transition*), each
entity is the final `::` segment of an `ExposeTargetSelection`'s qualified
name, at most three are listed and the remainder is elided with a trailing
count, and a definition exposing nothing reports
`(no exposed entities)`. A diagram tab whose `SourceDefinition` is `null`
falls back to the tab's `Title`. Returning a plain `string` computed from
already-resident shell state keeps this class free of any UI dependency -
the Avalonia layer only renders the result.

#### Supporting Types

**SourceTextDocumentViewModel**/**SourceTextDocumentView**: the Dock
Expand Down
2 changes: 2 additions & 0 deletions docs/reqstream/sysml2-workbench.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,7 @@ sections:
children:
- 'SysML2Workbench-LoggingSubsystem-PersistOperationalEvents'
- 'SysML2Workbench-AppShellSubsystem-CoordinateSessionWorkspace'
- 'SysML2Workbench-AppShellSubsystem-DefaultSidebarPanel'
- 'SysML2Workbench-AppShellSubsystem-ShowActiveDocumentStatus'
tests:
- 'StartSession_OpensShellAndWritesOperationalLogs'
21 changes: 21 additions & 0 deletions docs/reqstream/sysml2-workbench/app-shell-subsystem.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,24 @@ sections:
- 'SysML2Workbench-AppShellSubsystem-ViewBuilderDialog-CancelDiscardsWithoutSideEffects'
tests:
- 'CustomViewWorkflow_PreviewsAndExportsFromShell'

- id: 'SysML2Workbench-AppShellSubsystem-DefaultSidebarPanel'
title: 'The AppShellSubsystem shall present the workspace panel as the sidebar panel shown by default when the application starts, ahead of the predefined-views panel in the same sidebar.'
justification: |
The workspace tree is the first thing a user needs at startup - sources must be added before any view can be
rendered - so it is the sidebar panel shown by default, matching the order the View menu already lists the
panels in. This behavior is realized by the subsystem's UI composition layer rather than by a documented
unit, so it is verified at subsystem level.
tests:
- 'Startup_ShowsWorkspacePanelAsDefaultSidebarTab'

- id: 'SysML2Workbench-AppShellSubsystem-ShowActiveDocumentStatus'
title: 'The AppShellSubsystem shall display a status area along the bottom of the main window summarizing the currently active document.'
justification: |
Users need to tell at a glance which file or view the front tab presents - the tab label alone is truncated and
omits the file path and the entities a diagram exposes - and need a neutral idle indication when no document
is open at all.
children:
- 'SysML2Workbench-AppShellSubsystem-MainWindowShell-SummarizeActiveTab'
tests:
- 'ActiveTabChanges_ProduceStatusSummary'
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ sections:
- 'PreviewCustomView_WithNoTabsOpen_OpensNewTab'
- 'OpenNewCustomPreviewTab_OpensEmptyActiveTab_AndSubsequentPreviewUpdatesIt'
- 'CloseDiagramTab_RemovesTab_AndReassignsActiveTab'
- 'NotifyActiveDiagramTab_UnknownId_IsIgnored'
- 'NotifyActiveDocumentTab_UnknownId_IsIgnored'
- 'SelectPredefinedView_TabsHaveIndependentCanvases'

- id: 'SysML2Workbench-AppShellSubsystem-MainWindowShell-SynchronizeSessionState'
Expand Down Expand Up @@ -85,3 +85,18 @@ sections:
- 'OpenSourceTextTab_NewFile_CreatesOneNewActiveTab'
- 'OpenSourceTextTab_SamePathTwice_RefocusesExistingTabWithoutDuplicating'
- 'GetTabFilePath_ReturnsPathForSourceTextTab_AndNullOtherwise'

- id: 'SysML2Workbench-AppShellSubsystem-MainWindowShell-SummarizeActiveTab'
title: 'The MainWindowShell shall produce a short single-line summary of the currently active tab - the file name and full path for a source-text tab, the view kind and exposed entities for a diagram tab, and a neutral idle message when no tab is open.'
justification: |
The main window's bottom status area needs the summary text computed in Avalonia-free, unit-testable shell
code so the UI layer only renders it; the summary must stay single-line for any number of exposed entities
and must degrade gracefully for a diagram tab whose view definition could not be derived.
tests:
- 'GetActiveTabStatusSummary_NoTabOpen_ReturnsIdleSummary'
- 'GetActiveTabStatusSummary_SourceTextTab_ReturnsFileNameAndPath'
- 'GetActiveTabStatusSummary_PredefinedViewTab_ReturnsViewKindAndExposedEntities'
- 'GetActiveTabStatusSummary_CustomPreviewTab_ReturnsViewKindAndExposedEntities'
- 'GetActiveTabStatusSummary_ManyExposedEntities_ElidesWithMoreCount'
- 'GetActiveTabStatusSummary_DiagramTabWithoutDefinition_FallsBackToTabTitle'
- 'GetActiveTabStatusSummary_UnresolvedExposeTarget_ReportsNoExposedEntities'
22 changes: 21 additions & 1 deletion docs/user_guide/getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ for setup prerequisites.
added source as a tree - folders expand to show the files discovered
under them, files appear as non-expandable entries - and lets you add
more sources or remove one via its toolbar or a right-click/selection
plus **Remove**. The **Predefined Views** list is populated with every
plus **Remove**. **Workspace** is the sidebar tab shown by default when
the application starts, with **Predefined Views** as the second tab of
the same panel - click either tab to switch between them. The
**Predefined Views** list is populated with every
view usage declared in the workspace, and the **Diagnostics** panel
(bottom) lists any parser or reference-resolution problems found across
the whole workspace. Panels are docked in this default arrangement but can
Expand Down Expand Up @@ -195,6 +198,23 @@ found anywhere in the currently loaded workspace, refreshed automatically on
every reload (initial open or external file change). Each entry shows the
affected file, location, severity, and message.

## Status Bar

A status bar runs along the bottom edge of the window, below every docked
panel, showing a short summary of whichever document tab is currently
active:

- A **source-text tab** shows the file's name followed by its full path.
- A **diagram tab** - predefined view or custom-view preview - shows the
view kind followed by the entities being viewed, for example
*Interconnection view — Vehicle, Engine*. Only the first few entities are
listed; the rest are summarized as a trailing *(+N more)* count so the
status bar stays on one line.
- With **no tab open** at all, the status bar shows *Ready*.

The summary updates automatically as you open, close, or switch between
tabs.

## Local Log File

SysML2Workbench writes a local rolling log file recording notable operations
Expand Down
2 changes: 1 addition & 1 deletion docs/verification/ots/avalonia.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ OTS integration tests in `test/OtsSoftwareTests/AvaloniaTests.cs` verify the rea

### Test Scenarios

**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 and predefined-views list as real, discoverable Avalonia controls attached to the window's visual tree.
**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 and the workspace tree - the sidebar's leading, initially-active tab - 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.

Expand Down
Loading
Loading