rss
twitter
    Find out what I'm doing, Follow Me :)

Sunday, September 7, 2014

AvalonEdit – Search panel customization

AvalonEdit serves as the underlying text editor for SharpDevelop, the free and open source alternative to Visual Studio. The team at SharpDevelop have decoupled a lot of functionalities and have provided the text editor as a separate Nuget Package (available here).

While configurable syntax highlighting and code-completion is a very well-known and advertised feature of AvalonEdit, few are aware that AvalonEdit ships with an out-of-the-box Search panel functionality.

Search panel in action

ILSpy Search panel

Image source: AvalonEdit default search panel in ILSpy

Implementation

After referencing the TextEditor in XAML and in code-behind, simply call the install method on the SearchPanel class to enable integrated search panel functionality.

SearchPanel.Install(HostsEditor);

The above snippet will enable the search panel functionality in the editor when the user presses the “CTRL + F” shortcut.


There is a handy Uninstall method available too to disable the search panel functionality.


Customizing the search panel’s look and feel


While the default UI for the search panel might be sufficient for your application, you might need to style the panel in general, to match the look and feel of your application.


While developing EasyHosts (an open-source hosts file editor), I had to re-style the default look and feel of the search panel to match the theme of the application (shown below). Re-styling the search panel template is a breeze thanks to WPF.


EasyHosts Search panel


Search panel template


The default search panel template is available on Github.


Note: EasyHosts uses MahApps.Metro UI theme, so brush references below will have to be replaced by your custom theme brushes.




<Style TargetType="editor:SearchPanel">

    <Setter Property="Template">

        <Setter.Value>

            <ControlTemplate TargetType="{x:Type editor:SearchPanel}">

                <Border Background="{DynamicResource WindowBackgroundBrush}" 

                        BorderBrush="{DynamicResource AccentColorBrush}" 

                        BorderThickness="1,0,1,1" HorizontalAlignment="Right" 

                        VerticalAlignment="Top" Cursor="Arrow">

                    <StackPanel Orientation="Horizontal">

                        <TextBox Name="PART_searchTextBox" Focusable="True" 

                                BorderBrush="{DynamicResource AccentColorBrush}" 

                                Width="150" Height="Auto" Margin="3,3,0,3">

                            <TextBox.Text>

                                <Binding Path="SearchPattern" 

                                        RelativeSource="{RelativeSource TemplatedParent}" 

                                        UpdateSourceTrigger="PropertyChanged">

                                    <Binding.ValidationRules>

                                        <ExceptionValidationRule />

                                    </Binding.ValidationRules>

                                </Binding>

                            </TextBox.Text>

                        </TextBox>

 

                        <!-- FindNext button -->

                        <Button Margin="0,1,1,1" Height="30" Width="30" Command="editor:SearchCommands.FindNext" 

                            ToolTip="{Binding Localization.FindNextText, 

                                    RelativeSource={RelativeSource TemplatedParent}}" 

                            Padding="1" Style="{DynamicResource MetroAccentButton}"

                            BorderThickness="0" BorderBrush="Transparent">

                            <Path Data="F1M-218.342,2910.79L-234.066,2926.52 -233.954,2926.63 -225.428,2926.63 -210.87,2912.07 -206.495,2907.7 -225.313,2888.88 -234.066,2888.88 -218.342,2904.6 -259.829,2904.6 -259.829,2910.79 -218.342,2910.79z" 

                                Style="{DynamicResource DefaultButtonPathStyle}" />

                        </Button>

 

                        <!-- FindPrevious (set visibility if required) button -->

                        <Button Margin="1" Height="30" Width="30" Command="editor:SearchCommands.FindPrevious" 

                            ToolTip="{Binding Localization.FindPreviousText, 

                            RelativeSource={RelativeSource TemplatedParent}}" 

                            Padding="1" Style="{DynamicResource AccentedSquareButtonStyle}" 

                            BorderThickness="0" BorderBrush="Transparent" Visibility="Collapsed">

                            <Path Data="F1M-185.925,-2026.96L-203.062,-2048.74C-197.485,-2056.51 -197.433,-2067.31 -203.64,-2075.2 -211.167,-2084.76 -225.019,-2086.42 -234.588,-2078.89 -244.154,-2071.36 -245.808,-2057.51 -238.282,-2047.94 -231.986,-2039.95 -221.274,-2037.5 -212.337,-2041.31L-195.262,-2019.61 -185.925,-2026.96z M-231.201,-2053.51C-235.653,-2059.17 -234.674,-2067.36 -229.02,-2071.81 -223.36,-2076.26 -215.169,-2075.29 -210.721,-2069.63 -206.269,-2063.97 -207.245,-2055.78 -212.902,-2051.33 -218.559,-2046.88 -226.752,-2047.86 -231.201,-2053.51z" 

                                Stretch="Uniform" Fill="{DynamicResource IdealForegroundColorBrush}" 

                                Width="16" Height="16" />

                        </Button>

 

                        <StackPanel Orientation="Horizontal">

                            <ToggleButton Width="36" Height="36" Margin="0" Cursor="Hand"

                                ToolTip="{Binding Localization.MatchCaseText, RelativeSource={RelativeSource TemplatedParent}}"

                                IsChecked="{Binding MatchCase, RelativeSource={RelativeSource TemplatedParent}}"

                                Style="{DynamicResource MetroCircleToggleButtonStyle}" Content="aA" FontWeight="Bold" FontFamily="Consolas,Courier New,Courier">

                            </ToggleButton>

 

                            <ToggleButton Width="36" Height="36" Margin="0" Cursor="Hand" Style="{DynamicResource MetroCircleToggleButtonStyle}" 

                                ToolTip="{Binding Localization.MatchWholeWordsText, RelativeSource={RelativeSource TemplatedParent}}"

                                IsChecked="{Binding WholeWords, RelativeSource={RelativeSource TemplatedParent}}"

                                Content="Ab" FontWeight="Bold" FontFamily="Consolas,Courier New,Courier">

                            </ToggleButton>

 

                            <ToggleButton Width="36" Height="36" Margin="0" Cursor="Hand" Style="{DynamicResource MetroCircleToggleButtonStyle}" 

                                ToolTip="{Binding Localization.UseRegexText, RelativeSource={RelativeSource TemplatedParent}}"

                                IsChecked="{Binding UseRegex, RelativeSource={RelativeSource TemplatedParent}}"

                                Content="a*" FontWeight="Bold" FontFamily="Consolas,Courier New,Courier">

                            </ToggleButton>

                        </StackPanel>

 

                        <!-- Search Panel close button -->

                        <Button Height="16" Width="16" HorizontalAlignment="Right" Padding="0"

                                Background="Transparent" Cursor="Hand"

                                VerticalAlignment="Top" Command="editor:SearchCommands.CloseSearchPanel"

                                VerticalContentAlignment="Center" HorizontalContentAlignment="Center">

                            <Path Data="F1M54.0573,47.8776L38.1771,31.9974 54.0547,16.1198C55.7604,14.4141 55.7604,11.6511 54.0573,9.94531 52.3516,8.23962 49.5859,8.23962 47.8802,9.94531L32.0026,25.8229 16.1224,9.94531C14.4167,8.23962 11.6511,8.23962 9.94794,9.94531 8.24219,11.6511 8.24219,14.4141 9.94794,16.1198L25.8255,32 9.94794,47.8776C8.24219,49.5834 8.24219,52.3477 9.94794,54.0534 11.6511,55.7572 14.4167,55.7585 16.1224,54.0534L32.0026,38.1745 47.8802,54.0534C49.5859,55.7585 52.3516,55.7572 54.0573,54.0534 55.7604,52.3477 55.763,49.5834 54.0573,47.8776z" 

                                Height="10" Width="10" Stretch="Uniform" Fill="Red" Margin="0" />

                        </Button>

                    </StackPanel>

                </Border>

            </ControlTemplate>

        </Setter.Value>

    </Setter>


