rewrite into avalonia for linux support

This commit is contained in:
Matěj Kubíček
2026-04-21 18:15:07 +02:00
parent fadca1bd6e
commit 8a57680b0e
33 changed files with 1288 additions and 1213 deletions
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AvaloniaProject">
<option name="projectPerEditor">
<map>
<entry key="NetworkDiagramAvalonia/App.axaml" value="NetworkDiagramAvalonia/NetworkDiagramAvalonia.csproj" />
</map>
</option>
</component>
</project>
+1
View File
@@ -1,3 +1,4 @@
<Solution>
<Project Path="NetworkDiagram/NetworkDiagram.csproj" />
<Project Path="NetworkDiagramAvalonia/NetworkDiagramAvalonia.csproj" />
</Solution>
+88
View File
@@ -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>
+23
View File
@@ -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();
}
}
-202
View File
@@ -1,202 +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>
<!-- Color Palette -->
<SolidColorBrush x:Key="PrimaryBlue" Color="#004578"/>
<SolidColorBrush x:Key="AccentBlue" Color="#0078D7"/>
<SolidColorBrush x:Key="LightBlue" Color="#E1EEFA"/>
<SolidColorBrush x:Key="GridBlue" Color="#D0E2F2"/>
<SolidColorBrush x:Key="BgWhite" Color="#F5F9FF"/>
<SolidColorBrush x:Key="TextDark" Color="#1B1B1B"/>
<SolidColorBrush x:Key="TextLight" Color="#FFFFFF"/>
<SolidColorBrush x:Key="BorderGray" Color="#CCCCCC"/>
<!-- Modern Button Style -->
<Style x:Key="ModernButtonStyle" TargetType="Button">
<Setter Property="Background" Value="{StaticResource AccentBlue}"/>
<Setter Property="Foreground" Value="{StaticResource TextLight}"/>
<Setter Property="Padding" Value="12,6"/>
<Setter Property="Margin" Value="4"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="border" Property="Background" Value="#005A9E"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="border" Property="Background" Value="#004578"/>
<Setter TargetName="border" Property="RenderTransform">
<Setter.Value>
<ScaleTransform ScaleX="0.98" ScaleY="0.98" CenterX="20" CenterY="10"/>
</Setter.Value>
</Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Secondary Button Style -->
<Style x:Key="SecondaryButtonStyle" TargetType="Button" BasedOn="{StaticResource ModernButtonStyle}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{StaticResource PrimaryBlue}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BorderBrush" Value="{StaticResource PrimaryBlue}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="border" Property="Background" Value="#D0E2F2"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="border" Property="Background" Value="#B0C2D2"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Header Button Style -->
<Style x:Key="HeaderButtonStyle" TargetType="Button" BasedOn="{StaticResource ModernButtonStyle}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Opacity" Value="0.9"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="border"
Background="{TemplateBinding Background}"
CornerRadius="4">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" Margin="8,4"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="border" Property="Background" Value="#20FFFFFF"/>
<Setter Property="Opacity" Value="1"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="border" Property="Background" Value="#40FFFFFF"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Modern TextBox Style -->
<Style TargetType="TextBox">
<Setter Property="Padding" Value="8,6"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderGray}"/>
<Setter Property="Background" Value="White"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Border x:Name="border"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
CornerRadius="4">
<ScrollViewer x:Name="PART_ContentHost" Focusable="false" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsFocused" Value="True">
<Setter TargetName="border" Property="BorderBrush" Value="{StaticResource AccentBlue}"/>
<Setter TargetName="border" Property="BorderThickness" Value="1.5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Modern ComboBox Style -->
<Style TargetType="ComboBox">
<Setter Property="Padding" Value="8,4"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderGray}"/>
<Setter Property="Background" Value="White"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
</Style>
<!-- Modern ListBox Style (for Sidebar) -->
<Style x:Key="SidebarListBoxStyle" TargetType="ListBox">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="ListBoxItem">
<Setter Property="Padding" Value="0"/>
<Setter Property="Margin" Value="4"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border x:Name="border"
Background="{TemplateBinding Background}"
CornerRadius="6"
Padding="4">
<ContentPresenter />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="border" Property="Background" Value="#150078D7"/>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="border" Property="Background" Value="#250078D7"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Setter.Value>
</Setter>
</Style>
<!-- Device Container Style -->
<Style x:Key="DeviceContainerStyle" TargetType="Border">
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Padding" Value="4"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="2"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#100078D7"/>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
</Application.Resources>
</Application>
-10
View File
@@ -1,10 +0,0 @@
using System.Windows;
namespace NetworkDiagram;
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
}
-10
View File
@@ -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)
)]
+22
View File
@@ -0,0 +1,22 @@
<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">
<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,20"/>
<TextBlock Grid.Row="2" Text="{DynamicResource IpLabel}" Margin="0,0,0,8" FontWeight="SemiBold" Foreground="{DynamicResource PrimaryBlue}"/>
<TextBox x:Name="IpBox" Grid.Row="3" Margin="0,0,0,25"
AcceptsReturn="True" VerticalContentAlignment="Top" MinHeight="80"/>
<StackPanel Grid.Row="4" 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>
+42
View File
@@ -0,0 +1,42 @@
using System;
using System.Linq;
using Avalonia;
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;
IpBox.Text = string.Join(Environment.NewLine, device.IpAddresses);
}
private void Save_Click(object? sender, RoutedEventArgs e)
{
_device.Name = NameBox.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);
}
}
}
-31
View File
@@ -1,31 +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="380" Width="420"
WindowStartupLocation="CenterOwner" ResizeMode="NoResize"
Background="{StaticResource BgWhite}"
WindowStyle="ThreeDBorderWindow"
Icon="/Assets/router.jpg">
<Grid Margin="25">
<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,8" FontWeight="SemiBold" Foreground="{StaticResource PrimaryBlue}"/>
<TextBox x:Name="NameBox" Grid.Row="1" Margin="0,0,0,20"/>
<TextBlock Grid.Row="2" Text="{DynamicResource IpLabel}" Margin="0,0,0,8" FontWeight="SemiBold" Foreground="{StaticResource PrimaryBlue}"/>
<TextBox x:Name="IpBox" Grid.Row="3" Margin="0,0,0,25"
AcceptsReturn="True" VerticalScrollBarVisibility="Auto" MinHeight="80"/>
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="{DynamicResource CancelBtn}" Style="{StaticResource SecondaryButtonStyle}" Click="Cancel_Click" Width="90" Margin="0,0,12,0"/>
<Button Content="{DynamicResource SaveDialogBtn}" Style="{StaticResource ModernButtonStyle}" Click="Save_Click" Width="90"/>
</StackPanel>
</Grid>
</Window>
-32
View File
@@ -1,32 +0,0 @@
using System.Windows;
using NetworkDiagram.Models;
namespace NetworkDiagram
{
public partial class EditDeviceWindow
{
private readonly 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([Environment.NewLine, "\n", "\r"], StringSplitOptions.RemoveEmptyEntries).ToList();
DialogResult = true;
Close();
}
private void Cancel_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
}
}
@@ -0,0 +1,25 @@
<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="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,25 @@
<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="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>
+112
View File
@@ -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"
PointerPressed="Toolbox_PointerPressed">
<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"
PointerPressed="Viewport_PointerPressed"
PointerMoved="Viewport_PointerMoved"
PointerReleased="Viewport_PointerReleased"
PointerWheelChanged="Viewport_PointerWheelChanged">
<Canvas x:Name="DiagramCanvas" Width="40000" Height="40000"
DragDrop.AllowDrop="True"
PointerPressed="DiagramCanvas_PointerPressed"
PointerMoved="DiagramCanvas_PointerMoved"
PointerReleased="DiagramCanvas_PointerReleased">
<Canvas.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<TranslateTransform X="-20000" Y="-20000"/>
</TransformGroup>
</Canvas.RenderTransform>
<Canvas.Background>
<DrawingBrush TileMode="Tile" 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>
+644
View File
@@ -0,0 +1,644 @@
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.VisualTree;
using NetworkDiagram.Models;
using Avalonia.Controls.Templates;
namespace NetworkDiagram
{
public partial class MainWindow : Window
{
private List<DeviceTemplate> _deviceTemplates = [];
private Diagram _currentDiagram = new();
// 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();
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, DiagramCanvas_DragOver);
AddHandler(DragDrop.DropEvent, DiagramCanvas_Drop);
}
private void CenterView()
{
if (CanvasTranslate == null || CanvasScale == null) return;
CanvasTranslate.X = -20000 + (CanvasViewport.Bounds.Width / 2);
CanvasTranslate.Y = -20000 + (CanvasViewport.Bounds.Height / 2);
CanvasScale.ScaleX = 1.0;
CanvasScale.ScaleY = 1.0;
}
#region Panning and Zooming
private void Viewport_PointerPressed(object? sender, PointerPressedEventArgs e)
{
var properties = e.GetCurrentPoint(CanvasViewport).Properties;
if (properties.IsMiddleButtonPressed)
{
_isPanning = true;
_lastPanPoint = e.GetPosition(CanvasViewport);
e.Pointer.Capture(CanvasViewport);
}
}
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)
{
if (e.InitialPressMouseButton == MouseButton.Middle)
{
_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.1 || newScale > 5) 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) ?? [];
ToolboxList.ItemsSource = _deviceTemplates;
}
catch (Exception ex) { Console.WriteLine($"Error loading templates: {ex.Message}"); }
}
private void LangCombo_SelectionChanged(object? sender, SelectionChangedEventArgs e)
{
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 async void Toolbox_PointerPressed(object? sender, PointerPressedEventArgs e)
{
var item = (e.Source as Control)?.FindAncestorOfType<ListBoxItem>();
if (item?.Content is DeviceTemplate template)
{
var data = new DataObject();
data.Set("DeviceTemplate", template);
await DragDrop.DoDragDrop(e, data, DragDropEffects.Copy);
}
}
private void DiagramCanvas_PointerPressed(object? sender, PointerPressedEventArgs e)
{
if (e.Source == 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.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);
}
// Implementation of DragDrop for Canvas
private void DiagramCanvas_DragOver(object? sender, DragEventArgs e)
{
if (e.Data.Contains("DeviceTemplate"))
e.DragEffects = DragDropEffects.Copy;
else
e.DragEffects = DragDropEffects.None;
}
private void DiagramCanvas_Drop(object? sender, DragEventArgs e)
{
if (e.Data.Get("DeviceTemplate") is DeviceTemplate template)
{
var 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 container = new Border
{
CornerRadius = new CornerRadius(8),
Padding = new Thickness(8),
Background = Brushes.Transparent,
BorderBrush = Brushes.Transparent,
BorderThickness = new Thickness(2),
MaxWidth = 160,
Tag = device
};
var stack = new StackPanel();
var image = new Image { MaxWidth = 64, MaxHeight = 64, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center };
image.Bind(Image.SourceProperty, new Avalonia.Data.Binding(nameof(device.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, Margin = new Thickness(0,4,0,0) };
if (primaryBlue is IBrush brush) nameText.Foreground = brush;
nameText.Bind(TextBlock.TextProperty, new Avalonia.Data.Binding(nameof(device.Name)));
stack.Children.Add(nameText);
var ips = new ItemsControl();
ips.Bind(ItemsControl.ItemsSourceProperty, new Avalonia.Data.Binding(nameof(device.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(30, 0, 120, 215));
}
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 = Brushes.Transparent;
b.Background = Brushes.Transparent;
}
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 s) || !_deviceElements.TryGetValue(conn.EndDevice, out var e)) return;
line.StartPoint = new Point(conn.StartDevice.X + s.Bounds.Width / 2, conn.StartDevice.Y + s.Bounds.Height / 2);
line.EndPoint = new Point(conn.EndDevice.X + e.Bounds.Width / 2, conn.EndDevice.Y + e.Bounds.Height / 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(); };
line.PointerPressed += (s, e) => {
if (!e.KeyModifiers.HasFlag(KeyModifiers.Control)) ClearSelection();
SelectElement(line);
e.Handled = true;
};
_connectionLines[conn] = line;
DiagramCanvas.Children.Insert(0, line);
// Initial position update after layout
LayoutUpdated += (s, e) => UpdatePos();
}
private async void EditDevice(PlacedDevice device)
{
var dialog = new EditDeviceWindow(device);
await dialog.ShowDialog(this);
if (_deviceElements.TryGetValue(device, out var elem))
{
// Refresh layout and connections
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))
{
l.StartPoint = new Point(c.StartDevice.X + _deviceElements[c.StartDevice].Bounds.Width / 2, c.StartDevice.Y + _deviceElements[c.StartDevice].Bounds.Height / 2);
l.EndPoint = new Point(c.EndDevice.X + _deviceElements[c.EndDevice].Bounds.Width / 2, c.EndDevice.Y + _deviceElements[c.EndDevice].Bounds.Height / 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();
}
}
#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 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)
{
using var stream = await files[0].OpenReadAsync();
var model = await JsonSerializer.DeserializeAsync<DiagramSaveModel>(stream);
if (model == null) return;
NewDiagram_Click(null, new RoutedEventArgs());
_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;
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)
{
if (CanvasTranslate == null || CanvasScale == null) return;
double centerX = (CanvasViewport.Bounds.Width / 2 - CanvasTranslate.X) / CanvasScale.ScaleX;
double centerY = (CanvasViewport.Bounds.Height / 2 - CanvasTranslate.Y) / CanvasScale.ScaleY;
AddDeviceToCanvas(new DeviceTemplate { Name = "Note", IconPath = "" }, centerX, centerY);
}
private async void ExportPng_Click(object? sender, RoutedEventArgs e)
{
if (_currentDiagram.Devices.Count == 0) 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)
{
var oldTranslate = new Point(CanvasTranslate.X, CanvasTranslate.Y);
var oldScale = new Point(CanvasScale.ScaleX, CanvasScale.ScaleY);
double minX = _currentDiagram.Devices.Min(d => d.X) - 20;
double minY = _currentDiagram.Devices.Min(d => d.Y) - 20;
double maxX = _currentDiagram.Devices.Max(d => d.X) + 180;
double maxY = _currentDiagram.Devices.Max(d => d.Y) + 120;
var pixelSize = new PixelSize((int)(maxX - minX), (int)(maxY - minY));
var rtb = new RenderTargetBitmap(pixelSize, new Vector(96, 96));
CanvasTranslate.X = -minX;
CanvasTranslate.Y = -minY;
CanvasScale.ScaleX = 1.0;
CanvasScale.ScaleY = 1.0;
await Task.Delay(50);
rtb.Render(DiagramCanvas);
using var stream = await file.OpenWriteAsync();
rtb.Save(stream);
CanvasTranslate.X = oldTranslate.X;
CanvasTranslate.Y = oldTranslate.Y;
CanvasScale.ScaleX = oldScale.X;
CanvasScale.ScaleY = oldScale.Y;
}
}
#endregion
}
}
-138
View File
@@ -1,138 +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"
mc:Ignorable="d"
Title="{DynamicResource AppTitle}" Height="750" Width="1100"
Background="{StaticResource BgWhite}"
KeyDown="Window_KeyDown"
Icon="/Assets/router.jpg">
<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="{StaticResource PrimaryBlue}" VerticalAlignment="Stretch">
<Border.Effect>
<DropShadowEffect BlurRadius="10" ShadowDepth="2" Opacity="0.3"/>
</Border.Effect>
<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"/>
<Button Content="{DynamicResource NewBtn}" Style="{StaticResource HeaderButtonStyle}" Click="NewDiagram_Click"/>
<Button Content="{DynamicResource SaveBtn}" Style="{StaticResource HeaderButtonStyle}" Click="SaveDiagram_Click"/>
<Button Content="{DynamicResource LoadBtn}" Style="{StaticResource HeaderButtonStyle}" Click="LoadDiagram_Click"/>
<Button Content="{DynamicResource ExportBtn}" Style="{StaticResource HeaderButtonStyle}" Click="ExportPng_Click"/>
<Rectangle Width="1" Height="24" Fill="White" Opacity="0.3" Margin="15,0"/>
<Button Content="{DynamicResource WireBtn}" Style="{StaticResource HeaderButtonStyle}" Click="WireTool_Click"/>
<Button Content="{DynamicResource WifiBtn}" Style="{StaticResource HeaderButtonStyle}" Click="WifiTool_Click"/>
<Button Content="{DynamicResource AddTextBtn}" Style="{StaticResource HeaderButtonStyle}" Click="AddText_Click"/>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
<ComboBox x:Name="LangCombo" Width="70" Height="30" SelectionChanged="LangCombo_SelectionChanged">
<ComboBoxItem Tag="en" IsSelected="True">EN</ComboBoxItem>
<ComboBoxItem Tag="cs">CZ</ComboBoxItem>
</ComboBox>
</StackPanel>
</DockPanel>
</Border>
<!-- Sidebar -->
<Border Grid.Row="1" Grid.Column="0" Background="{StaticResource LightBlue}" BorderBrush="{StaticResource 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="{StaticResource PrimaryBlue}"/>
<ListBox x:Name="ToolboxList" Style="{StaticResource SidebarListBoxStyle}"
PreviewMouseDown="Toolbox_PreviewMouseDown">
<ListBox.ItemTemplate>
<DataTemplate>
<Border Background="White" CornerRadius="8" Padding="12" BorderBrush="{StaticResource GridBlue}" BorderThickness="1">
<Border.Effect>
<DropShadowEffect BlurRadius="4" ShadowDepth="1" Opacity="0.05"/>
</Border.Effect>
<StackPanel Orientation="Horizontal">
<Image Source="{Binding IconPath}" Width="36" Height="36" Margin="0,0,12,0" RenderOptions.BitmapScalingMode="HighQuality"/>
<TextBlock Text="{Binding Name}" VerticalAlignment="Center" FontSize="14" FontWeight="SemiBold" Foreground="{StaticResource TextDark}"/>
</StackPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</Border>
<!-- Canvas Area -->
<Grid Grid.Row="1" Grid.Column="1" x:Name="CanvasViewport" ClipToBounds="True" Background="Transparent"
MouseDown="Viewport_MouseDown"
MouseMove="Viewport_MouseMove"
MouseUp="Viewport_MouseUp"
MouseWheel="Viewport_MouseWheel">
<!-- Increased size to 40k to provide plenty of space in all directions -->
<Canvas x:Name="DiagramCanvas" Width="40000" Height="40000" AllowDrop="True"
Drop="DiagramCanvas_Drop" DragOver="DiagramCanvas_DragOver"
MouseDown="DiagramCanvas_MouseDown"
MouseMove="DiagramCanvas_MouseMove"
MouseUp="DiagramCanvas_MouseUp">
<Canvas.RenderTransform>
<TransformGroup>
<ScaleTransform x:Name="CanvasScale" ScaleX="1" ScaleY="1"/>
<TranslateTransform x:Name="CanvasTranslate" X="-20000" Y="-20000"/>
</TransformGroup>
</Canvas.RenderTransform>
<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 GridBlue}" StrokeThickness="0.8"/>
</VisualBrush.Visual>
</VisualBrush>
</Canvas.Background>
<Canvas.Resources>
<DataTemplate x:Key="DeviceTemplate">
<Border x:Name="DeviceBorder" BorderBrush="Transparent" BorderThickness="2" CornerRadius="8"
Background="Transparent" Padding="8" MaxWidth="160">
<StackPanel>
<Image Source="{Binding IconPath}" MaxWidth="64" MaxHeight="64" HorizontalAlignment="Center" RenderOptions.BitmapScalingMode="HighQuality">
<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" FontSize="12" HorizontalAlignment="Center"
Foreground="{StaticResource PrimaryBlue}" TextWrapping="Wrap" TextAlignment="Center" Margin="0,4,0,0"/>
<ItemsControl ItemsSource="{Binding IpAddresses}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" FontSize="10" Foreground="{StaticResource AccentBlue}"
HorizontalAlignment="Center" TextWrapping="Wrap" Opacity="0.8"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
</DataTemplate>
</Canvas.Resources>
<!-- Selection rectangle visual -->
<Rectangle x:Name="SelectionRect" Stroke="{StaticResource AccentBlue}" StrokeThickness="1.5" StrokeDashArray="3,3"
Fill="#220078D7" Visibility="Collapsed" Panel.ZIndex="9999" RadiusX="2" RadiusY="2" IsHitTestVisible="False"/>
</Canvas>
</Grid>
</Grid>
</Window>
-710
View File
@@ -1,710 +0,0 @@
using System.IO;
using System.Text.Json;
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
{
private List<DeviceTemplate> _deviceTemplates = [];
private Diagram _currentDiagram = new();
// Selection & Dragging
private readonly HashSet<FrameworkElement> _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, FrameworkElement> _deviceElements = new();
// Panning and Zooming
private Point _lastPanPoint;
private bool _isPanning;
public MainWindow()
{
InitializeComponent();
LoadTemplates();
this.Loaded += MainWindow_Loaded;
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
CenterView();
}
private void CenterView()
{
// Center logically on (20000, 20000) minus half the viewport size to put 0,0 in the middle
CanvasTranslate.X = -20000 + (CanvasViewport.ActualWidth / 2);
CanvasTranslate.Y = -20000 + (CanvasViewport.ActualHeight / 2);
CanvasScale.ScaleX = 1.0;
CanvasScale.ScaleY = 1.0;
}
#region Panning and Zooming
private void Viewport_MouseDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Middle)
{
_isPanning = true;
_lastPanPoint = e.GetPosition(CanvasViewport);
CanvasViewport.CaptureMouse();
}
}
private void Viewport_MouseMove(object sender, MouseEventArgs 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_MouseUp(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Middle)
{
_isPanning = false;
CanvasViewport.ReleaseMouseCapture();
}
}
private void Viewport_MouseWheel(object sender, MouseWheelEventArgs e)
{
double zoom = e.Delta > 0 ? 1.1 : 0.9;
double newScale = CanvasScale.ScaleX * zoom;
if (newScale < 0.1 || newScale > 5) return;
Point mousePos = e.GetPosition(DiagramCanvas);
// Zoom centered on mouse
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) ?? [];
foreach (var t in _deviceTemplates.Where(t => !string.IsNullOrEmpty(t.IconPath)))
{
t.IconPath = Path.GetFullPath(t.IconPath);
}
ToolboxList.ItemsSource = _deviceTemplates;
}
catch (Exception ex) { MessageBox.Show($"Error loading templates: {ex.Message}"); }
}
private static string GetLocalizedString(string key) => Application.Current.Resources[key] as string ?? key;
private void LangCombo_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (LangCombo.SelectedItem is not ComboBoxItem { Tag: string lang }) return;
var dict = new ResourceDictionary
{
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)
{
// Find the item under the mouse instead of relying on SelectedItem
var element = e.OriginalSource as DependencyObject;
while (element != null && !(element is ListBoxItem))
element = VisualTreeHelper.GetParent(element);
if (element is ListBoxItem item && item.Content 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))) return;
var template = (DeviceTemplate)e.Data.GetData(typeof(DeviceTemplate));
var 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,
Style = (Style)Application.Current.Resources["DeviceContainerStyle"]
};
Canvas.SetLeft(container, device.X);
Canvas.SetTop(container, device.Y);
device.PropertyChanged += (s, e) =>
{
switch (e.PropertyName)
{
case nameof(PlacedDevice.X):
Canvas.SetLeft(container, device.X);
break;
case nameof(PlacedDevice.Y):
Canvas.SetTop(container, device.Y);
break;
}
};
container.MouseDown += Device_MouseDown;
container.MouseLeftButtonDown += (s, e) =>
{
if (e.ClickCount != 2) return;
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 not FrameworkElement { Tag: PlacedDevice device } element) return;
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);
if (element is Border b)
{
b.BorderBrush = (SolidColorBrush)Application.Current.Resources["AccentBlue"];
b.Background = new SolidColorBrush(Color.FromArgb(30, 0, 120, 215)); // Light blue tint
}
else if (element is Line l)
{
l.StrokeThickness = 5;
l.Opacity = 0.8;
}
}
private void DeselectElement(FrameworkElement element)
{
_selectedElements.Remove(element);
switch (element)
{
case Border b:
b.BorderBrush = Brushes.Transparent;
b.Background = Brushes.Transparent;
break;
case Line l:
l.StrokeThickness = 3;
l.Opacity = 1.0;
break;
}
}
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)
{
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;
// Real-time selection preview
var selectionBounds = new Rect(x, y, w, h);
foreach (var border in _deviceElements.Values)
{
var 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)
{
var 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)
{
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_MouseUp(object sender, MouseButtonEventArgs e)
{
if (_isSelectingArea)
{
var selectionBounds = new Rect(
Canvas.GetLeft(SelectionRect),
Canvas.GetTop(SelectionRect),
SelectionRect.Width,
SelectionRect.Height);
foreach (var border in _deviceElements.Values)
{
var 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 = [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)) return;
if (startElem.ActualWidth == 0 || startElem.ActualHeight == 0) startElem.UpdateLayout();
if (endElem.ActualWidth == 0 || endElem.ActualHeight == 0) endElem.UpdateLayout();
line.X1 = conn.StartDevice.X + (startElem.ActualWidth / 2);
line.Y1 = conn.StartDevice.Y + (startElem.ActualHeight / 2);
line.X2 = conn.EndDevice.X + (endElem.ActualWidth / 2);
line.Y2 = conn.EndDevice.Y + (endElem.ActualHeight / 2);
}
private void Window_KeyDown(object sender, KeyEventArgs e)
{
switch (e.Key)
{
case Key.Delete when _selectedElements.Count > 0:
{
foreach (var el in _selectedElements.ToList())
{
switch (el)
{
case Line line when el.Tag is Connection conn:
_currentDiagram.Connections.Remove(conn);
_connectionLines.Remove(conn);
DiagramCanvas.Children.Remove(line);
break;
case Border { Tag: PlacedDevice device } border:
{
_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);
break;
}
}
}
_selectedElements.Clear();
break;
}
case Key.Escape:
ResetConnectionTool(); StopDragging(); ClearSelection();
break;
}
}
private void EditDevice(PlacedDevice device)
{
var dialog = new EditDeviceWindow(device);
dialog.ShowDialog();
if (!_deviceElements.TryGetValue(device, out var elem)) return;
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();
// Re-add selection rectangle after clearing
if (SelectionRect != null) DiagramCanvas.Children.Add(SelectionRect);
_connectionLines.Clear();
_deviceElements.Clear();
ClearSelection();
CenterView();
}
private void SaveDiagram_Click(object sender, RoutedEventArgs e)
{
var sfd = new SaveFileDialog { Filter = "Network Diagram (*.ndjson)|*.ndjson" };
if (sfd.ShowDialog() != true) return;
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()
};
var 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) return;
var 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();
// Re-add selection rectangle after clearing
if (SelectionRect != null) DiagramCanvas.Children.Add(SelectionRect);
_connectionLines.Clear();
_deviceElements.Clear();
ClearSelection();
foreach (var device in _currentDiagram.Devices) RenderDevice(device);
foreach (var conn in model.Connections.Select(cModel => new Connection { StartDevice = _currentDiagram.Devices[cModel.StartIndex], EndDevice = _currentDiagram.Devices[cModel.EndIndex], Type = cModel.Type }))
{
_currentDiagram.Connections.Add(conn);
RenderConnection(conn);
}
// Center on loaded content
if (_currentDiagram.Devices.Count > 0)
{
double minX = _currentDiagram.Devices.Min(d => d.X);
double minY = _currentDiagram.Devices.Min(d => d.Y);
double maxX = _currentDiagram.Devices.Max(d => d.X);
double maxY = _currentDiagram.Devices.Max(d => d.Y);
CanvasTranslate.X = -((minX + maxX) / 2) * CanvasScale.ScaleX + (CanvasViewport.ActualWidth / 2);
CanvasTranslate.Y = -((minY + maxY) / 2) * CanvasScale.ScaleY + (CanvasViewport.ActualHeight / 2);
}
else
{
CenterView();
}
}
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)
{
// Center of the current viewport in canvas coordinates
double centerX = (CanvasViewport.ActualWidth / 2 - CanvasTranslate.X) / CanvasScale.ScaleX;
double centerY = (CanvasViewport.ActualHeight / 2 - CanvasTranslate.Y) / CanvasScale.ScaleY;
AddDeviceToCanvas(new DeviceTemplate { Name = "Note", IconPath = "" }, centerX, centerY);
}
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;
var 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)
{
var x = Canvas.GetLeft(border);
var 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));
}
const double margin = 20;
minX -= margin; minY -= margin; maxX += margin; maxY += margin;
var width = Math.Max(1, maxX - minX);
var height = Math.Max(1, maxY - minY);
try
{
var rtb = new RenderTargetBitmap((int)width, (int)height, 96, 96, PixelFormats.Pbgra32);
var dv = new DrawingVisual();
using (var 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;
var left = Canvas.GetLeft((UIElement)child);
var top = Canvas.GetTop((UIElement)child);
switch (child)
{
case 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));
break;
case FrameworkElement fe:
{
var vb = new VisualBrush(fe) { Stretch = Stretch.None };
dc.DrawRectangle(vb, null, new Rect(left, top, fe.ActualWidth, fe.ActualHeight));
break;
}
}
}
}
rtb.Render(dv);
var 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; } = [];
public List<ConnectionSaveModel> Connections { get; set; } = [];
}
public class ConnectionSaveModel {
public int StartIndex { get; init; }
public int EndIndex { get; set; }
public ConnectionType Type { get; set; }
}
}
+64 -11
View File
@@ -1,6 +1,14 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Avalonia;
using Avalonia.Data.Converters;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
namespace NetworkDiagram.Models
{
@@ -18,59 +26,63 @@ namespace NetworkDiagram.Models
public class PlacedDevice : INotifyPropertyChanged
{
private string _name = string.Empty;
public string Name
{
get;
get => _name;
set
{
field = value;
_name = value;
OnPropertyChanged();
}
} = string.Empty;
}
public string TemplateName { get; set; } = string.Empty;
[JsonIgnore]
public string IconPath { get; set; } = string.Empty;
private double _x;
public double X
{
get;
get => _x;
set
{
field = value;
_x = value;
OnPropertyChanged();
OnPropertyChanged(nameof(CenterX));
}
}
private double _y;
public double Y
{
get;
get => _y;
set
{
field = value;
_y = value;
OnPropertyChanged();
OnPropertyChanged(nameof(CenterY));
}
}
private List<string> _ipAddresses = [];
public List<string> IpAddresses
{
get;
get => _ipAddresses;
set
{
field = value;
_ipAddresses = value;
OnPropertyChanged();
}
} = [];
}
public double CenterX => X + 50;
public double CenterY => Y + 40;
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? name = null)
protected void OnPropertyChanged([CallerMemberName] string? name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
@@ -88,4 +100,45 @@ namespace NetworkDiagram.Models
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.IsNullOrEmpty(path))
{
try
{
if (path.StartsWith("avares://"))
return new Bitmap(AssetLoader.Open(new Uri(path)));
if (File.Exists(path))
return new Bitmap(path);
// Try as relative avares
var uri = new Uri($"avares://NetworkDiagram/{path.Replace("\\", "/")}");
return new Bitmap(AssetLoader.Open(uri));
}
catch
{
return null;
}
}
return null;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => throw new NotImplementedException();
}
public class DiagramSaveModel {
public List<PlacedDevice> Devices { get; set; } = [];
public List<ConnectionSaveModel> Connections { get; set; } = [];
}
public class ConnectionSaveModel {
public int StartIndex { get; set; }
public int EndIndex { get; set; }
public ConnectionType Type { get; set; }
}
}
+13 -8
View File
@@ -1,20 +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>
<Resource Include="Assets\**" />
<None Update="Assets\**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
+17
View File
@@ -0,0 +1,17 @@
using Avalonia;
using System;
namespace NetworkDiagram;
class Program
{
[STAThread]
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
}
+15
View File
@@ -0,0 +1,15 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="NetworkDiagramAvalonia.App"
xmlns:local="using:NetworkDiagramAvalonia"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>
+28
View File
@@ -0,0 +1,28 @@
using Avalonia.Controls.ApplicationLifetimes;
using System.Net.Mime;
using Avalonia.Markup.Xaml;
using NetworkDiagramAvalonia.ViewModels;
using NetworkDiagramAvalonia.Views;
namespace NetworkDiagramAvalonia;
public partial class App : MediaTypeNames.Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(),
};
}
base.OnFrameworkInitializationCompleted();
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<ItemGroup>
<Folder Include="Models\"/>
<AvaloniaResource Include="Assets\**"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="12.0.1"/>
<PackageReference Include="Avalonia.Desktop" Version="12.0.1"/>
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.0.1"/>
<PackageReference Include="Avalonia.Fonts.Inter" Version="12.0.1"/>
<PackageReference Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.1">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.1"/>
</ItemGroup>
</Project>
+24
View File
@@ -0,0 +1,24 @@
using Avalonia;
using System;
namespace NetworkDiagramAvalonia;
sealed class Program
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
#if DEBUG
.WithDeveloperTools()
#endif
.WithInterFont()
.LogToTrace();
}
+37
View File
@@ -0,0 +1,37 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using NetworkDiagramAvalonia.ViewModels;
namespace NetworkDiagramAvalonia;
/// <summary>
/// Given a view model, returns the corresponding view if possible.
/// </summary>
[RequiresUnreferencedCode(
"Default implementation of ViewLocator involves reflection which may be trimmed away.",
Url = "https://docs.avaloniaui.net/docs/concepts/view-locator")]
public class ViewLocator : IDataTemplate
{
public Control? Build(object? param)
{
if (param is null)
return null;
var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
var type = Type.GetType(name);
if (type != null)
{
return (Control)Activator.CreateInstance(type)!;
}
return new TextBlock { Text = "Not Found: " + name };
}
public bool Match(object? data)
{
return data is ViewModelBase;
}
}
@@ -0,0 +1,6 @@
namespace NetworkDiagramAvalonia.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
public string Greeting { get; } = "Welcome to Avalonia!";
}
@@ -0,0 +1,7 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace NetworkDiagramAvalonia.ViewModels;
public abstract class ViewModelBase : ObservableObject
{
}
@@ -0,0 +1,21 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:NetworkDiagramAvalonia.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:NetworkDiagramAvalonia.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="NetworkDiagramAvalonia.Views.MainWindow"
x:DataType="viewModels:MainWindowViewModel"
Icon="/Assets/avalonia-logo.ico"
Title="NetworkDiagramAvalonia">
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<viewModels:MainWindowViewModel/>
</Design.DataContext>
<TextBlock Text="{Binding Greeting}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Window>
@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace NetworkDiagramAvalonia.Views;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embedded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="NetworkDiagramAvalonia.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>