Implemented tab reordering

Implemented tab undocking / docking
This commit is contained in:
Mark van Renswoude 2021-12-18 14:02:55 +01:00
parent 2e6524f3b9
commit c9636aff04
22 changed files with 760 additions and 93 deletions

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 58 58" style="enable-background:new 0 0 58 58;" xml:space="preserve">
<g>
<g>
<rect y="16" style="fill:#ECF0F1;" width="50" height="35"/>
<rect y="2" style="fill:#546A79;" width="50" height="14"/>
<circle style="fill:#ED7161;" cx="7" cy="9" r="3"/>
<circle style="fill:#EFC41A;" cx="16" cy="9" r="3"/>
<circle style="fill:#4FBA6E;" cx="25" cy="9" r="3"/>
</g>
<g>
<rect x="36" y="34" style="fill:#48A0DC;" width="22" height="22"/>
<rect x="46" y="40" style="fill:#FFFFFF;" width="2" height="16"/>
<polygon style="fill:#FFFFFF;" points="52.293,46.707 47,41.414 41.707,46.707 40.293,45.293 47,38.586 53.707,45.293 "/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 58 58" style="enable-background:new 0 0 58 58;" xml:space="preserve">
<g>
<g>
<rect y="16" style="fill:#ECF0F1;" width="50" height="35"/>
<rect y="2" style="fill:#546A79;" width="50" height="14"/>
<circle style="fill:#ED7161;" cx="7" cy="9" r="3"/>
<circle style="fill:#EFC41A;" cx="16" cy="9" r="3"/>
<circle style="fill:#4FBA6E;" cx="25" cy="9" r="3"/>
</g>
<g>
<rect x="36" y="34" style="fill:#21AE5E;" width="22" height="22"/>
<rect x="46" y="35.586" style="fill:#FFFFFF;" width="2" height="16"/>
<polygon style="fill:#FFFFFF;" points="47,53 40,46.293 41.476,44.879 47,50.172 52.524,44.879 54,46.293 "/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -18,13 +18,16 @@
</PropertyGroup>
<ItemGroup>
<None Remove="Images\Dock.svg" />
<None Remove="Images\PublishSend.svg" />
<None Remove="Images\Undock.svg" />
</ItemGroup>
<ItemGroup>
<Resource Include="Images\Clear.svg" />
<Resource Include="Images\Connect.svg" />
<Resource Include="Images\Disconnect.svg" />
<Resource Include="Images\Dock.svg" />
<Resource Include="Images\Publish.svg" />
<Resource Include="Images\PublishSend.svg" />
<Resource Include="Images\Subscribe.svg" />
@ -42,6 +45,10 @@
<ProjectReference Include="..\PettingZoo.Settings.LiteDB\PettingZoo.Settings.LiteDB.csproj" />
</ItemGroup>
<ItemGroup>
<Resource Include="Images\Undock.svg" />
</ItemGroup>
<ItemGroup>
<Compile Update="UI\Connection\ConnectionDisplayNameStrings.Designer.cs">
<DesignTime>True</DesignTime>
@ -91,6 +98,11 @@
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
<Compile Update="UI\Tab\Undocked\UndockedTabHostStrings.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>UndockedTabHostStrings.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
@ -130,6 +142,10 @@
<LastGenOutput>SubscriberViewStrings.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="UI\Tab\Undocked\UndockedTabHostStrings.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>UndockedTabHostStrings.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>

View File

@ -1,11 +1,10 @@
Must-have
---------
- Option to not save password in profiles
Should-have
-----------
- Support undocking tabs (and redocking afterwards)
- Allow tab reordering
- Save / load publisher messages (either as templates or to disk)
- Export received messages to Tapeti JSON file / Tapeti.Cmd command-line

View File