</Style>




Happy Coding!

Thursday, May 8, 2014

Azure Portal Navigation Style in WPF

When it comes to product user experiences, Microsoft has always delighted and surprised its users with innovative user interfaces, be it the Office Ribbon UI, Dynamics CRM or Metro UI in Windows 8. The Windows Azure portal too is a great example of elegant user experience that embraces effective call to action. Below is one of the screens from the management portal:

clip_image002

Image source: Microsoft Azure portal.

Ever since I got a glimpse of the portal I have been inching to use this navigation pattern in my application and when finally I did get a chance to put the navigation pattern to use, WPF was the obvious choice. The final result of the ListBox styling is shown below:

clip_image004

The tools and techniques used to design/style the UI is described below.

Tools of the trade:

  • Kaxaml – the hands down best XAML editor on this planet (and beyond)
  • ColorZilla’s eye dropper – for extracting colors from the portal image above
  • Metro Studio – the awesome developer friendly icon editor for XAML icons

 

Step 1: Defining base colors

<!-- background color of the grid and the list box -->

<SolidColorBrush x:Key="azureItemBackground" Color="#3C454F" />

<!-- background color of the selected list box item -->

<SolidColorBrush x:Key="azureItemSelected" Color="#6D747B" />

<!-- foreground color for the extra text displayed under the primary text -->

<SolidColorBrush x:Key="azureItemHighlightText" Color="#89C402" />

<!-- metro color for the title and other highlighting -->

<SolidColorBrush x:Key="AccentColorBrush" Color="CornflowerBlue" />

Step 2: Styling the ListBox

