UI/Console/HDTConsole.xaml

<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Hephaestus Deployment Toolkit"
    Height="900" Width="1800"
    MinHeight="520" MinWidth="900"
    WindowStartupLocation="Manual"
    Background="{DynamicResource HDTWindowBrush}">
 
    <!--
        C1 of the WPF-first direction: the admin console, showing what is on a
        deployment share.
 
        NO x:Class AND NO CODE-BEHIND, exactly as HDTWizard.xaml. The console
        runs on a desktop where a compiler exists, so this one is a choice rather
        than a constraint - and the choice is that a page written this way can
        move into the WinPE wizard later without being rewritten, where
        XamlReader::Load is the only option there is. Handlers are attached in
        PowerShell, by name, after the tree is loaded.
 
        WindowStartupLocation IS Manual BECAUSE THE POSITION IS DECIDED, NOT
        GUESSED. This window opens at the top-left of the work area, filling it,
        which is where an administrator's other management tooling sits. Manual
        is the only value under which WPF honours the Left and Top the host
        assigns - CenterScreen recentres after the assignment and throws them
        away, and the two would happen to agree on this desk only because a
        work-area-sized window centres back onto the corner it started at.
 
        THE CORNER IS THE WORK AREA'S, NOT A LITERAL 0,0. A taskbar docked at the
        top or the left moves the origin, and a window pinned to zero would open
        underneath it. Resolve-HDTConsoleWindowPosition measures that; the four
        numbers above are only what a window loaded on its own falls back to.
 
        NAMED CONTROLS ARE THE CONTRACT between this file and New-HDTConsoleHost:
        HDTShareText, HDTDeployRootText, HDTRootText, HDTConsoleTree,
        HDTDetailText, HDTCommandText and HDTCloseButton are found with FindName,
        and tests/unit/ConsoleWindow.Tests.ps1 asserts every one of those names is
        present, so renaming one here fails on a developer machine rather than in
        front of an administrator.
 
        EVERY COLOUR IS A DynamicResource, AND THE LIGHT PALETTE IS DECLARED
        BELOW. The values here are the defaults, so this file renders correctly
        on its own; New-HDTConsoleHost replaces the same keys with whatever
        Get-HDTConsoleTheme returned, which is how one window file serves both
        the light and dark palettes without a second copy to keep in step. The
        keys are asserted against that command, so a colour added to one and not
        the other fails on a developer machine.
 
        THE TREE EXPANDS AND COLLAPSES, AND IT IS STILL THE HOST THAT DECIDES
        NOTHING. A HierarchicalDataTemplate binds Children, so WPF builds the
        nesting itself from rows Get-HDTConsoleTreeNode already linked together;
        the host assigns ItemsSource and attaches one handler. Icon, Text and
        IsExpanded are read off the row, which means what a task sequence looks
        like and which branches start open are decisions made in a command and
        asserted in tests/unit/ConsoleTreeNode.Tests.ps1 - not in this file, and
        not in the adapter.
 
        THE DETAIL PANE IS MONOSPACED AND THE TREE IS NOT. The pane shows
        64-character SHA-256 hashes, which are read by comparing columns; the
        tree shows names, which are read as words.
 
        THE TREE NEVER SCROLLS SIDEWAYS. WPF brings a newly selected TreeViewItem
        into view horizontally as well as vertically, so clicking a row with a
        long name drags the whole tree left and takes the icons and the
        indentation off the edge of the panel with it - the tree stops looking
        like a tree, and the thing an administrator just clicked is the only row
        still readable. Horizontal scrolling is therefore off and long names end
        in an ellipsis. Nothing is lost: the full name is the second line of the
        detail pane, which is where Workbench puts it too.
 
        THE ICONS ARE CHARACTERS. A .png is another file to ship, find at
        runtime and get right in two themes. These come out of Windows' own
        fonts, scale with the row, and survive being copied out as text.
 
        THE DEFAULT SIZE IS SIZED AGAINST THE BOOT IMAGE, which is the longest
        pane in the console - seventeen fields, two of them 64-character hashes.
        Measured rather than guessed: at 720 the pane was 146 units short, and it
        wants 866 to show everything at once, so this opens at 900 with room to
        spare, inside the 1104-unit work area of the machine it was measured on.
        1800 wide is what puts a full SHA-256 on ONE line beside its caption. The
        default must never be a shape that wraps a hash or hides one behind a
        scrollbar.
 
        THESE ARE THE FIRST-RUN NUMBERS ONLY. Get-HDTConsoleSetting reads the
        size the console was last left at and New-HDTConsoleHost applies it, so
        an administrator sizes this window once. The values here are what a
        machine with no remembered size opens at, and Hephaestus.psm1 carries
        the same four numbers for the reader and the writer to agree on - a test
        asserts the two files match.
    -->
 
    <Window.Resources>
        <SolidColorBrush x:Key="HDTWindowBrush" Color="#FFF3F3F3" />
        <SolidColorBrush x:Key="HDTBannerBrush" Color="#FF0E639C" />
        <SolidColorBrush x:Key="HDTBannerTextBrush" Color="#FFFFFFFF" />
        <SolidColorBrush x:Key="HDTBannerLabelBrush" Color="#FFD6E9F5" />
        <SolidColorBrush x:Key="HDTPanelBrush" Color="#FFFFFFFF" />
        <SolidColorBrush x:Key="HDTPanelTextBrush" Color="#FF1B1B1B" />
        <SolidColorBrush x:Key="HDTBorderBrush" Color="#FFC8C8C8" />
        <SolidColorBrush x:Key="HDTLabelBrush" Color="#FF0E639C" />
        <SolidColorBrush x:Key="HDTCommandBrush" Color="#FFF7F7F7" />
        <SolidColorBrush x:Key="HDTCommandTextBrush" Color="#FFA31515" />
        <SolidColorBrush x:Key="HDTFieldBrush" Color="#FFEDEDED" />
        <SolidColorBrush x:Key="HDTHintTextBrush" Color="#FF6B6B6B" />
        <SolidColorBrush x:Key="HDTErrorBrush" Color="#FFA31515" />
        <SolidColorBrush x:Key="HDTFooterBrush" Color="#FFE8E8E8" />
        <SolidColorBrush x:Key="HDTButtonBrush" Color="#FF0E639C" />
        <SolidColorBrush x:Key="HDTButtonTextBrush" Color="#FFFFFFFF" />
        <SolidColorBrush x:Key="HDTButtonHoverBrush" Color="#FFCCE4F7" />
        <SolidColorBrush x:Key="HDTButtonHoverTextBrush" Color="#FF000000" />
        <SolidColorBrush x:Key="HDTButtonPressedBrush" Color="#FFA9CFEC" />
 
        <!--
            THE BUTTON CARRIES ITS OWN TEMPLATE, AND ITS COLOURS ARE SETTERS
            RATHER THAN LOCAL VALUES. Two WPF traps meet on this control. A local
            Foreground="White" on the Button element beats any style trigger, so
            a hover rule written that way silently does nothing - the wizard
            workstream hit exactly that. And the stock button template ignores
            Background on hover entirely, painting its own wash, so a Setter
            alone cannot fix the pairing either. The template below is the only
            arrangement where "light wash, dark label" actually reaches the
            screen; Get-HDTConsoleTheme decides which colours those are, and a
            test measures the contrast of the pair in both palettes.
        -->
        <Style TargetType="Button">
            <Setter Property="Background" Value="{DynamicResource HDTButtonBrush}" />
            <Setter Property="Foreground" Value="{DynamicResource HDTButtonTextBrush}" />
            <Setter Property="BorderThickness" Value="0" />
            <Setter Property="FontSize" Value="13" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="Button">
                        <Border x:Name="HDTButtonSurface"
                                Background="{TemplateBinding Background}"
                                BorderBrush="{TemplateBinding BorderBrush}"
                                BorderThickness="{TemplateBinding BorderThickness}">
                            <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsMouseOver" Value="True">
                                <Setter TargetName="HDTButtonSurface" Property="Background"
                                        Value="{DynamicResource HDTButtonHoverBrush}" />
                                <Setter Property="Foreground"
                                        Value="{DynamicResource HDTButtonHoverTextBrush}" />
                            </Trigger>
                            <Trigger Property="IsPressed" Value="True">
                                <Setter TargetName="HDTButtonSurface" Property="Background"
                                        Value="{DynamicResource HDTButtonPressedBrush}" />
                                <Setter Property="Foreground"
                                        Value="{DynamicResource HDTButtonHoverTextBrush}" />
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
 
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
            <RowDefinition Height="64" />
        </Grid.RowDefinitions>
 
        <!-- banner: the share, and BOTH of its paths -->
        <Border Grid.Row="0" Background="{DynamicResource HDTBannerBrush}" Padding="24,16,24,16">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                </Grid.RowDefinitions>
 
                <TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
                           x:Name="HDTShareText"
                           Foreground="{DynamicResource HDTBannerTextBrush}"
                           FontSize="21" FontWeight="SemiBold"
                           TextTrimming="CharacterEllipsis" />
 
                <TextBlock Grid.Row="1" Grid.Column="0"
                           x:Name="HDTConsoleDeployRootLabel"
                           Foreground="{DynamicResource HDTBannerLabelBrush}"
                           FontSize="12" Margin="0,10,14,0" Width="86" />
                <TextBlock Grid.Row="1" Grid.Column="1"
                           x:Name="HDTDeployRootText"
                           Text=""
                           Foreground="{DynamicResource HDTBannerTextBrush}" FontSize="12"
                           FontFamily="Consolas, Courier New" Margin="0,10,0,0"
                           TextTrimming="CharacterEllipsis" />
 
                <TextBlock Grid.Row="2" Grid.Column="0"
                           x:Name="HDTConsoleOpenedFromLabel"
                           Foreground="{DynamicResource HDTBannerLabelBrush}"
                           FontSize="12" Margin="0,4,14,0" Width="86" />
                <TextBlock Grid.Row="2" Grid.Column="1"
                           x:Name="HDTRootText"
                           Text=""
                           Foreground="{DynamicResource HDTBannerTextBrush}" FontSize="12"
                           FontFamily="Consolas, Courier New" Margin="0,4,0,0"
                           TextTrimming="CharacterEllipsis" />
            </Grid>
        </Border>
 
        <!-- body: the tree on the left, the selected row on the right -->
        <Grid Grid.Row="1" Margin="24,20,24,0">
            <Grid.ColumnDefinitions>
                <!--
                    BOTH SIDES ARE STAR-SIZED so the splitter can give width to
                    either one. A fixed left column and a star right column
                    would let the splitter grow the tree and never the details,
                    which is the direction nobody needs: the pane is where the
                    long text is. The MinWidths stop either side being dragged
                    shut, because a pane dragged to nothing looks like a bug
                    rather than a choice.
                -->
                <ColumnDefinition Width="420*" MinWidth="220" />
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="740*" MinWidth="260" />
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="*" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
 
            <TextBlock Grid.Row="0" Grid.Column="0" x:Name="HDTConsoleTreeLabel"
                       Foreground="{DynamicResource HDTLabelBrush}" FontSize="12" Margin="0,0,0,6" />
 
            <TreeView Grid.Row="1" Grid.Column="0"
                      x:Name="HDTConsoleTree"
                      Background="{DynamicResource HDTPanelBrush}"
                      Foreground="{DynamicResource HDTPanelTextBrush}"
                      BorderBrush="{DynamicResource HDTBorderBrush}" BorderThickness="1"
                      FontFamily="Segoe UI" FontSize="13"
                      ScrollViewer.HorizontalScrollBarVisibility="Disabled">
 
                <!--
                    MDT'S New Task Sequence, WHERE MDT PUTS IT: a right-click on
                    the Task Sequences node. A button in the footer was the wrong
                    shape - this is an action ON a node, and the footer is where
                    a window's own actions live.
 
                    THE ITEM IS DARK UNLESS THAT NODE IS THE ONE UNDER THE
                    POINTER, and the host tells which node that is by NAME - the
                    label carries a count and would put a parser in the window.
                -->
                <TreeView.ContextMenu>
                    <!--
                        ONE MENU, AND THE ROW DECIDES WHICH ITEMS IT HAS.
                        New Task Sequence belongs to the Task Sequences category;
                        Remove belongs to a sequence. Items that do not apply are
                        collapsed rather than greyed, and a row with nothing to
                        offer opens no menu at all - a menu that appears
                        everywhere with one live item teaches that right-click
                        does nothing here, on the rows where it does something.
                    -->
                    <ContextMenu x:Name="HDTConsoleTreeMenu">
                        <MenuItem x:Name="HDTNewSequenceMenuItem" />
                        <MenuItem x:Name="HDTRemoveSequenceMenuItem" />
                        <MenuItem x:Name="HDTImportOperatingSystemMenuItem" />
                        <MenuItem x:Name="HDTRemoveOperatingSystemMenuItem" />
                    </ContextMenu>
                </TreeView.ContextMenu>
 
                <TreeView.ItemContainerStyle>
                    <Style TargetType="TreeViewItem">
                        <!-- Which branches start open is the row's decision. -->
                        <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
                        <Setter Property="Foreground" Value="{DynamicResource HDTPanelTextBrush}" />
                        <Setter Property="Padding" Value="2,3,2,3" />
 
                        <!--
                            WITHOUT THIS THE ROW ANNOUNCES ITSELF AS A DUMP OF
                            THE WHOLE OBJECT. A TreeViewItem bound to data with
                            no AutomationProperties.Name falls back to the item's
                            ToString(), and a PSCustomObject's ToString() is
                            every property it has - Depth, Kind, the entire
                            Detail text, the icon, the header. A screen reader
                            reads that blob instead of "Boot Image", and so does
                            any automation that looks a row up by name. Found by
                            driving the window through UI Automation; nothing on
                            screen looks wrong.
                        -->
                        <Setter Property="AutomationProperties.Name" Value="{Binding Text}" />
                    </Style>
                </TreeView.ItemContainerStyle>
 
                <TreeView.ItemTemplate>
                    <HierarchicalDataTemplate ItemsSource="{Binding Children}">
                        <StackPanel Orientation="Horizontal">
                            <!-- COLOURED, AND THE COLOUR IS THE ROW'S. Bound rather than
                                 themed: see Get-HDTConsoleIconColor for why a
                                 per-row DynamicResource key is not possible in
                                 markup XamlReader loads. WPF converts the
                                 string to a Brush through the target property's
                                 own type converter. -->
                            <TextBlock Text="{Binding Icon}"
                                       Foreground="{Binding IconColor}"
                                       FontFamily="Segoe UI Emoji, Segoe UI Symbol"
                                       FontSize="13" Margin="0,0,7,0"
                                       VerticalAlignment="Center" />
                            <TextBlock Text="{Binding Text}"
                                       VerticalAlignment="Center"
                                       TextTrimming="CharacterEllipsis" />
                        </StackPanel>
                    </HierarchicalDataTemplate>
                </TreeView.ItemTemplate>
            </TreeView>
 
            <!--
                The handle between them. It spans the label row as well as the
                tree, so the grip runs the full height of the two panes and
                there is no dead strip above it to grab at by mistake.
            -->
            <GridSplitter Grid.Row="0" Grid.RowSpan="2" Grid.Column="1"
                          x:Name="HDTSplitter"
                          Width="8" Margin="6,0,6,0"
                          HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
                          Background="{DynamicResource HDTBorderBrush}"
                          ShowsPreview="False" />
 
            <TextBlock Grid.Row="0" Grid.Column="2" x:Name="HDTConsoleDetailLabel"
                       Foreground="{DynamicResource HDTLabelBrush}" FontSize="12" Margin="0,0,0,6" />
 
            <!--
                A PROPERTIES SHEET, NOT A PARAGRAPH. One labelled box per fact,
                the way Deployment Workbench shows a selected item: each value
                can be selected and copied on its own, long ones wrap instead of
                running off the edge, and the layout is already the shape an
                editor needs when C2 makes these writable.
 
                WHICH BOXES TAKE TYPING IS THE ROW'S DECISION, not this file's:
                IsReadOnly is bound to the row, and New-HDTConsoleField sets it
                from whether the row names a key in the document. A task
                sequence's name and description do; a step count, a validation
                result and a path do not, and a box that took typing and threw it
                away would be worse than one that plainly does not.
            -->
            <Border Grid.Row="1" Grid.Column="2"
                    Background="{DynamicResource HDTPanelBrush}"
                    BorderBrush="{DynamicResource HDTBorderBrush}" BorderThickness="1">
                <ScrollViewer VerticalScrollBarVisibility="Auto"
                              HorizontalScrollBarVisibility="Disabled"
                              Padding="14,12,14,12">
                    <ItemsControl x:Name="HDTDetailList">
                        <ItemsControl.ItemTemplate>
                            <DataTemplate>
                                <Grid Margin="0,0,0,8">
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition Width="150" />
                                        <ColumnDefinition Width="*" />
                                    </Grid.ColumnDefinitions>
 
                                    <TextBlock Grid.Column="0" Text="{Binding Label}"
                                               Foreground="{DynamicResource HDTPanelTextBrush}"
                                               FontFamily="Segoe UI" FontSize="12"
                                               TextWrapping="Wrap"
                                               Margin="0,5,10,0" VerticalAlignment="Top" />
 
                                    <TextBox Grid.Column="1" x:Name="HDTDetailBox"
                                             Text="{Binding Value}"
                                             IsReadOnly="{Binding ReadOnly}"
                                             AcceptsReturn="True" TextWrapping="Wrap"
                                             Background="{DynamicResource HDTPanelBrush}"
                                             Foreground="{DynamicResource HDTPanelTextBrush}"
                                             BorderBrush="{DynamicResource HDTBorderBrush}"
                                             BorderThickness="1"
                                             FontFamily="Consolas, Courier New" FontSize="12"
                                             Padding="6,4,6,4" />
                                </Grid>
 
                                <!-- WHITE MEANS TYPE HERE, and it is the only
                                     thing on this pane that says so: seven
                                     identical boxes of which two accept a
                                     rename is a pane nobody discovers. The
                                     editor's Properties rows are painted the
                                     same way and by the same means.
 
                                     A TRIGGER INSIDE THE TEMPLATE, NOT A STYLE.
                                     A local Background beats a Style trigger in
                                     WPF - the trap the wizard workstream hit
                                     with Foreground - but a template trigger
                                     naming the element wins. -->
                                <DataTemplate.Triggers>
                                    <DataTrigger Binding="{Binding ReadOnly}" Value="True">
                                        <Setter TargetName="HDTDetailBox" Property="Background"
                                                Value="{DynamicResource HDTFieldBrush}" />
                                    </DataTrigger>
                                </DataTemplate.Triggers>
                            </DataTemplate>
                        </ItemsControl.ItemTemplate>
                    </ItemsControl>
                </ScrollViewer>
            </Border>
 
            <!--
                DESIGN 12: "the console shows that invocation - so an admin can
                learn the automation surface by clicking around, and script
                anything they can do in the UI". Selectable, so it can be copied.
            -->
            <Grid Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3" Margin="0,16,0,0">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
 
                <TextBlock Grid.Column="0" x:Name="HDTConsoleCommandLabel"
                           Foreground="{DynamicResource HDTLabelBrush}" FontSize="12"
                           VerticalAlignment="Center" Margin="0,0,12,0" />
 
                <TextBox Grid.Column="1"
                         x:Name="HDTCommandText"
                         Text=""
                         IsReadOnly="True" TextWrapping="NoWrap"
                         Background="{DynamicResource HDTCommandBrush}"
                         Foreground="{DynamicResource HDTCommandTextBrush}"
                         BorderBrush="{DynamicResource HDTBorderBrush}" BorderThickness="1"
                         FontFamily="Consolas, Courier New" FontSize="13"
                         Padding="8,6,8,6"
                         HorizontalScrollBarVisibility="Auto" />
            </Grid>
        </Grid>
 
        <!-- footer -->
        <Border Grid.Row="2" Background="{DynamicResource HDTFooterBrush}" Margin="0,20,0,0">
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Right"
                        VerticalAlignment="Center" Margin="0,0,24,0">
                <!--
                    No local Background or Foreground: they are Setters in the
                    style above, because a local value would beat the hover
                    trigger and the label would stay white on a light wash.
                -->
                <!--
                    APPLY WRITES WHAT IS TYPED IN THE DETAIL PANE, and it is
                    the only button on this window that touches a share. It is
                    dark until there is something to write: a row whose value
                    differs from what it was built with, on a task sequence.
 
                    IT IS NEEDED EVEN THOUGH A BOX WRITES WHEN IT LOSES FOCUS.
                    The last box somebody types in is the one still holding the
                    caret when they look for a button - so an editor with no
                    Apply asks them to click elsewhere before their change is
                    real, which is a rule nobody can see.
                -->
                <Button x:Name="HDTApplyButton"
                        Width="112" Height="34" Margin="0,0,10,0"
                        IsEnabled="False" />
 
                <Button x:Name="HDTCloseButton"
                        Width="112" Height="34"
                        IsDefault="True" IsCancel="True" />
            </StackPanel>
        </Border>
    </Grid>
</Window>