@ -235,7 +235,6 @@ namespace PettingZoo.UI.Connection
private bool SaveCanExecute()
{
// TODO check changes in parameters (excluding password)
return SelectedStoredConnection != null &&
SelectedStoredConnection.Id != Guid.Empty &&
ValidConnection(false) &&

View File

@ -0,0 +1,8 @@
namespace PettingZoo.UI.Main
{
public interface ITabContainer
{
public double TabWidth { get; }
public double TabHeight { get; }
}
}

View File

@ -6,6 +6,7 @@
xmlns:main="clr-namespace:PettingZoo.UI.Main"
xmlns:tab="clr-namespace:PettingZoo.UI.Tab"
xmlns:svgc="http://sharpvectors.codeplex.com/svgc/"
xmlns:ui="clr-namespace:PettingZoo.UI"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance main:DesignTimeMainWindowViewModel, IsDesignTimeCreatable=True}"
Width="800"
@ -17,7 +18,11 @@
Closed="MainWindow_OnClosed">
<Window.InputBindings>
<KeyBinding Modifiers="Control" Key="W" Command="{Binding CloseTabCommand}" />
<KeyBinding Modifiers="Control" Key="U" Command="{Binding UndockTabCommand}" />
</Window.InputBindings>
<Window.Resources>
<ui:BindingProxy x:Key="ContextMenuProxy" Data="{Binding}" />
</Window.Resources>
<DockPanel>
<ToolBar DockPanel.Dock="Top" ToolBarTray.IsLocked="True">
<Button Command="{Binding ConnectCommand}">
@ -45,6 +50,12 @@
<TextBlock Margin="3,0,0,0" Text="{x:Static main:MainWindowStrings.CommandPublish}" />
</StackPanel>
</Button>
<Button Command="{Binding UndockTabCommand}">
<StackPanel Orientation="Horizontal">
<Image Source="{svgc:SvgImage Source=/Images/Undock.svg, AppName=PettingZoo}" Width="16" Height="16" Style="{StaticResource ToolbarIcon}"/>
<TextBlock Margin="3,0,0,0" Text="{x:Static main:MainWindowStrings.CommandUndock}" />
</StackPanel>
</Button>
<Separator Style="{StaticResource {x:Static ToolBar.SeparatorStyleKey}}" Visibility="{Binding ToolbarCommandsSeparatorVisibility}" />
<ItemsControl ItemsSource="{Binding ToolbarCommands}">
@ -70,28 +81,39 @@
<TextBlock Text="{Binding ConnectionStatus}"/>
</StatusBarItem>
</StatusBar>
<TabControl
<Grid>
<TabControl
Name="SubscriberTabs"
ItemsSource="{Binding Tabs}"
SelectedValue="{Binding ActiveTab}">
<TabControl.ItemContainerStyle>
<Style TargetType="{x:Type TabItem}">
<Setter Property="Header" Value="{Binding Title}" />
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu>
<MenuItem Header="{x:Static main:MainWindowStrings.ContextMenuCloseTab}" Command="{Binding CloseTabCommand}" InputGestureText="Ctrl+W" />
</ContextMenu>
</Setter.Value>
</Setter>
</Style>
</TabControl.ItemContainerStyle>
<TabControl.ContentTemplate>
<DataTemplate DataType="{x:Type tab:ITab}">
<ContentControl Content="{Binding Content}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
<TabControl.ItemContainerStyle>
<Style TargetType="{x:Type TabItem}">
<!-- ReSharper disable once Xaml.BindingWithContextNotResolved - valid property for ITab, not sure how to specify the DataContext here so just ignore the warning for now -->
<Setter Property="Header" Value="{Binding Title}" />
<Setter Property="AllowDrop" Value="True" />
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu>
<!-- ReSharper disable Xaml.BindingWithContextNotResolved - binding is correct, just weird because of the required proxy -->
<MenuItem Header="{x:Static main:MainWindowStrings.ContextMenuUndockTab}" Command="{Binding Data.UndockTabCommand, Source={StaticResource ContextMenuProxy}}" InputGestureText="Ctrl+U" />
<MenuItem Header="{x:Static main:MainWindowStrings.ContextMenuCloseTab}" Command="{Binding Data.CloseTabCommand, Source={StaticResource ContextMenuProxy}}" InputGestureText="Ctrl+W" />
<!-- ReSharper restore Xaml.BindingWithContextNotResolved -->
</ContextMenu>
</Setter.Value>
</Setter>
<EventSetter Event="PreviewMouseRightButtonDown" Handler="TabItem_PreviewRightMouseDown" />
<EventSetter Event="PreviewMouseMove" Handler="TabItem_PreviewMouseMove" />
<EventSetter Event="Drop" Handler="TabItem_Drop" />
</Style>
</TabControl.ItemContainerStyle>
<TabControl.ContentTemplate>
<DataTemplate DataType="{x:Type tab:ITab}">
<ContentControl Content="{Binding Content}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
<TextBlock Text="{x:Static main:MainWindowStrings.TabsEmptyText}" HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="{Binding NoTabsVisibility}" />
</Grid>
</DockPanel>
</Window>