The ListBox control along with the content composition model in WPF makes designing this interface a breeze. First the ListBox control itself requires some property changes as below:

<Style x:Key="azureListBoxStyle" TargetType="ListBox">

   <Setter Property="BorderThickness" Value="0,0,1,0" />

   <Setter Property="Background" Value="{StaticResource azureItemBackground}" />

   <Setter Property="HorizontalAlignment" Value="Left" />

   <Setter Property="VerticalAlignment" Value="Stretch" />

   <Setter Property="HorizontalContentAlignment" Value="Stretch" />

   <Setter Property="VerticalContentAlignment" Value="Stretch" />

</Style>

Step 3: Creating the ListBoxItemTemplate (the critical piece)

<DataTemplate x:Key="azureListItemTemplate">

        <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">

            <Grid.ColumnDefinitions>

                <ColumnDefinition Width="20" />

                <ColumnDefinition Width="Auto" />

                <ColumnDefinition Width="*" />

            </Grid.ColumnDefinitions>

            <Rectangle Width="13" Margin="-30,0,0,0" Grid.Column="0">

                <Rectangle.Style>

                    <Style TargetType="Rectangle">

                        <Setter Property="Fill" Value="Transparent" />

                        <Style.Triggers>

                            <DataTrigger Binding="{Binding Path=IsSelected,

                                                   RelativeSource={RelativeSource Mode=FindAncestor,

                                                   AncestorType={x:Type ListBoxItem}}}"

                                     Value="True">

                                <Setter Property="Fill" Value="White" />

                            </DataTrigger>

                            <DataTrigger Binding="{Binding Path=IsMouseOver,

                                                   RelativeSource={RelativeSource Mode=FindAncestor,

                                                   AncestorType={x:Type ListBoxItem}}}"

                                     Value="True">

                                <Setter Property="Fill" Value="{DynamicResource AccentColorBrush}" />

                            </DataTrigger>

                        </Style.Triggers>

                    </Style>

                </Rectangle.Style>

            </Rectangle>

   

            <Path Margin="0,10,5,10" Grid.Row="0" Grid.Column="1" x:Name="listItemIcon"

              HorizontalAlignment="Center" VerticalAlignment="Center"

              Stretch="Uniform" Width="30" Height="30"

              Data="{Binding XPath=@Picture}">

                <Path.Style>

                    <Style TargetType="Path">

                        <Setter Property="Fill" Value="#DADCDE" />

                        <Style.Triggers>

                            <DataTrigger Binding="{Binding Path=IsSelected, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}}"

                                     Value="True">

                                <Setter Property="Fill" Value="White" />

                            </DataTrigger>

                            <DataTrigger Binding="{Binding Path=IsMouseOver,

                                                   RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}}"

                                     Value="True">

                                <Setter Property="Fill" Value="White" />

                            </DataTrigger>

                        </Style.Triggers>

                    </Style>

                </Path.Style>

            </Path>

   

            <TextBlock Grid.Row="0" Grid.Column="2" Margin="10"

                   HorizontalAlignment="Left" VerticalAlignment="Center">

              <Run Text="{Binding XPath=@Name}" FontWeight="SemiBold"

                   Foreground="{Binding ElementName=listItemIcon, Path=Fill}" />

              <Run Text="{Binding XPath=@Hint}" FontWeight="Bold"

                   Foreground="{StaticResource azureItemHighlightText}" />

            </TextBlock>

        </Grid>

    </DataTemplate>

 

Step 4: Styling the ItemContainer

<Style x:Key="azureItemContainerStyle" TargetType="ListBoxItem">

   <Setter Property="Cursor" Value="Hand" />

   <Setter Property="Background" Value="{StaticResource azureItemBackground}" />

   <Style.Triggers>

      <Trigger Property="IsMouseOver" Value="True">

         <Setter Property="Background" Value="{StaticResource azureItemSelected}" />

      </Trigger>

      <Trigger Property="IsSelected" Value="True">

         <Setter Property="Background" Value="{StaticResource azureItemSelected}" />

</Trigger>

   </Style.Triggers>

</Style>

 

Step 5: Overwriting windows Highlight Brush styles

<ListBox Grid.Row="1" ItemTemplate="{StaticResource azureListItemTemplate}" Width="300" Style="{StaticResource azureListBoxStyle}" ItemContainerStyle="{StaticResource azureItemContainerStyle}"

     ItemsSource="{Binding Source={StaticResource AzureActions}, XPath=//Action}">

   <ListBox.Resources>

        <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="#6D747B" />

        <SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="#6D747B" />

   </ListBox.Resources>

</ListBox>

For populating the ListBox’s with data, XmlDataProvider has been used.

Please download the full XAML file and paste it in Kaxaml or Visual Studio to see it in action. No external dependencies.

download AzureNavigationStyle.xaml

Imitation is indeed the best form of flattery.

Happy Coding!