mirror of
https://github.com/egmont11/NetworkDiagram.git
synced 2026-07-24 07:09:43 +02:00
Compare commits
13
Commits
v1.1EN
...
b54df1fd4c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b54df1fd4c | ||
|
|
5efa49584d | ||
|
|
c2e1649387 | ||
|
|
70ca51d9ef | ||
|
|
8a57680b0e | ||
|
|
fadca1bd6e | ||
|
|
9a97079945 | ||
|
|
2bd215c91a | ||
|
|
b77d2f12aa | ||
|
|
e46f38d153 | ||
|
|
9e88ce1cee | ||
|
|
f409ac732d | ||
|
|
bcf2e89e6d |
+16
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AvaloniaProject">
|
||||
<option name="projectPerEditor">
|
||||
<map>
|
||||
<entry key="NetworkDiagram/EditDeviceWindow.axaml" value="NetworkDiagram/NetworkDiagram.csproj" />
|
||||
<entry key="NetworkDiagram/ExportOptionsWindow.axaml" value="NetworkDiagram/NetworkDiagram.csproj" />
|
||||
<entry key="NetworkDiagram/Localization/Strings.cs.axaml" value="NetworkDiagram/NetworkDiagram.csproj" />
|
||||
<entry key="NetworkDiagram/Localization/Strings.en.axaml" value="NetworkDiagram/NetworkDiagram.csproj" />
|
||||
<entry key="NetworkDiagram/MainWindow.axaml" value="NetworkDiagram/NetworkDiagram.csproj" />
|
||||
<entry key="NetworkDiagramAvalonia/App.axaml" value="NetworkDiagramAvalonia/NetworkDiagramAvalonia.csproj" />
|
||||
<entry key="NetworkDiagramAvalonia/Views/MainWindow.axaml" value="NetworkDiagramAvalonia/NetworkDiagramAvalonia.csproj" />
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="LocationsWithSilentDeleteHolder">
|
||||
<option name="locations">
|
||||
<list>
|
||||
<option value="$PROJECT_DIR$/NetworkDiagram/bin/Release/net10.0-windows/win-x64/publish" />
|
||||
<option value="$PROJECT_DIR$/NetworkDiagram/bin/Release/net10.0/linux-x64/publish" />
|
||||
<option value="$PROJECT_DIR$/NetworkDiagram/bin/Release/net10.0/linux-arm64/publish" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,88 @@
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="NetworkDiagram.App"
|
||||
RequestedThemeVariant="Light">
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
|
||||
<!-- Custom Styles -->
|
||||
<Style Selector="Button.HeaderButtonStyle">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="Padding" Value="8,4"/>
|
||||
<Setter Property="Margin" Value="4"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border x:Name="border"
|
||||
Background="{TemplateBinding Background}"
|
||||
CornerRadius="4">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" Content="{TemplateBinding Content}" Margin="8,4"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style Selector="Button.HeaderButtonStyle:pointerover /template/ Border#border">
|
||||
<Setter Property="Background" Value="#20FFFFFF"/>
|
||||
</Style>
|
||||
<Style Selector="Button.HeaderButtonStyle:pressed /template/ Border#border">
|
||||
<Setter Property="Background" Value="#40FFFFFF"/>
|
||||
</Style>
|
||||
|
||||
<Style Selector="Button.ModernButtonStyle">
|
||||
<Setter Property="Background" Value="{DynamicResource AccentBlue}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextLight}"/>
|
||||
<Setter Property="Padding" Value="12,6"/>
|
||||
<Setter Property="Margin" Value="4"/>
|
||||
<Setter Property="CornerRadius" Value="4"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
</Style>
|
||||
|
||||
<Style Selector="Button.SecondaryButtonStyle">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource PrimaryBlue}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource PrimaryBlue}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="12,6"/>
|
||||
<Setter Property="Margin" Value="4"/>
|
||||
<Setter Property="CornerRadius" Value="4"/>
|
||||
</Style>
|
||||
|
||||
<Style Selector="ListBox.SidebarListBoxStyle">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
</Style>
|
||||
<Style Selector="ListBox.SidebarListBoxStyle ListBoxItem">
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="Margin" Value="4"/>
|
||||
<Setter Property="CornerRadius" Value="6"/>
|
||||
</Style>
|
||||
</Application.Styles>
|
||||
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceInclude Source="/Localization/Strings.en.axaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<!-- Color Palette -->
|
||||
<Color x:Key="PrimaryBlueColor">#004578</Color>
|
||||
<Color x:Key="AccentBlueColor">#0078D7</Color>
|
||||
<Color x:Key="LightBlueColor">#E1EEFA</Color>
|
||||
<Color x:Key="GridBlueColor">#D0E2F2</Color>
|
||||
<Color x:Key="BgWhiteColor">#F5F9FF</Color>
|
||||
<Color x:Key="TextDarkColor">#1B1B1B</Color>
|
||||
<Color x:Key="TextLightColor">#FFFFFF</Color>
|
||||
<Color x:Key="BorderGrayColor">#CCCCCC</Color>
|
||||
|
||||
<SolidColorBrush x:Key="PrimaryBlue" Color="{StaticResource PrimaryBlueColor}"/>
|
||||
<SolidColorBrush x:Key="AccentBlue" Color="{StaticResource AccentBlueColor}"/>
|
||||
<SolidColorBrush x:Key="LightBlue" Color="{StaticResource LightBlueColor}"/>
|
||||
<SolidColorBrush x:Key="GridBlue" Color="{StaticResource GridBlueColor}"/>
|
||||
<SolidColorBrush x:Key="BgWhite" Color="{StaticResource BgWhiteColor}"/>
|
||||
<SolidColorBrush x:Key="TextDark" Color="{StaticResource TextDarkColor}"/>
|
||||
<SolidColorBrush x:Key="TextLight" Color="{StaticResource TextLightColor}"/>
|
||||
<SolidColorBrush x:Key="BorderGray" Color="{StaticResource BorderGrayColor}"/>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@@ -0,0 +1,23 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace NetworkDiagram;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
desktop.MainWindow = new MainWindow();
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<Application x:Class="NetworkDiagram.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:NetworkDiagram"
|
||||
StartupUri="MainWindow.xaml">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="Localization/Strings.en.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@@ -1,12 +0,0 @@
|
||||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.Windows;
|
||||
|
||||
namespace NetworkDiagram;
|
||||
|
||||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using System.Windows;
|
||||
|
||||
[assembly: ThemeInfo(
|
||||
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
||||
//(used if a resource is not found in the page,
|
||||
// or application resource dictionaries)
|
||||
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||
//(used if a resource is not found in the page,
|
||||
// app, or any theme specific resource dictionaries)
|
||||
)]
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
@@ -0,0 +1,26 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="NetworkDiagram.EditDeviceWindow"
|
||||
Title="{DynamicResource EditTitle}" Height="380" Width="420"
|
||||
WindowStartupLocation="CenterOwner" CanResize="False"
|
||||
Background="{DynamicResource BgWhite}"
|
||||
Icon="/Assets/router.jpg">
|
||||
<Grid Margin="25" RowDefinitions="Auto,Auto,Auto,Auto,Auto,*,Auto">
|
||||
|
||||
<TextBlock Text="{DynamicResource DeviceNameLabel}" Margin="0,0,0,8" FontWeight="SemiBold" Foreground="{DynamicResource PrimaryBlue}"/>
|
||||
<TextBox x:Name="NameBox" Grid.Row="1" Margin="0,0,0,15"/>
|
||||
|
||||
<TextBlock Grid.Row="2" Text="{DynamicResource DescLabel}" Margin="0,0,0,8" FontWeight="SemiBold" Foreground="{DynamicResource PrimaryBlue}"/>
|
||||
<TextBox x:Name="DescBox" Grid.Row="3" Margin="0,0,0,15"
|
||||
AcceptsReturn="True" VerticalContentAlignment="Top" MinHeight="60"/>
|
||||
|
||||
<TextBlock Grid.Row="4" Text="{DynamicResource IpLabel}" Margin="0,0,0,8" FontWeight="SemiBold" Foreground="{DynamicResource PrimaryBlue}"/>
|
||||
<TextBox x:Name="IpBox" Grid.Row="5" Margin="0,0,0,25"
|
||||
AcceptsReturn="True" VerticalContentAlignment="Top" MinHeight="60"/>
|
||||
|
||||
<StackPanel Grid.Row="6" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button Content="{DynamicResource CancelBtn}" Classes="SecondaryButtonStyle" Click="Cancel_Click" Width="90" Margin="0,0,12,0"/>
|
||||
<Button Content="{DynamicResource SaveDialogBtn}" Classes="ModernButtonStyle" Click="Save_Click" Width="90"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,41 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using NetworkDiagram.Models;
|
||||
|
||||
namespace NetworkDiagram
|
||||
{
|
||||
public partial class EditDeviceWindow : Window
|
||||
{
|
||||
private readonly PlacedDevice _device;
|
||||
|
||||
public EditDeviceWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
_device = new PlacedDevice(); // Should not be used but needed for designer
|
||||
}
|
||||
|
||||
public EditDeviceWindow(PlacedDevice device)
|
||||
{
|
||||
InitializeComponent();
|
||||
_device = device;
|
||||
NameBox.Text = device.Name;
|
||||
DescBox.Text = device.Description;
|
||||
IpBox.Text = string.Join(Environment.NewLine, device.IpAddresses);
|
||||
}
|
||||
|
||||
private void Save_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_device.Name = NameBox.Text ?? string.Empty;
|
||||
_device.Description = DescBox.Text ?? string.Empty;
|
||||
_device.IpAddresses = (IpBox.Text ?? string.Empty)
|
||||
.Split(new[] { Environment.NewLine, "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.ToList();
|
||||
Close(true);
|
||||
}
|
||||
|
||||
private void Cancel_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
Close(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<Window x:Class="NetworkDiagram.EditDeviceWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="{DynamicResource EditTitle}" Height="350" Width="400"
|
||||
WindowStartupLocation="CenterOwner" ResizeMode="NoResize"
|
||||
Background="#F5F9FF">
|
||||
<Grid Margin="20">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Text="{DynamicResource DeviceNameLabel}" Margin="0,0,0,5"/>
|
||||
<TextBox x:Name="NameBox" Grid.Row="1" Margin="0,0,0,15" Padding="5"/>
|
||||
|
||||
<TextBlock Grid.Row="2" Text="{DynamicResource IpLabel}" Margin="0,0,0,5"/>
|
||||
<TextBox x:Name="IpBox" Grid.Row="3" Margin="0,0,0,15" Padding="5"
|
||||
AcceptsReturn="True" VerticalScrollBarVisibility="Auto"/>
|
||||
|
||||
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button Content="{DynamicResource CancelBtn}" Click="Cancel_Click" Width="80" Margin="0,0,10,0" Padding="5"/>
|
||||
<Button Content="{DynamicResource SaveDialogBtn}" Click="Save_Click" Width="80" Padding="5" Background="#0078D7" Foreground="White"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -1,34 +0,0 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using NetworkDiagram.Models;
|
||||
|
||||
namespace NetworkDiagram
|
||||
{
|
||||
public partial class EditDeviceWindow : Window
|
||||
{
|
||||
private PlacedDevice _device;
|
||||
|
||||
public EditDeviceWindow(PlacedDevice device)
|
||||
{
|
||||
InitializeComponent();
|
||||
_device = device;
|
||||
NameBox.Text = device.Name;
|
||||
IpBox.Text = string.Join(Environment.NewLine, device.IpAddresses);
|
||||
}
|
||||
|
||||
private void Save_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_device.Name = NameBox.Text;
|
||||
_device.IpAddresses = IpBox.Text.Split(new[] { Environment.NewLine, "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries).ToList();
|
||||
DialogResult = true;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void Cancel_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DialogResult = false;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="NetworkDiagram.ExportOptionsWindow"
|
||||
Title="{DynamicResource ExportOptionsTitle}" Width="400" Height="180"
|
||||
WindowStartupLocation="CenterOwner" CanResize="False"
|
||||
Background="{DynamicResource BgWhite}">
|
||||
<StackPanel Margin="20" Spacing="20">
|
||||
<TextBlock Text="{DynamicResource ExportWhiteBgMsg}" TextWrapping="Wrap" HorizontalAlignment="Center" TextAlignment="Center"/>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="20">
|
||||
<Button Content="Yes (White)" Click="White_Click" Width="100" Classes="ModernButtonStyle"/>
|
||||
<Button Content="No (Transparent)" Click="Transparent_Click" Width="140" Classes="SecondaryButtonStyle"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,23 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
namespace NetworkDiagram
|
||||
{
|
||||
public partial class ExportOptionsWindow : Window
|
||||
{
|
||||
public ExportOptionsWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void White_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
Close(true);
|
||||
}
|
||||
|
||||
private void Transparent_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
Close(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<ResourceDictionary xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<x:String x:Key="AppTitle">Editor síťových diagramů</x:String>
|
||||
<x:String x:Key="NewBtn">Nový</x:String>
|
||||
<x:String x:Key="SaveBtn">Uložit</x:String>
|
||||
<x:String x:Key="LoadBtn">Načíst</x:String>
|
||||
<x:String x:Key="ExportBtn">Exportovat PNG</x:String>
|
||||
<x:String x:Key="WireBtn">Kabelové připojení</x:String>
|
||||
<x:String x:Key="WifiBtn">Bezdrátové (WiFi)</x:String>
|
||||
<x:String x:Key="AddTextBtn">Přidat text</x:String>
|
||||
<x:String x:Key="DevicesHeader">Zařízení</x:String>
|
||||
|
||||
<!-- Edit Dialog -->
|
||||
<x:String x:Key="EditTitle">Upravit Zařízení</x:String>
|
||||
<x:String x:Key="DeviceNameLabel">Název zařízení:</x:String>
|
||||
<x:String x:Key="DescLabel">Popis:</x:String>
|
||||
<x:String x:Key="IpLabel">IP Adresy (jedna na řádek):</x:String>
|
||||
<x:String x:Key="CancelBtn">Zrušit</x:String>
|
||||
<x:String x:Key="SaveDialogBtn">Uložit</x:String>
|
||||
|
||||
<!-- Messages -->
|
||||
<x:String x:Key="ExportWhiteBgMsg">Chcete bílé pozadí? (Ne = průhlednost)</x:String>
|
||||
<x:String x:Key="ExportOptionsTitle">Možnosti exportu</x:String>
|
||||
<x:String x:Key="ExportSuccess">Export byl úspěšný!</x:String>
|
||||
<x:String x:Key="EmptyDiagramMsg">Diagram je prázdný.</x:String>
|
||||
</ResourceDictionary>
|
||||
@@ -1,26 +0,0 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:system="clr-namespace:System;assembly=mscorlib">
|
||||
<system:String x:Key="AppTitle">Editor síťových diagramů</system:String>
|
||||
<system:String x:Key="NewBtn">Nový</system:String>
|
||||
<system:String x:Key="SaveBtn">Uložit</system:String>
|
||||
<system:String x:Key="LoadBtn">Načíst</system:String>
|
||||
<system:String x:Key="ExportBtn">Exportovat PNG</system:String>
|
||||
<system:String x:Key="WireBtn">Kabelové připojení</system:String>
|
||||
<system:String x:Key="WifiBtn">Bezdrátové (WiFi)</system:String>
|
||||
<system:String x:Key="AddTextBtn">Přidat text</system:String>
|
||||
<system:String x:Key="DevicesHeader">Zařízení</system:String>
|
||||
|
||||
<!-- Edit Dialog -->
|
||||
<system:String x:Key="EditTitle">Upravit zařízení</system:String>
|
||||
<system:String x:Key="DeviceNameLabel">Název zařízení:</system:String>
|
||||
<system:String x:Key="IpLabel">IP adresy (jedna na řádek):</system:String>
|
||||
<system:String x:Key="CancelBtn">Zrušit</system:String>
|
||||
<system:String x:Key="SaveDialogBtn">Uložit</system:String>
|
||||
|
||||
<!-- Messages -->
|
||||
<system:String x:Key="ExportWhiteBgMsg">Chcete bílé pozadí? (Ne = průhlednost)</system:String>
|
||||
<system:String x:Key="ExportOptionsTitle">Možnosti exportu</system:String>
|
||||
<system:String x:Key="ExportSuccess">Export byl úspěšný!</system:String>
|
||||
<system:String x:Key="EmptyDiagramMsg">Diagram je prázdný.</system:String>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,26 @@
|
||||
<ResourceDictionary xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<x:String x:Key="AppTitle">Network Diagram Studio</x:String>
|
||||
<x:String x:Key="NewBtn">New</x:String>
|
||||
<x:String x:Key="SaveBtn">Save</x:String>
|
||||
<x:String x:Key="LoadBtn">Load</x:String>
|
||||
<x:String x:Key="ExportBtn">Export PNG</x:String>
|
||||
<x:String x:Key="WireBtn">Wire Connection</x:String>
|
||||
<x:String x:Key="WifiBtn">Wifi Connection</x:String>
|
||||
<x:String x:Key="AddTextBtn">Add Text</x:String>
|
||||
<x:String x:Key="DevicesHeader">Devices</x:String>
|
||||
|
||||
<!-- Edit Dialog -->
|
||||
<x:String x:Key="EditTitle">Edit Device</x:String>
|
||||
<x:String x:Key="DeviceNameLabel">Device Name:</x:String>
|
||||
<x:String x:Key="DescLabel">Description:</x:String>
|
||||
<x:String x:Key="IpLabel">IP Addresses (one per line):</x:String>
|
||||
<x:String x:Key="CancelBtn">Cancel</x:String>
|
||||
<x:String x:Key="SaveDialogBtn">Save</x:String>
|
||||
|
||||
<!-- Messages -->
|
||||
<x:String x:Key="ExportWhiteBgMsg">Do you want a white background? (No will result in transparency)</x:String>
|
||||
<x:String x:Key="ExportOptionsTitle">Export Options</x:String>
|
||||
<x:String x:Key="ExportSuccess">Export successful!</x:String>
|
||||
<x:String x:Key="EmptyDiagramMsg">Diagram is empty.</x:String>
|
||||
</ResourceDictionary>
|
||||
@@ -1,26 +0,0 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:system="clr-namespace:System;assembly=mscorlib">
|
||||
<system:String x:Key="AppTitle">Network Diagram Studio</system:String>
|
||||
<system:String x:Key="NewBtn">New</system:String>
|
||||
<system:String x:Key="SaveBtn">Save</system:String>
|
||||
<system:String x:Key="LoadBtn">Load</system:String>
|
||||
<system:String x:Key="ExportBtn">Export PNG</system:String>
|
||||
<system:String x:Key="WireBtn">Wire Connection</system:String>
|
||||
<system:String x:Key="WifiBtn">Wifi Connection</system:String>
|
||||
<system:String x:Key="AddTextBtn">Add Text</system:String>
|
||||
<system:String x:Key="DevicesHeader">Devices</system:String>
|
||||
|
||||
<!-- Edit Dialog -->
|
||||
<system:String x:Key="EditTitle">Edit Device</system:String>
|
||||
<system:String x:Key="DeviceNameLabel">Device Name:</system:String>
|
||||
<system:String x:Key="IpLabel">IP Addresses (one per line):</system:String>
|
||||
<system:String x:Key="CancelBtn">Cancel</system:String>
|
||||
<system:String x:Key="SaveDialogBtn">Save</system:String>
|
||||
|
||||
<!-- Messages -->
|
||||
<system:String x:Key="ExportWhiteBgMsg">Do you want a white background? (No will result in transparency)</system:String>
|
||||
<system:String x:Key="ExportOptionsTitle">Export Options</system:String>
|
||||
<system:String x:Key="ExportSuccess">Export successful!</system:String>
|
||||
<system:String x:Key="EmptyDiagramMsg">Diagram is empty.</system:String>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,112 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
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:models="clr-namespace:NetworkDiagram.Models"
|
||||
mc:Ignorable="d" d:DesignWidth="1100" d:DesignHeight="750"
|
||||
x:Class="NetworkDiagram.MainWindow"
|
||||
Title="{DynamicResource AppTitle}"
|
||||
Background="{DynamicResource BgWhite}"
|
||||
Icon="/Assets/router.jpg"
|
||||
KeyDown="Window_KeyDown">
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="240"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="60"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Header -->
|
||||
<Border Grid.Row="0" Grid.ColumnSpan="2" Background="{DynamicResource PrimaryBlue}" ZIndex="10">
|
||||
<Border.BoxShadow>
|
||||
<BoxShadows>0 2 10 0 #4D000000</BoxShadows>
|
||||
</Border.BoxShadow>
|
||||
<DockPanel Margin="15,0">
|
||||
<StackPanel Orientation="Horizontal" DockPanel.Dock="Left" VerticalAlignment="Center">
|
||||
<TextBlock Text="{DynamicResource AppTitle}" Foreground="White" FontSize="20" FontWeight="Bold" Margin="0,0,30,0" VerticalAlignment="Center"/>
|
||||
|
||||
<Button Content="{DynamicResource NewBtn}" Classes="HeaderButtonStyle" Click="NewDiagram_Click"/>
|
||||
<Button Content="{DynamicResource SaveBtn}" Classes="HeaderButtonStyle" Click="SaveDiagram_Click"/>
|
||||
<Button Content="{DynamicResource LoadBtn}" Classes="HeaderButtonStyle" Click="LoadDiagram_Click"/>
|
||||
<Button Content="{DynamicResource ExportBtn}" Classes="HeaderButtonStyle" Click="ExportPng_Click"/>
|
||||
|
||||
<Rectangle Width="1" Height="24" Fill="White" Opacity="0.3" Margin="15,0"/>
|
||||
|
||||
<Button Content="{DynamicResource WireBtn}" Classes="HeaderButtonStyle" Click="WireTool_Click"/>
|
||||
<Button Content="{DynamicResource WifiBtn}" Classes="HeaderButtonStyle" Click="WifiTool_Click"/>
|
||||
<Button Content="{DynamicResource AddTextBtn}" Classes="HeaderButtonStyle" Click="AddText_Click"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||
<ComboBox x:Name="LangCombo" Width="80" Height="34" SelectionChanged="LangCombo_SelectionChanged" VerticalContentAlignment="Center">
|
||||
<ComboBoxItem Tag="en">EN</ComboBoxItem>
|
||||
<ComboBoxItem Tag="cs">CZ</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<Border Grid.Row="1" Grid.Column="0" Background="{DynamicResource LightBlue}" BorderBrush="{DynamicResource GridBlue}" BorderThickness="0,0,1,0">
|
||||
<DockPanel Margin="10,15">
|
||||
<TextBlock Text="{DynamicResource DevicesHeader}" DockPanel.Dock="Top" FontSize="18" FontWeight="Bold" Margin="10,0,10,15" Foreground="{DynamicResource PrimaryBlue}"/>
|
||||
<ListBox x:Name="ToolboxList" Classes="SidebarListBoxStyle"
|
||||
PointerMoved="Toolbox_PointerMoved">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:DeviceTemplate">
|
||||
<Border Background="White" CornerRadius="8" Padding="12" BorderBrush="{DynamicResource GridBlue}" BorderThickness="1">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Image Source="{Binding IconPath, Converter={x:Static models:ImageConverter.Instance}}" Width="36" Height="36" Margin="0,0,12,0"/>
|
||||
<TextBlock Text="{Binding Name}" VerticalAlignment="Center" FontSize="14" FontWeight="SemiBold" Foreground="{DynamicResource TextDark}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Canvas Area -->
|
||||
<Panel Grid.Row="1" Grid.Column="1" x:Name="CanvasViewport" ClipToBounds="True" Background="Transparent"
|
||||
DragDrop.AllowDrop="True"
|
||||
PointerPressed="Viewport_PointerPressed"
|
||||
PointerMoved="Viewport_PointerMoved"
|
||||
PointerReleased="Viewport_PointerReleased"
|
||||
PointerWheelChanged="Viewport_PointerWheelChanged">
|
||||
|
||||
<Canvas x:Name="DiagramCanvas" Width="40000" Height="40000"
|
||||
PointerPressed="DiagramCanvas_PointerPressed"
|
||||
PointerMoved="DiagramCanvas_PointerMoved"
|
||||
PointerReleased="DiagramCanvas_PointerReleased">
|
||||
<Canvas.RenderTransform>
|
||||
<TransformGroup>
|
||||
<ScaleTransform ScaleX="1" ScaleY="1"/>
|
||||
<TranslateTransform X="0" Y="0"/>
|
||||
</TransformGroup>
|
||||
</Canvas.RenderTransform>
|
||||
<Canvas.Background>
|
||||
<DrawingBrush TileMode="Tile" SourceRect="0,0,40,40" DestinationRect="0,0,40,40">
|
||||
<DrawingBrush.Drawing>
|
||||
<GeometryDrawing>
|
||||
<GeometryDrawing.Geometry>
|
||||
<PathGeometry Figures="M 0 40 L 0 0 L 40 0" />
|
||||
</GeometryDrawing.Geometry>
|
||||
<GeometryDrawing.Pen>
|
||||
<Pen Brush="{DynamicResource GridBlue}" Thickness="0.8"/>
|
||||
</GeometryDrawing.Pen>
|
||||
</GeometryDrawing>
|
||||
</DrawingBrush.Drawing>
|
||||
</DrawingBrush>
|
||||
</Canvas.Background>
|
||||
|
||||
<!-- Selection rectangle visual -->
|
||||
<Rectangle x:Name="SelectionRect" Stroke="{DynamicResource AccentBlue}" StrokeThickness="1.5" StrokeDashArray="3,3"
|
||||
Fill="#220078D7" IsVisible="False" ZIndex="9999" IsHitTestVisible="False"/>
|
||||
</Canvas>
|
||||
</Panel>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,879 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Controls.Shapes;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Markup.Xaml.Styling;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Threading;
|
||||
using Avalonia.VisualTree;
|
||||
using NetworkDiagram.Models;
|
||||
using Avalonia.Controls.Templates;
|
||||
|
||||
namespace NetworkDiagram
|
||||
{
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private List<DeviceTemplate> _deviceTemplates = [];
|
||||
private Diagram _currentDiagram = new();
|
||||
private bool _langComboReady;
|
||||
|
||||
// Selection & Dragging
|
||||
private readonly HashSet<Control> _selectedElements = [];
|
||||
private Point _dragStartPoint;
|
||||
private bool _isDraggingDevices;
|
||||
private bool _isSelectingArea;
|
||||
private Point _selectionStartPoint;
|
||||
|
||||
private ConnectionType? _activeTool;
|
||||
private PlacedDevice? _firstDeviceForConnection;
|
||||
|
||||
private readonly Dictionary<Connection, Line> _connectionLines = new();
|
||||
private readonly Dictionary<PlacedDevice, Control> _deviceElements = new();
|
||||
|
||||
// Panning and Zooming
|
||||
private Point _lastPanPoint;
|
||||
private bool _isPanning;
|
||||
|
||||
private TranslateTransform CanvasTranslate = null!;
|
||||
private ScaleTransform CanvasScale = null!;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
LangCombo.SelectedIndex = 0;
|
||||
_langComboReady = true;
|
||||
LoadTemplates();
|
||||
this.Opened += (s, e) => {
|
||||
if (DiagramCanvas.RenderTransform is TransformGroup group)
|
||||
{
|
||||
CanvasScale = group.Children.OfType<ScaleTransform>().First();
|
||||
CanvasTranslate = group.Children.OfType<TranslateTransform>().First();
|
||||
}
|
||||
CenterView();
|
||||
};
|
||||
|
||||
AddHandler(DragDrop.DragOverEvent, Viewport_DragOver);
|
||||
AddHandler(DragDrop.DropEvent, Viewport_Drop);
|
||||
|
||||
// Use Tunneling for drag start to avoid ListBoxItem selection interference
|
||||
ToolboxList.AddHandler(PointerPressedEvent, Toolbox_PointerPressed, RoutingStrategies.Tunnel);
|
||||
}
|
||||
|
||||
// The logical center of the 40000x40000 canvas
|
||||
private const double CanvasCenterX = 20000;
|
||||
private const double CanvasCenterY = 20000;
|
||||
|
||||
private void CenterView()
|
||||
{
|
||||
if (CanvasTranslate == null || CanvasScale == null) return;
|
||||
CanvasScale.ScaleX = 1.0;
|
||||
CanvasScale.ScaleY = 1.0;
|
||||
// Změna na 0,0 aby levý horní roh plátna odpovídal levému hornímu rohu viewportu
|
||||
CanvasTranslate.X = 0;
|
||||
CanvasTranslate.Y = 0;
|
||||
}
|
||||
|
||||
#region Panning and Zooming
|
||||
private void Viewport_PointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
var properties = e.GetCurrentPoint(CanvasViewport).Properties;
|
||||
if (properties.IsMiddleButtonPressed || properties.IsRightButtonPressed)
|
||||
{
|
||||
_isPanning = true;
|
||||
_lastPanPoint = e.GetPosition(CanvasViewport);
|
||||
e.Pointer.Capture(CanvasViewport);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void Viewport_PointerMoved(object? sender, PointerEventArgs e)
|
||||
{
|
||||
if (_isPanning)
|
||||
{
|
||||
Point currentPoint = e.GetPosition(CanvasViewport);
|
||||
double deltaX = currentPoint.X - _lastPanPoint.X;
|
||||
double deltaY = currentPoint.Y - _lastPanPoint.Y;
|
||||
|
||||
CanvasTranslate.X += deltaX;
|
||||
CanvasTranslate.Y += deltaY;
|
||||
|
||||
_lastPanPoint = currentPoint;
|
||||
}
|
||||
}
|
||||
|
||||
private void Viewport_PointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
var props = e.GetCurrentPoint(CanvasViewport).Properties;
|
||||
if (!props.IsMiddleButtonPressed && !props.IsRightButtonPressed)
|
||||
{
|
||||
_isPanning = false;
|
||||
e.Pointer.Capture(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void Viewport_PointerWheelChanged(object? sender, PointerWheelEventArgs e)
|
||||
{
|
||||
if (CanvasScale == null || CanvasTranslate == null) return;
|
||||
double zoom = e.Delta.Y > 0 ? 1.1 : 0.9;
|
||||
|
||||
double newScale = CanvasScale.ScaleX * zoom;
|
||||
if (newScale < 0.05 || newScale > 10) return;
|
||||
|
||||
Point mousePos = e.GetPosition(DiagramCanvas);
|
||||
|
||||
CanvasScale.ScaleX = newScale;
|
||||
CanvasScale.ScaleY = newScale;
|
||||
|
||||
Point newMousePos = e.GetPosition(DiagramCanvas);
|
||||
CanvasTranslate.X += (newMousePos.X - mousePos.X) * CanvasScale.ScaleX;
|
||||
CanvasTranslate.Y += (newMousePos.Y - mousePos.Y) * CanvasScale.ScaleY;
|
||||
}
|
||||
#endregion
|
||||
|
||||
private void LoadTemplates()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists("devices.json")) return;
|
||||
var json = File.ReadAllText("devices.json");
|
||||
_deviceTemplates = JsonSerializer.Deserialize<List<DeviceTemplate>>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? [];
|
||||
ToolboxList.ItemsSource = _deviceTemplates;
|
||||
}
|
||||
catch (Exception ex) { Console.WriteLine($"Error loading templates: {ex.Message}"); }
|
||||
}
|
||||
|
||||
private void LangCombo_SelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (!_langComboReady) return;
|
||||
if (LangCombo.SelectedItem is ComboBoxItem { Tag: string lang })
|
||||
{
|
||||
var app = Application.Current;
|
||||
if (app == null) return;
|
||||
|
||||
var mergedDicts = app.Resources.MergedDictionaries;
|
||||
var oldDict = mergedDicts.OfType<ResourceInclude>().FirstOrDefault(ri => ri.Source?.OriginalString.Contains("Localization/Strings.") == true);
|
||||
if (oldDict != null) mergedDicts.Remove(oldDict);
|
||||
|
||||
mergedDicts.Add(new ResourceInclude(new Uri("avares://NetworkDiagram/App.axaml"))
|
||||
{
|
||||
Source = new Uri($"avares://NetworkDiagram/Localization/Strings.{lang}.axaml")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#region Drag and Drop (Toolbox to Canvas)
|
||||
private Point _toolboxDragStart;
|
||||
private DeviceTemplate? _toolboxPendingTemplate;
|
||||
|
||||
private void Toolbox_PointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
var item = (e.Source as Control)?.FindAncestorOfType<ListBoxItem>();
|
||||
if (item?.Content is DeviceTemplate template)
|
||||
{
|
||||
_toolboxDragStart = e.GetPosition(null);
|
||||
_toolboxPendingTemplate = template;
|
||||
}
|
||||
}
|
||||
|
||||
private async void Toolbox_PointerMoved(object? sender, PointerEventArgs e)
|
||||
{
|
||||
if (_toolboxPendingTemplate != null && e.GetCurrentPoint(null).Properties.IsLeftButtonPressed)
|
||||
{
|
||||
var pos = e.GetPosition(null);
|
||||
var diff = _toolboxDragStart - pos;
|
||||
if (Math.Abs(diff.X) > 5 || Math.Abs(diff.Y) > 5)
|
||||
{
|
||||
var template = _toolboxPendingTemplate;
|
||||
_toolboxPendingTemplate = null;
|
||||
|
||||
var data = new DataObject();
|
||||
data.Set("DeviceTemplate", template);
|
||||
await DragDrop.DoDragDrop(e, data, DragDropEffects.Copy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Viewport_DragOver(object? sender, DragEventArgs e)
|
||||
{
|
||||
if (e.Data.Contains("DeviceTemplate"))
|
||||
e.DragEffects = DragDropEffects.Copy;
|
||||
else
|
||||
e.DragEffects = DragDropEffects.None;
|
||||
}
|
||||
|
||||
private void Viewport_Drop(object? sender, DragEventArgs e)
|
||||
{
|
||||
if (e.Data.Get("DeviceTemplate") is DeviceTemplate template)
|
||||
{
|
||||
var dropPoint = e.GetPosition(DiagramCanvas);
|
||||
AddDeviceToCanvas(template, dropPoint.X, dropPoint.Y);
|
||||
}
|
||||
}
|
||||
|
||||
private void DiagramCanvas_PointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
var props = e.GetCurrentPoint(DiagramCanvas).Properties;
|
||||
if (e.Source == DiagramCanvas && props.IsLeftButtonPressed)
|
||||
{
|
||||
ClearSelection();
|
||||
_isSelectingArea = true;
|
||||
_selectionStartPoint = e.GetPosition(DiagramCanvas);
|
||||
|
||||
Canvas.SetLeft(SelectionRect, _selectionStartPoint.X);
|
||||
Canvas.SetTop(SelectionRect, _selectionStartPoint.Y);
|
||||
SelectionRect.Width = 0;
|
||||
SelectionRect.Height = 0;
|
||||
SelectionRect.IsVisible = true;
|
||||
e.Pointer.Capture(DiagramCanvas);
|
||||
}
|
||||
}
|
||||
|
||||
private void DiagramCanvas_PointerMoved(object? sender, PointerEventArgs e)
|
||||
{
|
||||
var currentPoint = e.GetPosition(DiagramCanvas);
|
||||
|
||||
if (_isSelectingArea)
|
||||
{
|
||||
var x = Math.Min(_selectionStartPoint.X, currentPoint.X);
|
||||
var y = Math.Min(_selectionStartPoint.Y, currentPoint.Y);
|
||||
var w = Math.Abs(_selectionStartPoint.X - currentPoint.X);
|
||||
var h = Math.Abs(_selectionStartPoint.Y - currentPoint.Y);
|
||||
|
||||
Canvas.SetLeft(SelectionRect, x);
|
||||
Canvas.SetTop(SelectionRect, y);
|
||||
SelectionRect.Width = w;
|
||||
SelectionRect.Height = h;
|
||||
|
||||
var selectionBounds = new Rect(x, y, w, h);
|
||||
foreach (var entry in _deviceElements)
|
||||
{
|
||||
var border = entry.Value;
|
||||
var elementBounds = new Rect(Canvas.GetLeft(border), Canvas.GetTop(border), border.Bounds.Width, border.Bounds.Height);
|
||||
if (selectionBounds.Intersects(elementBounds))
|
||||
{
|
||||
if (!_selectedElements.Contains(border)) SelectElement(border);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_selectedElements.Contains(border)) DeselectElement(border);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (_isDraggingDevices)
|
||||
{
|
||||
var deltaX = currentPoint.X - _dragStartPoint.X;
|
||||
var deltaY = currentPoint.Y - _dragStartPoint.Y;
|
||||
|
||||
foreach (var element in _selectedElements)
|
||||
{
|
||||
if (element.Tag is PlacedDevice device)
|
||||
{
|
||||
device.X += deltaX;
|
||||
device.Y += deltaY;
|
||||
}
|
||||
}
|
||||
_dragStartPoint = currentPoint;
|
||||
}
|
||||
}
|
||||
|
||||
private void DiagramCanvas_PointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
StopDragging();
|
||||
e.Pointer.Capture(null);
|
||||
}
|
||||
#endregion
|
||||
|
||||
private Point GetVisibleCanvasCenter()
|
||||
{
|
||||
if (CanvasTranslate == null || CanvasScale == null) return new Point(CanvasCenterX, CanvasCenterY);
|
||||
double vw = CanvasViewport.Bounds.Width > 0 ? CanvasViewport.Bounds.Width : 1000;
|
||||
double vh = CanvasViewport.Bounds.Height > 0 ? CanvasViewport.Bounds.Height : 700;
|
||||
return new Point(
|
||||
(vw / 2 - CanvasTranslate.X) / CanvasScale.ScaleX,
|
||||
(vh / 2 - CanvasTranslate.Y) / CanvasScale.ScaleY
|
||||
);
|
||||
}
|
||||
|
||||
private void AddDeviceToCanvas(DeviceTemplate template, double x, double y)
|
||||
{
|
||||
var placed = new PlacedDevice { Name = template.Name, TemplateName = template.Name, IconPath = template.IconPath, X = x, Y = y };
|
||||
_currentDiagram.Devices.Add(placed);
|
||||
RenderDevice(placed);
|
||||
}
|
||||
|
||||
private void RenderDevice(PlacedDevice device)
|
||||
{
|
||||
var container = new Border
|
||||
{
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(12),
|
||||
Background = Brushes.White,
|
||||
BorderBrush = (IBrush)Application.Current!.FindResource("GridBlue")!,
|
||||
BorderThickness = new Thickness(1),
|
||||
MinWidth = 100,
|
||||
MinHeight = 80,
|
||||
MaxWidth = 180,
|
||||
Tag = device,
|
||||
ZIndex = 10,
|
||||
DataContext = device
|
||||
};
|
||||
|
||||
var stack = new StackPanel { Spacing = 4 };
|
||||
|
||||
var image = new Image { Width = 64, Height = 64, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center };
|
||||
image.Bind(Image.SourceProperty, new Avalonia.Data.Binding("IconPath") { Converter = ImageConverter.Instance });
|
||||
stack.Children.Add(image);
|
||||
|
||||
var primaryBlue = Application.Current!.FindResource("PrimaryBlue");
|
||||
var nameText = new TextBlock { FontWeight = FontWeight.Bold, FontSize = 12, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, TextWrapping = TextWrapping.Wrap, TextAlignment = TextAlignment.Center };
|
||||
if (primaryBlue is IBrush brush) nameText.Foreground = brush;
|
||||
nameText.Bind(TextBlock.TextProperty, new Avalonia.Data.Binding("Name"));
|
||||
stack.Children.Add(nameText);
|
||||
|
||||
var descText = new TextBlock { FontSize = 11, FontStyle = FontStyle.Italic, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, TextWrapping = TextWrapping.Wrap, TextAlignment = TextAlignment.Center, Opacity = 0.8 };
|
||||
descText.Bind(TextBlock.TextProperty, new Avalonia.Data.Binding("Description"));
|
||||
descText.Bind(TextBlock.IsVisibleProperty, new Avalonia.Data.Binding("Description") { Converter = Avalonia.Data.Converters.StringConverters.IsNotNullOrEmpty });
|
||||
stack.Children.Add(descText);
|
||||
|
||||
var ips = new ItemsControl();
|
||||
ips.Bind(ItemsControl.ItemsSourceProperty, new Avalonia.Data.Binding("IpAddresses"));
|
||||
|
||||
var accentBlue = Application.Current!.FindResource("AccentBlue");
|
||||
ips.ItemTemplate = new FuncDataTemplate<string>((val, _) => {
|
||||
var tb = new TextBlock { Text = val, FontSize = 10, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, TextWrapping = TextWrapping.Wrap, Opacity = 0.8 };
|
||||
if (accentBlue is IBrush b) tb.Foreground = b;
|
||||
return tb;
|
||||
});
|
||||
|
||||
stack.Children.Add(ips);
|
||||
|
||||
container.Child = stack;
|
||||
|
||||
Canvas.SetLeft(container, device.X);
|
||||
Canvas.SetTop(container, device.Y);
|
||||
|
||||
device.PropertyChanged += (s, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(PlacedDevice.X)) Canvas.SetLeft(container, device.X);
|
||||
if (e.PropertyName == nameof(PlacedDevice.Y)) Canvas.SetTop(container, device.Y);
|
||||
};
|
||||
|
||||
container.PointerPressed += (s, e) =>
|
||||
{
|
||||
if (_activeTool != null)
|
||||
{
|
||||
HandleConnectionTool(device, container);
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!e.KeyModifiers.HasFlag(KeyModifiers.Control))
|
||||
{
|
||||
if (!_selectedElements.Contains(container))
|
||||
{
|
||||
ClearSelection();
|
||||
SelectElement(container);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_selectedElements.Contains(container)) DeselectElement(container);
|
||||
else SelectElement(container);
|
||||
}
|
||||
|
||||
_isDraggingDevices = true;
|
||||
_dragStartPoint = e.GetPosition(DiagramCanvas);
|
||||
e.Pointer.Capture(DiagramCanvas);
|
||||
e.Handled = true;
|
||||
|
||||
if (e.ClickCount == 2)
|
||||
{
|
||||
StopDragging();
|
||||
EditDevice(device);
|
||||
}
|
||||
};
|
||||
|
||||
_deviceElements[device] = container;
|
||||
DiagramCanvas.Children.Add(container);
|
||||
}
|
||||
|
||||
private void StopDragging()
|
||||
{
|
||||
_isDraggingDevices = false;
|
||||
_isSelectingArea = false;
|
||||
SelectionRect.IsVisible = false;
|
||||
}
|
||||
|
||||
private void SelectElement(Control element)
|
||||
{
|
||||
_selectedElements.Add(element);
|
||||
if (element is Border b)
|
||||
{
|
||||
var accentBlue = Application.Current!.FindResource("AccentBlue");
|
||||
if (accentBlue is IBrush brush) b.BorderBrush = brush;
|
||||
b.Background = new SolidColorBrush(Color.FromArgb(50, 0, 120, 215));
|
||||
b.BorderThickness = new Thickness(2);
|
||||
}
|
||||
else if (element is Line l)
|
||||
{
|
||||
l.StrokeThickness = 5;
|
||||
l.Opacity = 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
private void DeselectElement(Control element)
|
||||
{
|
||||
_selectedElements.Remove(element);
|
||||
if (element is Border b)
|
||||
{
|
||||
b.BorderBrush = (IBrush)Application.Current!.FindResource("GridBlue")!;
|
||||
b.Background = Brushes.White;
|
||||
b.BorderThickness = new Thickness(1);
|
||||
}
|
||||
else if (element is Line l)
|
||||
{
|
||||
l.StrokeThickness = 3;
|
||||
l.Opacity = 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearSelection()
|
||||
{
|
||||
foreach (var el in _selectedElements.ToList()) DeselectElement(el);
|
||||
}
|
||||
|
||||
private void HandleConnectionTool(PlacedDevice device, Control element)
|
||||
{
|
||||
if (_firstDeviceForConnection == null)
|
||||
{
|
||||
_firstDeviceForConnection = device;
|
||||
element.Opacity = 0.5;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_firstDeviceForConnection != device)
|
||||
{
|
||||
var conn = new Connection { StartDevice = _firstDeviceForConnection, EndDevice = device, Type = _activeTool!.Value };
|
||||
_currentDiagram.Connections.Add(conn);
|
||||
RenderConnection(conn);
|
||||
}
|
||||
ResetConnectionTool();
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetConnectionTool()
|
||||
{
|
||||
_firstDeviceForConnection = null;
|
||||
_activeTool = null;
|
||||
foreach (var child in _deviceElements.Values) child.Opacity = 1.0;
|
||||
}
|
||||
|
||||
private void RenderConnection(Connection conn)
|
||||
{
|
||||
var line = new Line
|
||||
{
|
||||
Stroke = conn.Type == ConnectionType.Wifi ? Brushes.DeepSkyBlue : Brushes.SlateGray,
|
||||
StrokeThickness = 3,
|
||||
Tag = conn,
|
||||
ZIndex = 0
|
||||
};
|
||||
if (conn.Type == ConnectionType.Wifi) line.StrokeDashArray = new AvaloniaList<double>(new[] { 2.0, 2.0 });
|
||||
|
||||
void UpdatePos()
|
||||
{
|
||||
if (!_deviceElements.TryGetValue(conn.StartDevice, out var startEl) || !_deviceElements.TryGetValue(conn.EndDevice, out var endEl)) return;
|
||||
double sw = startEl.Bounds.Width > 0 ? startEl.Bounds.Width : 120;
|
||||
double sh = startEl.Bounds.Height > 0 ? startEl.Bounds.Height : 100;
|
||||
double ew = endEl.Bounds.Width > 0 ? endEl.Bounds.Width : 120;
|
||||
double eh = endEl.Bounds.Height > 0 ? endEl.Bounds.Height : 100;
|
||||
|
||||
line.StartPoint = new Point(conn.StartDevice.X + sw / 2, conn.StartDevice.Y + sh / 2);
|
||||
line.EndPoint = new Point(conn.EndDevice.X + ew / 2, conn.EndDevice.Y + eh / 2);
|
||||
}
|
||||
|
||||
UpdatePos();
|
||||
conn.StartDevice.PropertyChanged += (s, e) => { if (e.PropertyName == "X" || e.PropertyName == "Y") UpdatePos(); };
|
||||
conn.EndDevice.PropertyChanged += (s, e) => { if (e.PropertyName == "X" || e.PropertyName == "Y") UpdatePos(); };
|
||||
|
||||
// Re-calculate line endpoints once device bounds are known after first layout pass
|
||||
// Unsubscribe immediately after first call to avoid repeated triggers
|
||||
if (_deviceElements.TryGetValue(conn.StartDevice, out var startElem))
|
||||
{
|
||||
EventHandler? handler = null;
|
||||
handler = (s, e) => { startElem.LayoutUpdated -= handler; UpdatePos(); };
|
||||
startElem.LayoutUpdated += handler;
|
||||
}
|
||||
if (_deviceElements.TryGetValue(conn.EndDevice, out var endElem) && endElem != startElem)
|
||||
{
|
||||
EventHandler? handler = null;
|
||||
handler = (s, e) => { endElem.LayoutUpdated -= handler; UpdatePos(); };
|
||||
endElem.LayoutUpdated += handler;
|
||||
}
|
||||
|
||||
line.PointerPressed += (s, e) => {
|
||||
if (!e.KeyModifiers.HasFlag(KeyModifiers.Control)) ClearSelection();
|
||||
SelectElement(line);
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
_connectionLines[conn] = line;
|
||||
DiagramCanvas.Children.Insert(0, line);
|
||||
}
|
||||
|
||||
private async void EditDevice(PlacedDevice device)
|
||||
{
|
||||
var dialog = new EditDeviceWindow(device);
|
||||
await dialog.ShowDialog(this);
|
||||
|
||||
if (_deviceElements.TryGetValue(device, out var elem))
|
||||
{
|
||||
var attached = _currentDiagram.Connections.Where(c => c.StartDevice == device || c.EndDevice == device).ToList();
|
||||
foreach(var c in attached) if (_connectionLines.TryGetValue(c, out var l))
|
||||
{
|
||||
double sw = _deviceElements[c.StartDevice].Bounds.Width > 0 ? _deviceElements[c.StartDevice].Bounds.Width : 120;
|
||||
double sh = _deviceElements[c.StartDevice].Bounds.Height > 0 ? _deviceElements[c.StartDevice].Bounds.Height : 100;
|
||||
double ew = _deviceElements[c.EndDevice].Bounds.Width > 0 ? _deviceElements[c.EndDevice].Bounds.Width : 120;
|
||||
double eh = _deviceElements[c.EndDevice].Bounds.Height > 0 ? _deviceElements[c.EndDevice].Bounds.Height : 100;
|
||||
l.StartPoint = new Point(c.StartDevice.X + sw / 2, c.StartDevice.Y + sh / 2);
|
||||
l.EndPoint = new Point(c.EndDevice.X + ew / 2, c.EndDevice.Y + eh / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Window_KeyDown(object? sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Delete && _selectedElements.Count > 0)
|
||||
{
|
||||
foreach (var el in _selectedElements.ToList())
|
||||
{
|
||||
if (el.Tag is Connection conn && el is Line line)
|
||||
{
|
||||
_currentDiagram.Connections.Remove(conn);
|
||||
_connectionLines.Remove(conn);
|
||||
DiagramCanvas.Children.Remove(line);
|
||||
}
|
||||
else if (el.Tag is PlacedDevice device)
|
||||
{
|
||||
_currentDiagram.Devices.Remove(device);
|
||||
_deviceElements.Remove(device);
|
||||
var toRemove = _currentDiagram.Connections.Where(c => c.StartDevice == device || c.EndDevice == device).ToList();
|
||||
foreach (var c in toRemove)
|
||||
{
|
||||
if (_connectionLines.TryGetValue(c, out var l)) DiagramCanvas.Children.Remove(l);
|
||||
_currentDiagram.Connections.Remove(c);
|
||||
_connectionLines.Remove(c);
|
||||
}
|
||||
DiagramCanvas.Children.Remove(el);
|
||||
}
|
||||
}
|
||||
_selectedElements.Clear();
|
||||
}
|
||||
else if (e.Key == Key.Escape)
|
||||
{
|
||||
ResetConnectionTool(); StopDragging(); ClearSelection();
|
||||
}
|
||||
else if (e.Key == Key.Home)
|
||||
{
|
||||
if (_currentDiagram.Devices.Count > 0) CenterOnContent();
|
||||
else CenterView();
|
||||
}
|
||||
}
|
||||
|
||||
#region Toolbar Events
|
||||
private void NewDiagram_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_currentDiagram = new Diagram();
|
||||
DiagramCanvas.Children.Clear();
|
||||
DiagramCanvas.Children.Add(SelectionRect);
|
||||
_connectionLines.Clear();
|
||||
_deviceElements.Clear();
|
||||
ClearSelection();
|
||||
CenterView();
|
||||
}
|
||||
|
||||
private async void SaveDiagram_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var topLevel = GetTopLevel(this);
|
||||
if (topLevel == null) return;
|
||||
|
||||
var file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||
{
|
||||
Title = "Save Network Diagram",
|
||||
FileTypeChoices = new[] { new FilePickerFileType("Network Diagram") { Patterns = new[] { "*.ndjson" } } }
|
||||
});
|
||||
|
||||
if (file != null)
|
||||
{
|
||||
var model = new DiagramSaveModel {
|
||||
Devices = _currentDiagram.Devices,
|
||||
Connections = _currentDiagram.Connections.Select(c => new ConnectionSaveModel {
|
||||
StartIndex = _currentDiagram.Devices.IndexOf(c.StartDevice),
|
||||
EndIndex = _currentDiagram.Devices.IndexOf(c.EndDevice),
|
||||
Type = c.Type
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
using var stream = await file.OpenWriteAsync();
|
||||
await JsonSerializer.SerializeAsync(stream, model, new JsonSerializerOptions { WriteIndented = true });
|
||||
}
|
||||
}
|
||||
|
||||
private void CenterOnContent()
|
||||
{
|
||||
if (CanvasTranslate == null || CanvasScale == null || _currentDiagram.Devices.Count == 0) return;
|
||||
|
||||
var devices = _currentDiagram.Devices;
|
||||
double minX = devices.Min(d => d.X);
|
||||
double minY = devices.Min(d => d.Y);
|
||||
double maxX = devices.Max(d => d.X) + 160;
|
||||
double maxY = devices.Max(d => d.Y) + 120;
|
||||
|
||||
double contentWidth = maxX - minX;
|
||||
double contentHeight = maxY - minY;
|
||||
double contentCenterX = (minX + maxX) / 2.0;
|
||||
double contentCenterY = (minY + maxY) / 2.0;
|
||||
|
||||
double vw = CanvasViewport.Bounds.Width > 0 ? CanvasViewport.Bounds.Width : (this.Bounds.Width > 240 ? this.Bounds.Width - 240 : 860);
|
||||
double vh = CanvasViewport.Bounds.Height > 0 ? CanvasViewport.Bounds.Height : (this.Bounds.Height > 60 ? this.Bounds.Height - 60 : 600);
|
||||
|
||||
// Zoom-to-fit: scale content to fill 85% of viewport, capped between 0.05 and 2.0
|
||||
double scaleToFit = Math.Min((vw * 0.85) / contentWidth, (vh * 0.85) / contentHeight);
|
||||
double newScale = Math.Clamp(scaleToFit, 0.05, 2.0);
|
||||
|
||||
CanvasScale.ScaleX = newScale;
|
||||
CanvasScale.ScaleY = newScale;
|
||||
|
||||
CanvasTranslate.X = vw / 2.0 - contentCenterX * newScale;
|
||||
CanvasTranslate.Y = vh / 2.0 - contentCenterY * newScale;
|
||||
}
|
||||
|
||||
private async void LoadDiagram_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var topLevel = GetTopLevel(this);
|
||||
if (topLevel == null) return;
|
||||
|
||||
var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = "Open Network Diagram",
|
||||
FileTypeFilter = new[] { new FilePickerFileType("Network Diagram") { Patterns = new[] { "*.ndjson" } } }
|
||||
});
|
||||
|
||||
if (files.Count > 0)
|
||||
{
|
||||
try {
|
||||
using var stream = await files[0].OpenReadAsync();
|
||||
var model = await JsonSerializer.DeserializeAsync<DiagramSaveModel>(stream, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
|
||||
if (model == null || model.Devices == null) return;
|
||||
|
||||
// Clear canvas without calling CenterView (we'll center on content after load)
|
||||
_currentDiagram = new Diagram();
|
||||
DiagramCanvas.Children.Clear();
|
||||
DiagramCanvas.Children.Add(SelectionRect);
|
||||
_connectionLines.Clear();
|
||||
_deviceElements.Clear();
|
||||
ClearSelection();
|
||||
|
||||
// Load devices exactly as saved — no normalization
|
||||
_currentDiagram = new Diagram { Devices = model.Devices };
|
||||
|
||||
foreach (var device in _currentDiagram.Devices)
|
||||
{
|
||||
var template = _deviceTemplates.FirstOrDefault(t => t.Name == device.TemplateName);
|
||||
if (template != null) device.IconPath = template.IconPath;
|
||||
else if (device.TemplateName == "Note") device.IconPath = "";
|
||||
RenderDevice(device);
|
||||
}
|
||||
|
||||
if (model.Connections != null)
|
||||
{
|
||||
foreach (var cModel in model.Connections)
|
||||
{
|
||||
if (cModel.StartIndex >= 0 && cModel.StartIndex < _currentDiagram.Devices.Count &&
|
||||
cModel.EndIndex >= 0 && cModel.EndIndex < _currentDiagram.Devices.Count)
|
||||
{
|
||||
var conn = new Connection {
|
||||
StartDevice = _currentDiagram.Devices[cModel.StartIndex],
|
||||
EndDevice = _currentDiagram.Devices[cModel.EndIndex],
|
||||
Type = cModel.Type
|
||||
};
|
||||
_currentDiagram.Connections.Add(conn);
|
||||
RenderConnection(conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for Avalonia to measure/arrange the newly added controls
|
||||
await Dispatcher.UIThread.InvokeAsync(() => { }, DispatcherPriority.Loaded);
|
||||
await Dispatcher.UIThread.InvokeAsync(() => { }, DispatcherPriority.Render);
|
||||
} catch (Exception ex) {
|
||||
Console.WriteLine("Load failed: " + ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WireTool_Click(object? sender, RoutedEventArgs e) => _activeTool = ConnectionType.Wire;
|
||||
private void WifiTool_Click(object? sender, RoutedEventArgs e) => _activeTool = ConnectionType.Wifi;
|
||||
private void AddText_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var center = GetVisibleCanvasCenter();
|
||||
AddDeviceToCanvas(new DeviceTemplate { Name = "Note", IconPath = "" }, center.X - 60, center.Y - 40);
|
||||
}
|
||||
|
||||
private async void ExportPng_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_currentDiagram.Devices.Count == 0) return;
|
||||
|
||||
var prompt = new ExportOptionsWindow();
|
||||
var result = await prompt.ShowDialog<bool?>(this);
|
||||
if (result == null) return;
|
||||
|
||||
var topLevel = GetTopLevel(this);
|
||||
if (topLevel == null) return;
|
||||
|
||||
var file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||
{
|
||||
Title = "Export PNG",
|
||||
FileTypeChoices = new[] { new FilePickerFileType("PNG Image") { Patterns = new[] { "*.png" } } }
|
||||
});
|
||||
|
||||
if (file != null)
|
||||
{
|
||||
double minX = _currentDiagram.Devices.Min(d => d.X) - 50;
|
||||
double minY = _currentDiagram.Devices.Min(d => d.Y) - 50;
|
||||
double maxX = _currentDiagram.Devices.Max(d => d.X) + 210;
|
||||
double maxY = _currentDiagram.Devices.Max(d => d.Y) + 160;
|
||||
|
||||
int width = (int)(maxX - minX);
|
||||
int height = (int)(maxY - minY);
|
||||
if (width <= 0 || height <= 0) return;
|
||||
|
||||
// Create a temporary canvas for rendering to avoid live UI transform issues
|
||||
var tempCanvas = new Canvas {
|
||||
Width = width,
|
||||
Height = height,
|
||||
Background = result.Value ? Brushes.White : Brushes.Transparent
|
||||
};
|
||||
|
||||
// Create a container to host temp canvas for rendering
|
||||
var container = new Panel();
|
||||
container.Children.Add(tempCanvas);
|
||||
|
||||
var rtb = new RenderTargetBitmap(new PixelSize(width, height), new Vector(96, 96));
|
||||
|
||||
// 1. Draw connections
|
||||
foreach (var conn in _currentDiagram.Connections)
|
||||
{
|
||||
if (_connectionLines.TryGetValue(conn, out var line))
|
||||
{
|
||||
var tempLine = new Line {
|
||||
Stroke = line.Stroke,
|
||||
StrokeThickness = line.StrokeThickness,
|
||||
StrokeDashArray = line.StrokeDashArray,
|
||||
StartPoint = new Point(line.StartPoint.X - minX, line.StartPoint.Y - minY),
|
||||
EndPoint = new Point(line.EndPoint.X - minX, line.EndPoint.Y - minY)
|
||||
};
|
||||
tempCanvas.Children.Add(tempLine);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Draw devices
|
||||
foreach (var device in _currentDiagram.Devices)
|
||||
{
|
||||
if (_deviceElements.TryGetValue(device, out var original))
|
||||
{
|
||||
// Create a clone-like visual
|
||||
var clone = new Border {
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(12),
|
||||
Background = Brushes.White,
|
||||
BorderBrush = (IBrush)Application.Current!.FindResource("GridBlue")!,
|
||||
BorderThickness = new Thickness(1),
|
||||
MinWidth = 100,
|
||||
MinHeight = 80,
|
||||
MaxWidth = 180
|
||||
};
|
||||
|
||||
var stack = new StackPanel { Spacing = 4 };
|
||||
|
||||
var img = new Image { Width = 64, Height = 64 };
|
||||
if (ImageConverter.Instance.Convert(device.IconPath, typeof(IImage), null, System.Globalization.CultureInfo.CurrentCulture) is IImage iimg)
|
||||
{
|
||||
img.Source = iimg;
|
||||
}
|
||||
|
||||
var primaryBlue = Application.Current!.FindResource("PrimaryBlue");
|
||||
var txt = new TextBlock {
|
||||
Text = device.Name,
|
||||
FontWeight = FontWeight.Bold,
|
||||
FontSize = 12,
|
||||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
TextAlignment = TextAlignment.Center
|
||||
};
|
||||
if (primaryBlue is IBrush brush) txt.Foreground = brush;
|
||||
|
||||
stack.Children.Add(img);
|
||||
stack.Children.Add(txt);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(device.Description))
|
||||
{
|
||||
var desc = new TextBlock {
|
||||
Text = device.Description,
|
||||
FontSize = 11,
|
||||
FontStyle = FontStyle.Italic,
|
||||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
TextAlignment = TextAlignment.Center,
|
||||
Opacity = 0.8,
|
||||
Foreground = Brushes.Black
|
||||
};
|
||||
stack.Children.Add(desc);
|
||||
}
|
||||
|
||||
var accentBlue = Application.Current!.FindResource("AccentBlue");
|
||||
foreach (var ip in device.IpAddresses)
|
||||
{
|
||||
var tb = new TextBlock {
|
||||
Text = ip,
|
||||
FontSize = 10,
|
||||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
if (accentBlue is IBrush b) tb.Foreground = b;
|
||||
stack.Children.Add(tb);
|
||||
}
|
||||
|
||||
clone.Child = stack;
|
||||
Canvas.SetLeft(clone, device.X - minX);
|
||||
Canvas.SetTop(clone, device.Y - minY);
|
||||
tempCanvas.Children.Add(clone);
|
||||
}
|
||||
}
|
||||
|
||||
// Important: Ensure layout of temp visual
|
||||
container.Measure(new Size(width, height));
|
||||
container.Arrange(new Rect(0, 0, width, height));
|
||||
|
||||
await Task.Delay(100);
|
||||
rtb.Render(container);
|
||||
|
||||
using var stream = await file.OpenWriteAsync();
|
||||
rtb.Save(stream);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
<Window x:Class="NetworkDiagram.MainWindow"
|
||||
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:local="clr-namespace:NetworkDiagram"
|
||||
xmlns:models="clr-namespace:NetworkDiagram.Models"
|
||||
mc:Ignorable="d"
|
||||
Title="{DynamicResource AppTitle}" Height="700" Width="1080"
|
||||
Background="#F5F9FF"
|
||||
KeyDown="Window_KeyDown">
|
||||
|
||||
<Window.Resources>
|
||||
<SolidColorBrush x:Key="HeaderBlue" Color="#004578"/>
|
||||
<SolidColorBrush x:Key="AccentBlue" Color="#0078D7"/>
|
||||
<SolidColorBrush x:Key="SidebarBlue" Color="#E1EEFA"/>
|
||||
<SolidColorBrush x:Key="CanvasGridBlue" Color="#D0E2F2"/>
|
||||
|
||||
<Style x:Key="ModernButton" TargetType="Button">
|
||||
<Setter Property="Background" Value="{StaticResource AccentBlue}"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="Padding" Value="10,5"/>
|
||||
<Setter Property="Margin" Value="5"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="220"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border Grid.Row="0" Grid.ColumnSpan="2" Background="{StaticResource HeaderBlue}" Padding="10">
|
||||
<DockPanel>
|
||||
<StackPanel Orientation="Horizontal" DockPanel.Dock="Left">
|
||||
<TextBlock Text="{DynamicResource AppTitle}" Foreground="White" FontSize="18" FontWeight="Bold" VerticalAlignment="Center" Margin="0,0,20,0"/>
|
||||
<Button Content="{DynamicResource NewBtn}" Style="{StaticResource ModernButton}" Click="NewDiagram_Click"/>
|
||||
<Button Content="{DynamicResource SaveBtn}" Style="{StaticResource ModernButton}" Click="SaveDiagram_Click"/>
|
||||
<Button Content="{DynamicResource LoadBtn}" Style="{StaticResource ModernButton}" Click="LoadDiagram_Click"/>
|
||||
<Button Content="{DynamicResource ExportBtn}" Style="{StaticResource ModernButton}" Click="ExportPng_Click"/>
|
||||
<Separator Margin="10,0" Background="White" Opacity="0.5"/>
|
||||
<Button Content="{DynamicResource WireBtn}" Style="{StaticResource ModernButton}" Click="WireTool_Click"/>
|
||||
<Button Content="{DynamicResource WifiBtn}" Style="{StaticResource ModernButton}" Click="WifiTool_Click"/>
|
||||
<Button Content="{DynamicResource AddTextBtn}" Style="{StaticResource ModernButton}" Click="AddText_Click"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<ComboBox x:Name="LangCombo" VerticalAlignment="Center" Width="80" SelectionChanged="LangCombo_SelectionChanged" Margin="10,0,0,0">
|
||||
<ComboBoxItem Tag="en" IsSelected="True">EN</ComboBoxItem>
|
||||
<ComboBoxItem Tag="cs">CZ</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Row="1" Grid.Column="0" Background="{StaticResource SidebarBlue}" BorderBrush="{StaticResource CanvasGridBlue}" BorderThickness="0,0,1,0">
|
||||
<DockPanel>
|
||||
<TextBlock Text="{DynamicResource DevicesHeader}" DockPanel.Dock="Top" FontSize="16" FontWeight="SemiBold" Margin="10" Foreground="{StaticResource HeaderBlue}"/>
|
||||
<ListBox x:Name="ToolboxList" Background="Transparent" BorderThickness="0" Margin="5"
|
||||
PreviewMouseDown="Toolbox_PreviewMouseDown">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border BorderBrush="{StaticResource AccentBlue}" BorderThickness="1" CornerRadius="4" Margin="5" Padding="10" Background="White">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Image Source="{Binding IconPath}" Width="32" Height="32" Margin="0,0,10,0"/>
|
||||
<TextBlock Text="{Binding Name}" VerticalAlignment="Center" FontWeight="Medium"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<ScrollViewer Grid.Row="1" Grid.Column="1" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
|
||||
<Canvas x:Name="DiagramCanvas" Width="2000" Height="2000" AllowDrop="True"
|
||||
Drop="DiagramCanvas_Drop" DragOver="DiagramCanvas_DragOver"
|
||||
MouseDown="DiagramCanvas_MouseDown"
|
||||
MouseMove="DiagramCanvas_MouseMove"
|
||||
MouseUp="DiagramCanvas_MouseUp">
|
||||
<Canvas.Background>
|
||||
<VisualBrush TileMode="Tile" Viewport="0,0,40,40" ViewportUnits="Absolute">
|
||||
<VisualBrush.Visual>
|
||||
<Path Data="M 0 40 L 0 0 L 40 0" Stroke="{StaticResource CanvasGridBlue}" StrokeThickness="0.5"/>
|
||||
</VisualBrush.Visual>
|
||||
</VisualBrush>
|
||||
</Canvas.Background>
|
||||
|
||||
<Canvas.Resources>
|
||||
<DataTemplate x:Key="DeviceTemplate">
|
||||
<Border x:Name="DeviceBorder" BorderBrush="Transparent" BorderThickness="2" CornerRadius="4"
|
||||
Background="Transparent" Padding="5" MaxWidth="150">
|
||||
<StackPanel>
|
||||
<Image Source="{Binding IconPath}" Width="48" Height="48" HorizontalAlignment="Center">
|
||||
<Image.Style>
|
||||
<Style TargetType="Image">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="Source" Value="{x:Null}">
|
||||
<Setter Property="Height" Value="0"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Image.Style>
|
||||
</Image>
|
||||
<TextBlock Text="{Binding Name}" FontWeight="Bold" HorizontalAlignment="Center"
|
||||
Foreground="#004578" TextWrapping="Wrap" TextAlignment="Center"/>
|
||||
<ItemsControl ItemsSource="{Binding IpAddresses}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding}" FontSize="10" Foreground="#0078D7"
|
||||
HorizontalAlignment="Center" TextWrapping="Wrap"/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</Canvas.Resources>
|
||||
|
||||
<!-- Selection rectangle visual -->
|
||||
<Rectangle x:Name="SelectionRect" Stroke="{StaticResource AccentBlue}" StrokeDashArray="2,2"
|
||||
Fill="#330078D7" Visibility="Collapsed" Panel.ZIndex="9999"/>
|
||||
</Canvas>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -1,561 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shapes;
|
||||
using Microsoft.Win32;
|
||||
using NetworkDiagram.Models;
|
||||
using Path = System.IO.Path;
|
||||
|
||||
namespace NetworkDiagram
|
||||
{
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private List<DeviceTemplate> _deviceTemplates = new List<DeviceTemplate>();
|
||||
private Diagram _currentDiagram = new Diagram();
|
||||
|
||||
// Selection & Dragging
|
||||
private HashSet<FrameworkElement> _selectedElements = new HashSet<FrameworkElement>();
|
||||
private Point _dragStartPoint;
|
||||
private bool _isDraggingDevices;
|
||||
private bool _isSelectingArea;
|
||||
private Point _selectionStartPoint;
|
||||
|
||||
private ConnectionType? _activeTool;
|
||||
private PlacedDevice? _firstDeviceForConnection;
|
||||
|
||||
private Dictionary<Connection, Line> _connectionLines = new Dictionary<Connection, Line>();
|
||||
private Dictionary<PlacedDevice, FrameworkElement> _deviceElements = new Dictionary<PlacedDevice, FrameworkElement>();
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
LoadTemplates();
|
||||
}
|
||||
|
||||
private void LoadTemplates()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists("devices.json"))
|
||||
{
|
||||
string json = File.ReadAllText("devices.json");
|
||||
_deviceTemplates = JsonSerializer.Deserialize<List<DeviceTemplate>>(json) ?? new List<DeviceTemplate>();
|
||||
foreach(var t in _deviceTemplates)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(t.IconPath)) t.IconPath = Path.GetFullPath(t.IconPath);
|
||||
}
|
||||
ToolboxList.ItemsSource = _deviceTemplates;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { MessageBox.Show($"Error loading templates: {ex.Message}"); }
|
||||
}
|
||||
|
||||
private string GetLocalizedString(string key) => Application.Current.Resources[key] as string ?? key;
|
||||
|
||||
private void LangCombo_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (LangCombo.SelectedItem is ComboBoxItem item && item.Tag is string lang)
|
||||
{
|
||||
var dict = new ResourceDictionary();
|
||||
dict.Source = new Uri($"Localization/Strings.{lang}.xaml", UriKind.Relative);
|
||||
|
||||
// Replace the existing localization dictionary
|
||||
var oldDict = Application.Current.Resources.MergedDictionaries.FirstOrDefault(d => d.Source != null && d.Source.OriginalString.Contains("Localization/Strings."));
|
||||
if (oldDict != null) Application.Current.Resources.MergedDictionaries.Remove(oldDict);
|
||||
Application.Current.Resources.MergedDictionaries.Add(dict);
|
||||
}
|
||||
}
|
||||
|
||||
#region Drag and Drop (Toolbox to Canvas)
|
||||
private void Toolbox_PreviewMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is ListBox listBox && listBox.SelectedItem is DeviceTemplate template)
|
||||
DragDrop.DoDragDrop(listBox, template, DragDropEffects.Copy);
|
||||
}
|
||||
|
||||
private void DiagramCanvas_DragOver(object sender, DragEventArgs e)
|
||||
{
|
||||
e.Effects = e.Data.GetDataPresent(typeof(DeviceTemplate)) ? DragDropEffects.Copy : DragDropEffects.None;
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void DiagramCanvas_Drop(object sender, DragEventArgs e)
|
||||
{
|
||||
if (e.Data.GetDataPresent(typeof(DeviceTemplate)))
|
||||
{
|
||||
var template = (DeviceTemplate)e.Data.GetData(typeof(DeviceTemplate));
|
||||
Point dropPoint = e.GetPosition(DiagramCanvas);
|
||||
AddDeviceToCanvas(template, dropPoint.X, dropPoint.Y);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
private void AddDeviceToCanvas(DeviceTemplate template, double x, double y)
|
||||
{
|
||||
var placed = new PlacedDevice { Name = template.Name, TemplateName = template.Name, IconPath = template.IconPath, X = x, Y = y };
|
||||
_currentDiagram.Devices.Add(placed);
|
||||
RenderDevice(placed);
|
||||
}
|
||||
|
||||
private void RenderDevice(PlacedDevice device)
|
||||
{
|
||||
var template = (DataTemplate)DiagramCanvas.Resources["DeviceTemplate"];
|
||||
var contentPresenter = new ContentPresenter { Content = device, ContentTemplate = template };
|
||||
var container = new Border { Child = contentPresenter, Tag = device, Background = Brushes.Transparent };
|
||||
|
||||
Canvas.SetLeft(container, device.X);
|
||||
Canvas.SetTop(container, device.Y);
|
||||
|
||||
device.PropertyChanged += (s, e) => {
|
||||
if (e.PropertyName == nameof(PlacedDevice.X)) Canvas.SetLeft(container, device.X);
|
||||
if (e.PropertyName == nameof(PlacedDevice.Y)) Canvas.SetTop(container, device.Y);
|
||||
};
|
||||
|
||||
container.MouseDown += Device_MouseDown;
|
||||
container.MouseLeftButtonDown += (s, e) => {
|
||||
if (e.ClickCount == 2) {
|
||||
StopDragging();
|
||||
EditDevice(device);
|
||||
e.Handled = true;
|
||||
}
|
||||
};
|
||||
|
||||
_deviceElements[device] = container;
|
||||
DiagramCanvas.Children.Add(container);
|
||||
}
|
||||
|
||||
private void StopDragging()
|
||||
{
|
||||
_isDraggingDevices = false;
|
||||
_isSelectingArea = false;
|
||||
SelectionRect.Visibility = Visibility.Collapsed;
|
||||
DiagramCanvas.ReleaseMouseCapture();
|
||||
}
|
||||
|
||||
private void Device_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is FrameworkElement element && element.Tag is PlacedDevice device)
|
||||
{
|
||||
if (_activeTool != null)
|
||||
{
|
||||
HandleConnectionTool(device, element);
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle Selection
|
||||
if (!Keyboard.IsKeyDown(Key.LeftCtrl) && !Keyboard.IsKeyDown(Key.RightCtrl))
|
||||
{
|
||||
if (!_selectedElements.Contains(element))
|
||||
{
|
||||
ClearSelection();
|
||||
SelectElement(element);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_selectedElements.Contains(element)) DeselectElement(element);
|
||||
else SelectElement(element);
|
||||
}
|
||||
|
||||
// Start Dragging
|
||||
_isDraggingDevices = true;
|
||||
_dragStartPoint = e.GetPosition(DiagramCanvas);
|
||||
DiagramCanvas.CaptureMouse();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectElement(FrameworkElement element)
|
||||
{
|
||||
_selectedElements.Add(element);
|
||||
element.Opacity = 0.7;
|
||||
if (element is Line l) l.StrokeThickness = 5;
|
||||
}
|
||||
|
||||
private void DeselectElement(FrameworkElement element)
|
||||
{
|
||||
_selectedElements.Remove(element);
|
||||
element.Opacity = 1.0;
|
||||
if (element is Line l) l.StrokeThickness = 3;
|
||||
}
|
||||
|
||||
private void ClearSelection()
|
||||
{
|
||||
foreach (var el in _selectedElements.ToList()) DeselectElement(el);
|
||||
}
|
||||
|
||||
#region Canvas Interaction (Selection Area & Dragging)
|
||||
private void DiagramCanvas_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.OriginalSource == DiagramCanvas)
|
||||
{
|
||||
ClearSelection();
|
||||
_isSelectingArea = true;
|
||||
_selectionStartPoint = e.GetPosition(DiagramCanvas);
|
||||
|
||||
Canvas.SetLeft(SelectionRect, _selectionStartPoint.X);
|
||||
Canvas.SetTop(SelectionRect, _selectionStartPoint.Y);
|
||||
SelectionRect.Width = 0;
|
||||
SelectionRect.Height = 0;
|
||||
SelectionRect.Visibility = Visibility.Visible;
|
||||
DiagramCanvas.CaptureMouse();
|
||||
}
|
||||
}
|
||||
|
||||
private void DiagramCanvas_MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
Point currentPoint = e.GetPosition(DiagramCanvas);
|
||||
|
||||
if (_isSelectingArea)
|
||||
{
|
||||
double x = Math.Min(_selectionStartPoint.X, currentPoint.X);
|
||||
double y = Math.Min(_selectionStartPoint.Y, currentPoint.Y);
|
||||
double w = Math.Abs(_selectionStartPoint.X - currentPoint.X);
|
||||
double h = Math.Abs(_selectionStartPoint.Y - currentPoint.Y);
|
||||
|
||||
Canvas.SetLeft(SelectionRect, x);
|
||||
Canvas.SetTop(SelectionRect, y);
|
||||
SelectionRect.Width = w;
|
||||
SelectionRect.Height = h;
|
||||
|
||||
// Real-time selection preview
|
||||
Rect selectionBounds = new Rect(x, y, w, h);
|
||||
foreach (var border in _deviceElements.Values)
|
||||
{
|
||||
Rect elementBounds = new Rect(Canvas.GetLeft(border), Canvas.GetTop(border), border.ActualWidth, border.ActualHeight);
|
||||
if (selectionBounds.IntersectsWith(elementBounds))
|
||||
{
|
||||
if (!_selectedElements.Contains(border)) SelectElement(border);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Only deselect if we are in the middle of a selection area drag
|
||||
if (_selectedElements.Contains(border)) DeselectElement(border);
|
||||
}
|
||||
}
|
||||
|
||||
// Also preview for lines
|
||||
foreach (var line in _connectionLines.Values)
|
||||
{
|
||||
Rect lineBounds = new Rect(
|
||||
Math.Min(line.X1, line.X2),
|
||||
Math.Min(line.Y1, line.Y2),
|
||||
Math.Abs(line.X1 - line.X2),
|
||||
Math.Abs(line.Y1 - line.Y2));
|
||||
|
||||
if (selectionBounds.IntersectsWith(lineBounds))
|
||||
{
|
||||
if (!_selectedElements.Contains(line)) SelectElement(line);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_selectedElements.Contains(line)) DeselectElement(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (_isDraggingDevices && e.LeftButton == MouseButtonState.Pressed)
|
||||
{
|
||||
double deltaX = currentPoint.X - _dragStartPoint.X;
|
||||
double deltaY = currentPoint.Y - _dragStartPoint.Y;
|
||||
|
||||
foreach (var element in _selectedElements)
|
||||
{
|
||||
if (element.Tag is PlacedDevice device)
|
||||
{
|
||||
device.X += deltaX;
|
||||
device.Y += deltaY;
|
||||
}
|
||||
}
|
||||
_dragStartPoint = currentPoint;
|
||||
}
|
||||
}
|
||||
|
||||
private void DiagramCanvas_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (_isSelectingArea)
|
||||
{
|
||||
Rect selectionBounds = new Rect(
|
||||
Canvas.GetLeft(SelectionRect),
|
||||
Canvas.GetTop(SelectionRect),
|
||||
SelectionRect.Width,
|
||||
SelectionRect.Height);
|
||||
|
||||
foreach (var border in _deviceElements.Values)
|
||||
{
|
||||
Rect elementBounds = new Rect(Canvas.GetLeft(border), Canvas.GetTop(border), border.ActualWidth, border.ActualHeight);
|
||||
if (selectionBounds.IntersectsWith(elementBounds)) SelectElement(border);
|
||||
}
|
||||
}
|
||||
StopDragging();
|
||||
}
|
||||
#endregion
|
||||
|
||||
private void HandleConnectionTool(PlacedDevice device, FrameworkElement element)
|
||||
{
|
||||
if (_firstDeviceForConnection == null)
|
||||
{
|
||||
_firstDeviceForConnection = device;
|
||||
element.Opacity = 0.5;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_firstDeviceForConnection != device)
|
||||
{
|
||||
var conn = new Connection { StartDevice = _firstDeviceForConnection, EndDevice = device, Type = _activeTool.Value };
|
||||
_currentDiagram.Connections.Add(conn);
|
||||
RenderConnection(conn);
|
||||
}
|
||||
ResetConnectionTool();
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetConnectionTool()
|
||||
{
|
||||
_firstDeviceForConnection = null;
|
||||
_activeTool = null;
|
||||
foreach (var child in DiagramCanvas.Children.OfType<Border>()) child.Opacity = 1.0;
|
||||
}
|
||||
|
||||
private void RenderConnection(Connection conn)
|
||||
{
|
||||
var line = new Line
|
||||
{
|
||||
Stroke = conn.Type == ConnectionType.Wifi ? Brushes.DeepSkyBlue : Brushes.SlateGray,
|
||||
StrokeThickness = 3,
|
||||
Tag = conn
|
||||
};
|
||||
if (conn.Type == ConnectionType.Wifi) line.StrokeDashArray = new DoubleCollection { 2, 2 };
|
||||
|
||||
UpdateLinePosition(conn, line);
|
||||
|
||||
conn.StartDevice.PropertyChanged += (s, e) => { if (e.PropertyName == "X" || e.PropertyName == "Y") UpdateLinePosition(conn, line); };
|
||||
conn.EndDevice.PropertyChanged += (s, e) => { if (e.PropertyName == "X" || e.PropertyName == "Y") UpdateLinePosition(conn, line); };
|
||||
|
||||
line.MouseDown += (s, e) => {
|
||||
if (!Keyboard.IsKeyDown(Key.LeftCtrl) && !Keyboard.IsKeyDown(Key.RightCtrl)) ClearSelection();
|
||||
SelectElement(line);
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
_connectionLines[conn] = line;
|
||||
DiagramCanvas.Children.Insert(0, line);
|
||||
}
|
||||
|
||||
private void UpdateLinePosition(Connection conn, Line line)
|
||||
{
|
||||
if (_deviceElements.TryGetValue(conn.StartDevice, out var startElem) &&
|
||||
_deviceElements.TryGetValue(conn.EndDevice, out var endElem))
|
||||
{
|
||||
if (startElem.ActualWidth == 0) startElem.UpdateLayout();
|
||||
if (endElem.ActualWidth == 0) endElem.UpdateLayout();
|
||||
|
||||
line.X1 = conn.StartDevice.X + (startElem.ActualWidth / 2);
|
||||
line.Y1 = conn.StartDevice.Y + 24;
|
||||
line.X2 = conn.EndDevice.X + (endElem.ActualWidth / 2);
|
||||
line.Y2 = conn.EndDevice.Y + 24;
|
||||
}
|
||||
}
|
||||
|
||||
private void Window_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Delete && _selectedElements.Count > 0)
|
||||
{
|
||||
foreach (var el in _selectedElements.ToList())
|
||||
{
|
||||
if (el is Line line && el.Tag is Connection conn)
|
||||
{
|
||||
_currentDiagram.Connections.Remove(conn);
|
||||
_connectionLines.Remove(conn);
|
||||
DiagramCanvas.Children.Remove(line);
|
||||
}
|
||||
else if (el is Border border && border.Tag is PlacedDevice device)
|
||||
{
|
||||
_currentDiagram.Devices.Remove(device);
|
||||
_deviceElements.Remove(device);
|
||||
var toRemove = _currentDiagram.Connections.Where(c => c.StartDevice == device || c.EndDevice == device).ToList();
|
||||
foreach (var c in toRemove)
|
||||
{
|
||||
if (_connectionLines.TryGetValue(c, out var l)) DiagramCanvas.Children.Remove(l);
|
||||
_currentDiagram.Connections.Remove(c);
|
||||
_connectionLines.Remove(c);
|
||||
}
|
||||
DiagramCanvas.Children.Remove(border);
|
||||
}
|
||||
}
|
||||
_selectedElements.Clear();
|
||||
}
|
||||
else if (e.Key == Key.Escape) { ResetConnectionTool(); StopDragging(); ClearSelection(); }
|
||||
}
|
||||
|
||||
private void EditDevice(PlacedDevice device)
|
||||
{
|
||||
var dialog = new EditDeviceWindow(device);
|
||||
dialog.ShowDialog();
|
||||
if (_deviceElements.TryGetValue(device, out var elem))
|
||||
{
|
||||
elem.UpdateLayout();
|
||||
var attached = _currentDiagram.Connections.Where(c => c.StartDevice == device || c.EndDevice == device).ToList();
|
||||
foreach(var c in attached) if (_connectionLines.TryGetValue(c, out var l)) UpdateLinePosition(c, l);
|
||||
}
|
||||
}
|
||||
|
||||
#region Toolbar Events
|
||||
private void NewDiagram_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_currentDiagram = new Diagram();
|
||||
DiagramCanvas.Children.Clear();
|
||||
_connectionLines.Clear();
|
||||
_deviceElements.Clear();
|
||||
ClearSelection();
|
||||
}
|
||||
|
||||
private void SaveDiagram_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var sfd = new SaveFileDialog { Filter = "Network Diagram (*.ndjson)|*.ndjson" };
|
||||
if (sfd.ShowDialog() == true)
|
||||
{
|
||||
var model = new DiagramSaveModel {
|
||||
Devices = _currentDiagram.Devices,
|
||||
Connections = _currentDiagram.Connections.Select(c => new ConnectionSaveModel {
|
||||
StartIndex = _currentDiagram.Devices.IndexOf(c.StartDevice),
|
||||
EndIndex = _currentDiagram.Devices.IndexOf(c.EndDevice),
|
||||
Type = c.Type
|
||||
}).ToList()
|
||||
};
|
||||
string json = JsonSerializer.Serialize(model, new JsonSerializerOptions { WriteIndented = true });
|
||||
File.WriteAllText(sfd.FileName, json);
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadDiagram_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var ofd = new OpenFileDialog { Filter = "Network Diagram (*.ndjson)|*.ndjson" };
|
||||
if (ofd.ShowDialog() == true)
|
||||
{
|
||||
string json = File.ReadAllText(ofd.FileName);
|
||||
var model = JsonSerializer.Deserialize<DiagramSaveModel>(json);
|
||||
if (model == null) return;
|
||||
_currentDiagram = new Diagram { Devices = model.Devices };
|
||||
|
||||
foreach (var device in _currentDiagram.Devices)
|
||||
{
|
||||
var template = _deviceTemplates.FirstOrDefault(t => t.Name == device.TemplateName);
|
||||
if (template != null) device.IconPath = template.IconPath;
|
||||
}
|
||||
|
||||
DiagramCanvas.Children.Clear();
|
||||
_connectionLines.Clear();
|
||||
_deviceElements.Clear();
|
||||
ClearSelection();
|
||||
foreach (var device in _currentDiagram.Devices) RenderDevice(device);
|
||||
foreach (var cModel in model.Connections)
|
||||
{
|
||||
var conn = new Connection { StartDevice = _currentDiagram.Devices[cModel.StartIndex], EndDevice = _currentDiagram.Devices[cModel.EndIndex], Type = cModel.Type };
|
||||
_currentDiagram.Connections.Add(conn);
|
||||
RenderConnection(conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WireTool_Click(object sender, RoutedEventArgs e) => _activeTool = ConnectionType.Wire;
|
||||
private void WifiTool_Click(object sender, RoutedEventArgs e) => _activeTool = ConnectionType.Wifi;
|
||||
private void AddText_Click(object sender, RoutedEventArgs e) => AddDeviceToCanvas(new DeviceTemplate { Name = "Note", IconPath = "" }, 100, 100);
|
||||
|
||||
private void ExportPng_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_currentDiagram.Devices.Count == 0)
|
||||
{
|
||||
MessageBox.Show(GetLocalizedString("EmptyDiagramMsg"));
|
||||
return;
|
||||
}
|
||||
|
||||
var sfd = new SaveFileDialog { Filter = "PNG Image (*.png)|*.png", Title = GetLocalizedString("ExportOptionsTitle") };
|
||||
if (sfd.ShowDialog() != true) return;
|
||||
|
||||
var result = MessageBox.Show(GetLocalizedString("ExportWhiteBgMsg"), GetLocalizedString("ExportOptionsTitle"), MessageBoxButton.YesNoCancel);
|
||||
if (result == MessageBoxResult.Cancel) return;
|
||||
|
||||
bool whiteBg = result == MessageBoxResult.Yes;
|
||||
|
||||
// 1. Find content bounds
|
||||
double minX = double.MaxValue, minY = double.MaxValue, maxX = double.MinValue, maxY = double.MinValue;
|
||||
foreach (var border in _deviceElements.Values)
|
||||
{
|
||||
double x = Canvas.GetLeft(border);
|
||||
double y = Canvas.GetTop(border);
|
||||
minX = Math.Min(minX, x);
|
||||
minY = Math.Min(minY, y);
|
||||
maxX = Math.Max(maxX, x + border.ActualWidth);
|
||||
maxY = Math.Max(maxY, y + border.ActualHeight);
|
||||
}
|
||||
foreach (var line in _connectionLines.Values)
|
||||
{
|
||||
minX = Math.Min(minX, Math.Min(line.X1, line.X2));
|
||||
minY = Math.Min(minY, Math.Min(line.Y1, line.Y2));
|
||||
maxX = Math.Max(maxX, Math.Max(line.X1, line.X2));
|
||||
maxY = Math.Max(maxY, Math.Max(line.Y1, line.Y2));
|
||||
}
|
||||
|
||||
double margin = 20;
|
||||
minX -= margin; minY -= margin; maxX += margin; maxY += margin;
|
||||
double width = Math.Max(1, maxX - minX);
|
||||
double height = Math.Max(1, maxY - minY);
|
||||
|
||||
try
|
||||
{
|
||||
RenderTargetBitmap rtb = new RenderTargetBitmap((int)width, (int)height, 96, 96, PixelFormats.Pbgra32);
|
||||
DrawingVisual dv = new DrawingVisual();
|
||||
using (DrawingContext dc = dv.RenderOpen())
|
||||
{
|
||||
if (whiteBg) dc.DrawRectangle(Brushes.White, null, new Rect(0, 0, width, height));
|
||||
dc.PushTransform(new TranslateTransform(-minX, -minY));
|
||||
|
||||
foreach (var child in DiagramCanvas.Children)
|
||||
{
|
||||
if (child == SelectionRect || !(child is Visual v) || ((UIElement)child).Visibility != Visibility.Visible) continue;
|
||||
|
||||
double left = Canvas.GetLeft((UIElement)child);
|
||||
double top = Canvas.GetTop((UIElement)child);
|
||||
|
||||
if (child is Line line)
|
||||
{
|
||||
dc.DrawLine(new Pen(line.Stroke, line.StrokeThickness) { DashStyle = new DashStyle(line.StrokeDashArray, 0) },
|
||||
new Point(line.X1, line.Y1), new Point(line.X2, line.Y2));
|
||||
}
|
||||
else if (child is FrameworkElement fe)
|
||||
{
|
||||
VisualBrush vb = new VisualBrush(fe) { Stretch = Stretch.None };
|
||||
dc.DrawRectangle(vb, null, new Rect(left, top, fe.ActualWidth, fe.ActualHeight));
|
||||
}
|
||||
}
|
||||
}
|
||||
rtb.Render(dv);
|
||||
PngBitmapEncoder encoder = new PngBitmapEncoder();
|
||||
encoder.Frames.Add(BitmapFrame.Create(rtb));
|
||||
using (var stream = File.Create(sfd.FileName)) encoder.Save(stream);
|
||||
MessageBox.Show(GetLocalizedString("ExportSuccess"));
|
||||
}
|
||||
catch (Exception ex) { MessageBox.Show($"Export failed: {ex.Message}"); }
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
public class DiagramSaveModel {
|
||||
public List<PlacedDevice> Devices { get; set; } = new();
|
||||
public List<ConnectionSaveModel> Connections { get; set; } = new();
|
||||
}
|
||||
public class ConnectionSaveModel {
|
||||
public int StartIndex { get; set; }
|
||||
public int EndIndex { get; set; }
|
||||
public ConnectionType Type { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json.Serialization;
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Platform;
|
||||
|
||||
namespace NetworkDiagram.Models
|
||||
{
|
||||
@@ -13,57 +16,92 @@ namespace NetworkDiagram.Models
|
||||
|
||||
public class DeviceTemplate
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
[JsonPropertyName("Name")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("IconPath")]
|
||||
public string IconPath { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class PlacedDevice : INotifyPropertyChanged
|
||||
{
|
||||
private string _name = string.Empty;
|
||||
private List<string> _ipAddresses = new List<string>();
|
||||
private string _annotation = string.Empty;
|
||||
private double _x;
|
||||
private double _y;
|
||||
|
||||
[JsonPropertyName("Name")]
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set { _name = value; OnPropertyChanged(); }
|
||||
set
|
||||
{
|
||||
_name = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private string _description = string.Empty;
|
||||
|
||||
[JsonPropertyName("Description")]
|
||||
public string Description
|
||||
{
|
||||
get => _description;
|
||||
set
|
||||
{
|
||||
_description = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("TemplateName")]
|
||||
public string TemplateName { get; set; } = string.Empty;
|
||||
|
||||
[JsonIgnore]
|
||||
public string IconPath { get; set; } = string.Empty;
|
||||
|
||||
private double _x;
|
||||
[JsonPropertyName("X")]
|
||||
public double X
|
||||
{
|
||||
get => _x;
|
||||
set { _x = value; OnPropertyChanged(); OnPropertyChanged(nameof(CenterX)); }
|
||||
set
|
||||
{
|
||||
_x = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(CenterX));
|
||||
}
|
||||
}
|
||||
|
||||
private double _y;
|
||||
[JsonPropertyName("Y")]
|
||||
public double Y
|
||||
{
|
||||
get => _y;
|
||||
set { _y = value; OnPropertyChanged(); OnPropertyChanged(nameof(CenterY)); }
|
||||
set
|
||||
{
|
||||
_y = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(CenterY));
|
||||
}
|
||||
}
|
||||
|
||||
private List<string> _ipAddresses = [];
|
||||
[JsonPropertyName("IpAddresses")]
|
||||
public List<string> IpAddresses
|
||||
{
|
||||
get => _ipAddresses;
|
||||
set { _ipAddresses = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public string Annotation
|
||||
set
|
||||
{
|
||||
get => _annotation;
|
||||
set { _annotation = value; OnPropertyChanged(); }
|
||||
_ipAddresses = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public double CenterX => X + 50;
|
||||
[JsonIgnore]
|
||||
public double CenterY => Y + 40;
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
@@ -79,7 +117,62 @@ namespace NetworkDiagram.Models
|
||||
|
||||
public class Diagram
|
||||
{
|
||||
public List<PlacedDevice> Devices { get; set; } = new List<PlacedDevice>();
|
||||
public List<Connection> Connections { get; set; } = new List<Connection>();
|
||||
public List<PlacedDevice> Devices { get; set; } = [];
|
||||
public List<Connection> Connections { get; set; } = [];
|
||||
}
|
||||
|
||||
public class ImageConverter : IValueConverter
|
||||
{
|
||||
public static ImageConverter Instance { get; } = new();
|
||||
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is string path && !string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (path.StartsWith("avares://"))
|
||||
return new Bitmap(AssetLoader.Open(new Uri(path)));
|
||||
|
||||
if (File.Exists(path))
|
||||
return new Bitmap(path);
|
||||
|
||||
string uriPath = path.Replace("\\", "/");
|
||||
if (!uriPath.StartsWith("Assets/")) uriPath = "Assets/" + uriPath.TrimStart('/');
|
||||
|
||||
string escapedPath = uriPath.Replace(" ", "%20");
|
||||
var uri = new Uri($"avares://NetworkDiagram/{escapedPath}");
|
||||
|
||||
return new Bitmap(AssetLoader.Open(uri));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[ImageConverter] Error loading '{path}': {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public class DiagramSaveModel {
|
||||
[JsonPropertyName("Devices")]
|
||||
public List<PlacedDevice> Devices { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("Connections")]
|
||||
public List<ConnectionSaveModel> Connections { get; set; } = [];
|
||||
}
|
||||
|
||||
public class ConnectionSaveModel {
|
||||
[JsonPropertyName("StartIndex")]
|
||||
public int StartIndex { get; set; }
|
||||
|
||||
[JsonPropertyName("EndIndex")]
|
||||
public int EndIndex { get; set; }
|
||||
|
||||
[JsonPropertyName("Type")]
|
||||
public ConnectionType Type { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net10.0-windows</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="11.0.10" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.10" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.10" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.10" />
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.10" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="Assets\**" />
|
||||
<AvaloniaResource Include="Localization\**" />
|
||||
<None Update="devices.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\**">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
using Avalonia;
|
||||
|
||||
namespace NetworkDiagram;
|
||||
|
||||
class Program
|
||||
{
|
||||
[STAThread]
|
||||
public static void Main(string[] args) => BuildAvaloniaApp()
|
||||
.StartWithClassicDesktopLifetime(args);
|
||||
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
=> AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
.WithInterFont()
|
||||
.LogToTrace();
|
||||
}
|
||||
@@ -8,12 +8,12 @@
|
||||
"IconPath": "Assets/workgroup switch.jpg"
|
||||
},
|
||||
{
|
||||
"Name": "Server",
|
||||
"IconPath": "Assets/fileserver.jpg"
|
||||
"Name": "L3 Switch",
|
||||
"IconPath": "Assets/layer 3 switch.jpg"
|
||||
},
|
||||
{
|
||||
"Name": "Storage server",
|
||||
"IconPath": "Assets/storage server.jpg"
|
||||
"Name": "Server",
|
||||
"IconPath": "Assets/fileserver.jpg"
|
||||
},
|
||||
{
|
||||
"Name": "Personal Computer",
|
||||
@@ -54,5 +54,9 @@
|
||||
{
|
||||
"Name": "Wireless Router",
|
||||
"IconPath": "Assets/wireless router.jpg"
|
||||
},
|
||||
{
|
||||
"Name": "Video camera",
|
||||
"IconPath": "Assets/video camera.jpg"
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user