View File

@ -1,15 +1,20 @@
using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using PettingZoo.Core.Connection;
using PettingZoo.UI.Connection;
using PettingZoo.UI.Subscribe;
using PettingZoo.UI.Tab;
// TODO improve readability of the connection status (especially when connecting/disconnected)
namespace PettingZoo.UI.Main
{
#pragma warning disable CA1001 // MainWindow can't be IDisposable, handled instead in OnDispatcherShutDownStarted
public partial class MainWindow
public partial class MainWindow : ITabContainer
{
private readonly MainWindowViewModel viewModel;
@ -18,7 +23,7 @@ namespace PettingZoo.UI.Main
{
WindowStartupLocation = WindowStartupLocation.CenterScreen;
viewModel = new MainWindowViewModel(connectionFactory, connectionDialog, subscribeDialog);
viewModel = new MainWindowViewModel(connectionFactory, connectionDialog, subscribeDialog, this);
DataContext = viewModel;
InitializeComponent();
@ -43,6 +48,74 @@ namespace PettingZoo.UI.Main
{
var _ = Application.Current.Windows;
}
private void TabItem_PreviewRightMouseDown(object sender, MouseButtonEventArgs e)
{
var tabItem = GetParent<TabItem>(e.OriginalSource);
if (tabItem == null)
return;
var tabControl = GetParent<TabControl>(tabItem);
if (tabControl == null)
return;
tabControl.SelectedItem = tabItem.DataContext;
}
private void TabItem_PreviewMouseMove(object sender, MouseEventArgs e)
{
if (e.Source is not TabItem tabItem)
return;
if (Mouse.PrimaryDevice.LeftButton == MouseButtonState.Pressed)
DragDrop.DoDragDrop(tabItem, tabItem, DragDropEffects.All);
}
private void TabItem_Drop(object sender, DragEventArgs e)
{
var targetTab = GetParent<TabItem>(e.OriginalSource);
if (targetTab == null)
return;
var sourceTab = (TabItem?)e.Data.GetData(typeof(TabItem));
if (sourceTab == null || sourceTab == targetTab)
return;
var tabControl = GetParent<TabControl>(targetTab);
if (tabControl?.ItemsSource is not ObservableCollection<ITab> dataCollection)
return;
if (sourceTab.DataContext is not ITab sourceData || targetTab.DataContext is not ITab targetData)
return;
var sourceIndex = dataCollection.IndexOf(sourceData);
var targetIndex = dataCollection.IndexOf(targetData);
dataCollection.Move(sourceIndex, targetIndex);
}
private static T? GetParent<T>(object originalSource) where T : DependencyObject
{
var current = originalSource as DependencyObject;
while (current != null)
{
if (current is T targetType)
return targetType;
current = VisualTreeHelper.GetParent(current);
}
return null;
}
public double TabWidth => SubscriberTabs.ActualWidth;
public double TabHeight => SubscriberTabs.ActualHeight;
}
#pragma warning restore CA1001
}

View File

@ -19,7 +19,7 @@ namespace PettingZoo.UI.Main {
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class MainWindowStrings {
@ -96,6 +96,15 @@ namespace PettingZoo.UI.Main {
}
}
/// <summary>
/// Looks up a localized string similar to Undock tab.
/// </summary>
public static string CommandUndock {
get {
return ResourceManager.GetString("CommandUndock", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Close tab.
/// </summary>
@ -105,6 +114,15 @@ namespace PettingZoo.UI.Main {
}
}
/// <summary>
/// Looks up a localized string similar to Undock.
/// </summary>
public static string ContextMenuUndockTab {
get {
return ResourceManager.GetString("ContextMenuUndockTab", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Non-persistent.
/// </summary>
@ -159,6 +177,15 @@ namespace PettingZoo.UI.Main {
}
}
/// <summary>
/// Looks up a localized string similar to No open tabs. Click on New Publisher or New Subscriber to open a new tab..
/// </summary>
public static string TabsEmptyText {
get {
return ResourceManager.GetString("TabsEmptyText", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Petting Zoo - a RabbitMQ live message viewer.
/// </summary>

View File

@ -129,9 +129,15 @@
<data name="CommandSubscribe" xml:space="preserve">
<value>New Subscriber...</value>
</data>
<data name="CommandUndock" xml:space="preserve">
<value>Undock tab</value>
</data>
<data name="ContextMenuCloseTab" xml:space="preserve">
<value>Close tab</value>
</data>
<data name="ContextMenuUndockTab" xml:space="preserve">
<value>Undock</value>
</data>
<data name="DeliveryModeNonPersistent" xml:space="preserve">
<value>Non-persistent</value>
</data>
@ -150,6 +156,9 @@
<data name="StatusError" xml:space="preserve">
<value>Error: {0}</value>
</data>
<data name="TabsEmptyText" xml:space="preserve">
<value>No open tabs. Click on New Publisher or New Subscriber to open a new tab.</value>
</data>
<data name="WindowTitle" xml:space="preserve">
<value>Petting Zoo - a RabbitMQ live message viewer</value>
</data>

View File

@ -9,6 +9,7 @@ using PettingZoo.Core.Connection;
using PettingZoo.UI.Connection;
using PettingZoo.UI.Subscribe;
using PettingZoo.UI.Tab;
using PettingZoo.UI.Tab.Undocked;
namespace PettingZoo.UI.Main
{
@ -17,18 +18,21 @@ namespace PettingZoo.UI.Main
private readonly IConnectionFactory connectionFactory;
private readonly IConnectionDialog connectionDialog;
private readonly ISubscribeDialog subscribeDialog;
private readonly ITabContainer tabContainer;
private readonly ITabFactory tabFactory;
private SubscribeDialogParams? subscribeDialogParams;
private IConnection? connection;
private string connectionStatus;
private ITab? activeTab;
private readonly Dictionary<ITab, Window> undockedTabs = new();
private readonly DelegateCommand connectCommand;
private readonly DelegateCommand disconnectCommand;
private readonly DelegateCommand publishCommand;
private readonly DelegateCommand subscribeCommand;
private readonly DelegateCommand closeTabCommand;
private readonly DelegateCommand undockTabCommand;
public string ConnectionStatus
@ -60,6 +64,7 @@ namespace PettingZoo.UI.Main
public ICommand PublishCommand => publishCommand;
public ICommand SubscribeCommand => subscribeCommand;
public ICommand CloseTabCommand => closeTabCommand;
public ICommand UndockTabCommand => undockTabCommand;
public IEnumerable<TabToolbarCommand> ToolbarCommands => ActiveTab is ITabToolbarCommands tabToolbarCommands
? tabToolbarCommands.ToolbarCommands
@ -68,13 +73,17 @@ namespace PettingZoo.UI.Main
public Visibility ToolbarCommandsSeparatorVisibility =>
ToolbarCommands.Any() ? Visibility.Visible : Visibility.Collapsed;
public Visibility NoTabsVisibility =>
Tabs.Count > 0 ? Visibility.Collapsed : Visibility.Visible;
public MainWindowViewModel(IConnectionFactory connectionFactory, IConnectionDialog connectionDialog,
ISubscribeDialog subscribeDialog)
ISubscribeDialog subscribeDialog, ITabContainer tabContainer)
{
this.connectionFactory = connectionFactory;
this.connectionDialog = connectionDialog;
this.subscribeDialog = subscribeDialog;
this.tabContainer = tabContainer;
connectionStatus = GetConnectionStatus(null);
@ -83,9 +92,10 @@ namespace PettingZoo.UI.Main
disconnectCommand = new DelegateCommand(DisconnectExecute, IsConnectedCanExecute);
publishCommand = new DelegateCommand(PublishExecute, IsConnectedCanExecute);
subscribeCommand = new DelegateCommand(SubscribeExecute, IsConnectedCanExecute);
closeTabCommand = new DelegateCommand(CloseTabExecute, CloseTabCanExecute);
closeTabCommand = new DelegateCommand(CloseTabExecute, HasActiveTabCanExecute);
undockTabCommand = new DelegateCommand(UndockTabExecute, HasActiveTabCanExecute);
tabFactory = new ViewTabFactory(this, closeTabCommand);
tabFactory = new ViewTabFactory(this);
}
@ -125,7 +135,16 @@ namespace PettingZoo.UI.Main
private async void DisconnectExecute()
{
Tabs.Clear();
var capturedUndockedTabs = undockedTabs.ToList();
undockedTabs.Clear();
foreach (var undockedTab in capturedUndockedTabs)
undockedTab.Value.Close();
RaisePropertyChanged(nameof(NoTabsVisibility));
undockTabCommand.RaiseCanExecuteChanged();
if (connection != null)
{
await connection.DisposeAsync();
@ -170,24 +189,48 @@ namespace PettingZoo.UI.Main
private void CloseTabExecute()
{
if (ActiveTab == null)
RemoveActiveTab();
}
private void UndockTabExecute()
{
var tab = RemoveActiveTab();
if (tab == null)
return;
var tabHostWindow = UndockedTabHostWindow.Create(this, tab, tabContainer.TabWidth, tabContainer.TabHeight);
undockedTabs.Add(tab, tabHostWindow);
tabHostWindow.Show();
}
private ITab? RemoveActiveTab()
{
if (ActiveTab == null)
return null;
var activeTabIndex = Tabs.IndexOf(ActiveTab);
if (activeTabIndex == -1)
return;
return null;
var tab = Tabs[activeTabIndex];
Tabs.RemoveAt(activeTabIndex);
if (activeTabIndex == Tabs.Count)
activeTabIndex--;
ActiveTab = activeTabIndex >= 0 ? Tabs[activeTabIndex] : null;
closeTabCommand.RaiseCanExecuteChanged();
undockTabCommand.RaiseCanExecuteChanged();
RaisePropertyChanged(nameof(NoTabsVisibility));
return tab;
}
private bool CloseTabCanExecute()
private bool HasActiveTabCanExecute()
{
return ActiveTab != null;
}
@ -199,9 +242,25 @@ namespace PettingZoo.UI.Main
ActiveTab = tab;
closeTabCommand.RaiseCanExecuteChanged();
undockTabCommand.RaiseCanExecuteChanged();
RaisePropertyChanged(nameof(NoTabsVisibility));
}
public void DockTab(ITab tab)
{
if (undockedTabs.Remove(tab, out var tabHostWindow))
tabHostWindow.Close();
AddTab(tab);
}
public void UndockedTabClosed(ITab tab)
{
undockedTabs.Remove(tab);
}
private void ConnectionChanged()
{
disconnectCommand.RaiseCanExecuteChanged();
@ -232,7 +291,7 @@ namespace PettingZoo.UI.Main
public class DesignTimeMainWindowViewModel : MainWindowViewModel
{
public DesignTimeMainWindowViewModel() : base(null!, null!, null!)
public DesignTimeMainWindowViewModel() : base(null!, null!, null!, null!)
{
}
}

View File

@ -23,7 +23,6 @@ namespace PettingZoo.UI.Tab
{
string Title { get; }
ContentControl Content { get; }
ICommand CloseTabCommand { get; }
}

View File

@ -3,5 +3,8 @@
public interface ITabHost
{
void AddTab(ITab tab);
void DockTab(ITab tab);
void UndockedTabClosed(ITab tab);
}
}

View File

@ -10,54 +10,57 @@
d:DesignWidth="800"
d:DataContext="{d:DesignInstance res:DesignTimePublisherViewModel, IsDesignTimeCreatable=True}"
Background="White">
<ui:GridLayout Style="{StaticResource Form}" Margin="4" Grid.IsSharedSizeScope="True">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="16"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="16"/>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Label" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ui:GridLayout Style="{StaticResource Form}" Margin="4" Grid.IsSharedSizeScope="True">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="16"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="16"/>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Label" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="1">
<StackPanel Orientation="Horizontal">
<RadioButton Content="{x:Static res:PublisherViewStrings.LabelSendToExchange}" IsChecked="{Binding SendToExchange}" Style="{StaticResource TypeSelection}" />
<RadioButton Content="{x:Static res:PublisherViewStrings.LabelSendToQueue}" IsChecked="{Binding SendToQueue}" Style="{StaticResource TypeSelection}" />
<Label Grid.Row="0" Grid.Column="1">
<StackPanel Orientation="Horizontal">
<RadioButton Content="{x:Static res:PublisherViewStrings.LabelSendToExchange}" IsChecked="{Binding SendToExchange}" Style="{StaticResource TypeSelection}" />
<RadioButton Content="{x:Static res:PublisherViewStrings.LabelSendToQueue}" IsChecked="{Binding SendToQueue}" Style="{StaticResource TypeSelection}" />
</StackPanel>
</Label>
<Label Grid.Row="1" Grid.Column="0" Content="{x:Static res:PublisherViewStrings.LabelExchange}" Visibility="{Binding ExchangeVisibility}" />
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Exchange}" Visibility="{Binding ExchangeVisibility}" />
<Label Grid.Row="2" Grid.Column="0" Content="{x:Static res:PublisherViewStrings.LabelRoutingKey}" Visibility="{Binding ExchangeVisibility}" />
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding RoutingKey}" Visibility="{Binding ExchangeVisibility}" />
<Label Grid.Row="3" Grid.Column="0" Content="{x:Static res:PublisherViewStrings.LabelQueue}" Visibility="{Binding QueueVisibility}" />
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding Queue}" Visibility="{Binding QueueVisibility}" />
<Label Grid.Row="5" Grid.Column="0" Content="{x:Static res:PublisherViewStrings.LabelReplyTo}" />
<StackPanel Orientation="Horizontal" Grid.Row="5" Grid.Column="1">
<RadioButton Content="{x:Static res:PublisherViewStrings.LabelReplyToSpecified}" IsChecked="{Binding ReplyToSpecified}" Style="{StaticResource TypeSelection}" />
<RadioButton Content="{x:Static res:PublisherViewStrings.LabelReplyToNewSubscriber}" IsChecked="{Binding ReplyToNewSubscriber}" Style="{StaticResource TypeSelection}" />
</StackPanel>
</Label>
<TextBox Grid.Row="6" Grid.Column="1" Text="{Binding ReplyTo}" IsEnabled="{Binding ReplyToSpecified}" />
<Label Grid.Row="1" Grid.Column="0" Content="{x:Static res:PublisherViewStrings.LabelExchange}" Visibility="{Binding ExchangeVisibility}" />
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Exchange}" Visibility="{Binding ExchangeVisibility}" />
<StackPanel Orientation="Horizontal" Grid.Row="8" Grid.Column="0" Grid.ColumnSpan="2" HorizontalAlignment="Center">
<ToggleButton Content="{x:Static res:PublisherViewStrings.OptionMessageTypeRaw}" Style="{StaticResource TypeSelection}" IsChecked="{Binding MessageTypeRaw}" />
<ToggleButton Content="{x:Static res:PublisherViewStrings.OptionMessageTypeTapeti}" Style="{StaticResource TypeSelection}" IsChecked="{Binding MessageTypeTapeti}" />
</StackPanel>
<Label Grid.Row="2" Grid.Column="0" Content="{x:Static res:PublisherViewStrings.LabelRoutingKey}" Visibility="{Binding ExchangeVisibility}" />
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding RoutingKey}" Visibility="{Binding ExchangeVisibility}" />
<ContentControl Grid.Row="9" Grid.Column="0" Grid.ColumnSpan="2" Margin="0 8 0 0" Content="{Binding MessageTypeControl}" />
<Label Grid.Row="3" Grid.Column="0" Content="{x:Static res:PublisherViewStrings.LabelQueue}" Visibility="{Binding QueueVisibility}" />
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding Queue}" Visibility="{Binding QueueVisibility}" />
<Label Grid.Row="5" Grid.Column="0" Content="{x:Static res:PublisherViewStrings.LabelReplyTo}" />
<StackPanel Orientation="Horizontal" Grid.Row="5" Grid.Column="1">
<RadioButton Content="{x:Static res:PublisherViewStrings.LabelReplyToSpecified}" IsChecked="{Binding ReplyToSpecified}" Style="{StaticResource TypeSelection}" />
<RadioButton Content="{x:Static res:PublisherViewStrings.LabelReplyToNewSubscriber}" IsChecked="{Binding ReplyToNewSubscriber}" Style="{StaticResource TypeSelection}" />
</StackPanel>
<TextBox Grid.Row="6" Grid.Column="1" Text="{Binding ReplyTo}" IsEnabled="{Binding ReplyToSpecified}" />
<StackPanel Orientation="Horizontal" Grid.Row="8" Grid.Column="0" Grid.ColumnSpan="2" HorizontalAlignment="Center">
<ToggleButton Content="{x:Static res:PublisherViewStrings.OptionMessageTypeRaw}" Style="{StaticResource TypeSelection}" IsChecked="{Binding MessageTypeRaw}" />
<ToggleButton Content="{x:Static res:PublisherViewStrings.OptionMessageTypeTapeti}" Style="{StaticResource TypeSelection}" IsChecked="{Binding MessageTypeTapeti}" />
</StackPanel>
<ScrollViewer Grid.Row="9" Grid.Column="0" Grid.ColumnSpan="2" VerticalScrollBarVisibility="Auto">
<ContentControl Margin="0 8 0 0" Content="{Binding MessageTypeControl}" />
</ScrollViewer>
</ui:GridLayout>
<Button Grid.Row="10" Grid.Column="1" Command="{Binding PublishCommand}" Content="{x:Static res:PublisherViewStrings.CommandPublish}" HorizontalAlignment="Left" />
</ui:GridLayout>
</ScrollViewer>
</UserControl>

View File

@ -5,8 +5,6 @@ using System.Windows.Controls;
using System.Windows.Input;
using PettingZoo.Core.Connection;
// TODO publish button in page instead of just in toolbar
namespace PettingZoo.UI.Tab.Publisher
{
public enum MessageType

View File

@ -0,0 +1,72 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace PettingZoo.UI.Tab.Undocked {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class UndockedTabHostStrings {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal UndockedTabHostStrings() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.UI.Tab.Undocked.UndockedTabHostStrings", typeof(UndockedTabHostStrings).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Dock tab.
/// </summary>
public static string CommandDock {
get {
return ResourceManager.GetString("CommandDock", resourceCulture);
}
}
}
}

View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="CommandDock" xml:space="preserve">
<value>Dock tab</value>
</data>
</root>

View File

@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace PettingZoo.UI.Tab.Undocked
{
public class UndockedTabHostViewModel : BaseViewModel
{
private readonly ITabHost tabHost;
private readonly ITab tab;
private readonly DelegateCommand dockCommand;
public string Title => tab.Title;
public ContentControl Content => tab.Content;
public IEnumerable<TabToolbarCommand> ToolbarCommands => tab.ToolbarCommands;
public Visibility ToolbarCommandsSeparatorVisibility =>
ToolbarCommands.Any() ? Visibility.Visible : Visibility.Collapsed;
public ICommand DockCommand => dockCommand;
public UndockedTabHostViewModel(ITabHost tabHost, ITab tab)
{
this.tabHost = tabHost;
this.tab = tab;
tab.PropertyChanged += (_, args) =>
{
RaisePropertyChanged(args.PropertyName);
if (args.PropertyName == nameof(ToolbarCommands))
RaisePropertyChanged(nameof(ToolbarCommandsSeparatorVisibility));
};
dockCommand = new DelegateCommand(DockCommandExecute);
}
private void DockCommandExecute()
{
tabHost.DockTab(tab);
}
public void WindowClosed()
{
tabHost.UndockedTabClosed(tab);
}
}
public class DesignTimeUndockedTabHostViewModel : UndockedTabHostViewModel
{
public DesignTimeUndockedTabHostViewModel() : base(null!, new DesignTimeTab())
{
}
private class DesignTimeTab : ITab
{
public event PropertyChangedEventHandler? PropertyChanged;
public IEnumerable<TabToolbarCommand> ToolbarCommands { get; } = Array.Empty<TabToolbarCommand>();
public string Title => "Design-time tab title";
public ContentControl Content => null!;
public void Activate()
{
// Just to prevent the "PropertyChanged is never used" message
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(null));
}
public void Deactivate()
{
}
}
}
}

View File

@ -0,0 +1,44 @@
<Window x:Class="PettingZoo.UI.Tab.Undocked.UndockedTabHostWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:undocked="clr-namespace:PettingZoo.UI.Tab.Undocked"
xmlns:svgc="http://sharpvectors.codeplex.com/svgc/"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance undocked:DesignTimeUndockedTabHostViewModel, IsDesignTimeCreatable=True}"
Title="{Binding Title}"
Height="450"
Width="800"
WindowStyle="ThreeDBorderWindow">
<DockPanel>
<ToolBar DockPanel.Dock="Top" ToolBarTray.IsLocked="True">
<Button Command="{Binding DockCommand}">
<StackPanel Orientation="Horizontal">
<Image Source="{svgc:SvgImage Source=/Images/Dock.svg, AppName=PettingZoo}" Width="16" Height="16" Style="{StaticResource ToolbarIcon}"/>
<TextBlock Margin="3,0,0,0" Text="{x:Static undocked:UndockedTabHostStrings.CommandDock}" />
</StackPanel>
</Button>
<Separator Style="{StaticResource {x:Static ToolBar.SeparatorStyleKey}}" Visibility="{Binding ToolbarCommandsSeparatorVisibility}" />
<ItemsControl ItemsSource="{Binding ToolbarCommands}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Command="{Binding Command}" Style="{DynamicResource {x:Static ToolBar.ButtonStyleKey}}">
<StackPanel Orientation="Horizontal">
<Image Source="{Binding Icon}" Width="16" Height="16" Style="{StaticResource ToolbarIcon}"/>
<TextBlock Margin="3,0,0,0" Text="{Binding Caption}" />
</StackPanel>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ToolBar>
<ContentControl Content="{Binding Content}" />
</DockPanel>
</Window>

View File

@ -0,0 +1,34 @@
using System.Windows;
namespace PettingZoo.UI.Tab.Undocked
{
/// <summary>
/// Interaction logic for UndockedTabHostWindow.xaml
/// </summary>
public partial class UndockedTabHostWindow
{
public static UndockedTabHostWindow Create(ITabHost tabHost, ITab tab, double width, double height)
{
var viewModel = new UndockedTabHostViewModel(tabHost, tab);
var window = new UndockedTabHostWindow(viewModel)
{
Width = width,
Height = height
};
return window;
}
public UndockedTabHostWindow(UndockedTabHostViewModel viewModel)
{
DataContext = viewModel;
InitializeComponent();
Closed += (_, _) =>
{
viewModel.WindowClosed();
};
}
}
}

View File

@ -12,7 +12,6 @@ namespace PettingZoo.UI.Tab
{
public string Title => getTitle(viewModel);
public ContentControl Content { get; }
public ICommand CloseTabCommand { get; }
public IEnumerable<TabToolbarCommand> ToolbarCommands => viewModel is ITabToolbarCommands tabToolbarCommands
? tabToolbarCommands.ToolbarCommands
@ -25,14 +24,13 @@ namespace PettingZoo.UI.Tab
private readonly Func<TViewModel, string> getTitle;
public ViewTab(ICommand closeTabCommand, TView view, TViewModel viewModel, Expression<Func<TViewModel, string>> title)
public ViewTab(TView view, TViewModel viewModel, Expression<Func<TViewModel, string>> title)
{
if (title.Body is not MemberExpression titleMemberExpression)
throw new ArgumentException(@"Invalid expression type, expected viewModel => viewModel.TitlePropertyName", nameof(title));
var titlePropertyName = titleMemberExpression.Member.Name;
CloseTabCommand = closeTabCommand;
this.viewModel = viewModel;
getTitle = title.Compile();
Content = view;

View File

@ -8,13 +8,11 @@ namespace PettingZoo.UI.Tab
public class ViewTabFactory : ITabFactory
{
private readonly ITabHost tabHost;
private readonly ICommand closeTabCommand;
public ViewTabFactory(ITabHost tabHost, ICommand closeTabCommand)
public ViewTabFactory(ITabHost tabHost)
{
this.tabHost = tabHost;
this.closeTabCommand = closeTabCommand;
}
@ -22,7 +20,6 @@ namespace PettingZoo.UI.Tab
{
var viewModel = new SubscriberViewModel(tabHost, this, connection, subscriber);
return new ViewTab<SubscriberView, SubscriberViewModel>(
closeTabCommand,
new SubscriberView(viewModel),
viewModel,
vm => vm.Title);
@ -33,7 +30,6 @@ namespace PettingZoo.UI.Tab
{
var viewModel = new PublisherViewModel(tabHost, this, connection, fromReceivedMessage);
return new ViewTab<PublisherView, PublisherViewModel>(
closeTabCommand,
new PublisherView(viewModel),
viewModel,
vm => vm.Title);