diff --git a/.idea/.idea.NetworkDiagram/.idea/avalonia.xml b/.idea/.idea.NetworkDiagram/.idea/avalonia.xml
new file mode 100644
index 0000000..9d413b1
--- /dev/null
+++ b/.idea/.idea.NetworkDiagram/.idea/avalonia.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/NetworkDiagram.slnx b/NetworkDiagram.slnx
index c3d1e2c..a3de534 100644
--- a/NetworkDiagram.slnx
+++ b/NetworkDiagram.slnx
@@ -1,3 +1,4 @@
+
diff --git a/NetworkDiagram/App.axaml b/NetworkDiagram/App.axaml
new file mode 100644
index 0000000..f51cea6
--- /dev/null
+++ b/NetworkDiagram/App.axaml
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ #004578
+ #0078D7
+ #E1EEFA
+ #D0E2F2
+ #F5F9FF
+ #1B1B1B
+ #FFFFFF
+ #CCCCCC
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/NetworkDiagram/App.axaml.cs b/NetworkDiagram/App.axaml.cs
new file mode 100644
index 0000000..dc96a82
--- /dev/null
+++ b/NetworkDiagram/App.axaml.cs
@@ -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();
+ }
+}
diff --git a/NetworkDiagram/App.xaml b/NetworkDiagram/App.xaml
deleted file mode 100644
index 0c5c082..0000000
--- a/NetworkDiagram/App.xaml
+++ /dev/null
@@ -1,202 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/NetworkDiagram/App.xaml.cs b/NetworkDiagram/App.xaml.cs
deleted file mode 100644
index 57c6000..0000000
--- a/NetworkDiagram/App.xaml.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using System.Windows;
-
-namespace NetworkDiagram;
-
-///
-/// Interaction logic for App.xaml
-///
-public partial class App : Application
-{
-}
\ No newline at end of file
diff --git a/NetworkDiagram/AssemblyInfo.cs b/NetworkDiagram/AssemblyInfo.cs
deleted file mode 100644
index 4a05c7d..0000000
--- a/NetworkDiagram/AssemblyInfo.cs
+++ /dev/null
@@ -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)
-)]
\ No newline at end of file
diff --git a/NetworkDiagram/EditDeviceWindow.axaml b/NetworkDiagram/EditDeviceWindow.axaml
new file mode 100644
index 0000000..8aed92d
--- /dev/null
+++ b/NetworkDiagram/EditDeviceWindow.axaml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/NetworkDiagram/EditDeviceWindow.axaml.cs b/NetworkDiagram/EditDeviceWindow.axaml.cs
new file mode 100644
index 0000000..3e79828
--- /dev/null
+++ b/NetworkDiagram/EditDeviceWindow.axaml.cs
@@ -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);
+ }
+ }
+}
diff --git a/NetworkDiagram/EditDeviceWindow.xaml b/NetworkDiagram/EditDeviceWindow.xaml
deleted file mode 100644
index dc3f900..0000000
--- a/NetworkDiagram/EditDeviceWindow.xaml
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/NetworkDiagram/EditDeviceWindow.xaml.cs b/NetworkDiagram/EditDeviceWindow.xaml.cs
deleted file mode 100644
index 475f100..0000000
--- a/NetworkDiagram/EditDeviceWindow.xaml.cs
+++ /dev/null
@@ -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();
- }
- }
-}
diff --git a/NetworkDiagram/Localization/Strings.cs.axaml b/NetworkDiagram/Localization/Strings.cs.axaml
new file mode 100644
index 0000000..1c85d80
--- /dev/null
+++ b/NetworkDiagram/Localization/Strings.cs.axaml
@@ -0,0 +1,25 @@
+
+ Editor síťových diagramů
+ Nový
+ Uložit
+ Načíst
+ Exportovat PNG
+ Kabelové připojení
+ Bezdrátové (WiFi)
+ Přidat text
+ Zařízení
+
+
+ Upravit zařízení
+ Název zařízení:
+ IP adresy (jedna na řádek):
+ Zrušit
+ Uložit
+
+
+ Chcete bílé pozadí? (Ne = průhlednost)
+ Možnosti exportu
+ Export byl úspěšný!
+ Diagram je prázdný.
+
diff --git a/NetworkDiagram/Localization/Strings.cs.xaml b/NetworkDiagram/Localization/Strings.cs.xaml
deleted file mode 100644
index 7f674ea..0000000
--- a/NetworkDiagram/Localization/Strings.cs.xaml
+++ /dev/null
@@ -1,26 +0,0 @@
-
- Editor síťových diagramů
- Nový
- Uložit
- Načíst
- Exportovat PNG
- Kabelové připojení
- Bezdrátové (WiFi)
- Přidat text
- Zařízení
-
-
- Upravit zařízení
- Název zařízení:
- IP adresy (jedna na řádek):
- Zrušit
- Uložit
-
-
- Chcete bílé pozadí? (Ne = průhlednost)
- Možnosti exportu
- Export byl úspěšný!
- Diagram je prázdný.
-
diff --git a/NetworkDiagram/Localization/Strings.en.axaml b/NetworkDiagram/Localization/Strings.en.axaml
new file mode 100644
index 0000000..69b9b4c
--- /dev/null
+++ b/NetworkDiagram/Localization/Strings.en.axaml
@@ -0,0 +1,25 @@
+
+ Network Diagram Studio
+ New
+ Save
+ Load
+ Export PNG
+ Wire Connection
+ Wifi Connection
+ Add Text
+ Devices
+
+
+ Edit Device
+ Device Name:
+ IP Addresses (one per line):
+ Cancel
+ Save
+
+
+ Do you want a white background? (No will result in transparency)
+ Export Options
+ Export successful!
+ Diagram is empty.
+
diff --git a/NetworkDiagram/Localization/Strings.en.xaml b/NetworkDiagram/Localization/Strings.en.xaml
deleted file mode 100644
index 9bd2ace..0000000
--- a/NetworkDiagram/Localization/Strings.en.xaml
+++ /dev/null
@@ -1,26 +0,0 @@
-
- Network Diagram Studio
- New
- Save
- Load
- Export PNG
- Wire Connection
- Wifi Connection
- Add Text
- Devices
-
-
- Edit Device
- Device Name:
- IP Addresses (one per line):
- Cancel
- Save
-
-
- Do you want a white background? (No will result in transparency)
- Export Options
- Export successful!
- Diagram is empty.
-
diff --git a/NetworkDiagram/MainWindow.axaml b/NetworkDiagram/MainWindow.axaml
new file mode 100644
index 0000000..619edef
--- /dev/null
+++ b/NetworkDiagram/MainWindow.axaml
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0 2 10 0 #4D000000
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ EN
+ CZ
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/NetworkDiagram/MainWindow.axaml.cs b/NetworkDiagram/MainWindow.axaml.cs
new file mode 100644
index 0000000..df9983e
--- /dev/null
+++ b/NetworkDiagram/MainWindow.axaml.cs
@@ -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 _deviceTemplates = [];
+ private Diagram _currentDiagram = new();
+
+ // Selection & Dragging
+ private readonly HashSet _selectedElements = [];
+ private Point _dragStartPoint;
+ private bool _isDraggingDevices;
+ private bool _isSelectingArea;
+ private Point _selectionStartPoint;
+
+ private ConnectionType? _activeTool;
+ private PlacedDevice? _firstDeviceForConnection;
+
+ private readonly Dictionary _connectionLines = new();
+ private readonly Dictionary _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().First();
+ CanvasTranslate = group.Children.OfType().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>(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().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();
+ 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((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(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(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
+ }
+}
diff --git a/NetworkDiagram/MainWindow.xaml b/NetworkDiagram/MainWindow.xaml
deleted file mode 100644
index 83c8967..0000000
--- a/NetworkDiagram/MainWindow.xaml
+++ /dev/null
@@ -1,138 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- EN
- CZ
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/NetworkDiagram/MainWindow.xaml.cs b/NetworkDiagram/MainWindow.xaml.cs
deleted file mode 100644
index 44eab67..0000000
--- a/NetworkDiagram/MainWindow.xaml.cs
+++ /dev/null
@@ -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 _deviceTemplates = [];
- private Diagram _currentDiagram = new();
-
- // Selection & Dragging
- private readonly HashSet _selectedElements = [];
- private Point _dragStartPoint;
- private bool _isDraggingDevices;
- private bool _isSelectingArea;
- private Point _selectionStartPoint;
-
- private ConnectionType? _activeTool;
- private PlacedDevice? _firstDeviceForConnection;
-
- private readonly Dictionary _connectionLines = new();
- private readonly Dictionary _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>(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()) 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(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 Devices { get; set; } = [];
- public List Connections { get; set; } = [];
- }
- public class ConnectionSaveModel {
- public int StartIndex { get; init; }
- public int EndIndex { get; set; }
- public ConnectionType Type { get; set; }
- }
-}
diff --git a/NetworkDiagram/Models/DiagramModels.cs b/NetworkDiagram/Models/DiagramModels.cs
index befdc9c..a8b639b 100644
--- a/NetworkDiagram/Models/DiagramModels.cs
+++ b/NetworkDiagram/Models/DiagramModels.cs
@@ -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 _ipAddresses = [];
public List 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 Devices { get; set; } = [];
public List 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 Devices { get; set; } = [];
+ public List Connections { get; set; } = [];
+ }
+ public class ConnectionSaveModel {
+ public int StartIndex { get; set; }
+ public int EndIndex { get; set; }
+ public ConnectionType Type { get; set; }
+ }
}
diff --git a/NetworkDiagram/NetworkDiagram.csproj b/NetworkDiagram/NetworkDiagram.csproj
index adc7acb..d3bb0c3 100644
--- a/NetworkDiagram/NetworkDiagram.csproj
+++ b/NetworkDiagram/NetworkDiagram.csproj
@@ -1,20 +1,25 @@
-
+
+
+ WinExe
+ net10.0
+ enable
+ enable
+ true
+
-
- WinExe
- net10.0-windows
- enable
- enable
- true
-
+
+
+
+
+
+
+
-
-
- PreserveNewest
-
-
-
- PreserveNewest
-
-
+
+
+
+
+ PreserveNewest
+
+
diff --git a/NetworkDiagram/Program.cs b/NetworkDiagram/Program.cs
new file mode 100644
index 0000000..37885a5
--- /dev/null
+++ b/NetworkDiagram/Program.cs
@@ -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()
+ .UsePlatformDetect()
+ .WithInterFont()
+ .LogToTrace();
+}
diff --git a/NetworkDiagramAvalonia/App.axaml b/NetworkDiagramAvalonia/App.axaml
new file mode 100644
index 0000000..744d668
--- /dev/null
+++ b/NetworkDiagramAvalonia/App.axaml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/NetworkDiagramAvalonia/App.axaml.cs b/NetworkDiagramAvalonia/App.axaml.cs
new file mode 100644
index 0000000..78fbf3e
--- /dev/null
+++ b/NetworkDiagramAvalonia/App.axaml.cs
@@ -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();
+ }
+}
\ No newline at end of file
diff --git a/NetworkDiagramAvalonia/Assets/avalonia-logo.ico b/NetworkDiagramAvalonia/Assets/avalonia-logo.ico
new file mode 100644
index 0000000..f7da8bb
Binary files /dev/null and b/NetworkDiagramAvalonia/Assets/avalonia-logo.ico differ
diff --git a/NetworkDiagramAvalonia/NetworkDiagramAvalonia.csproj b/NetworkDiagramAvalonia/NetworkDiagramAvalonia.csproj
new file mode 100644
index 0000000..a69279e
--- /dev/null
+++ b/NetworkDiagramAvalonia/NetworkDiagramAvalonia.csproj
@@ -0,0 +1,26 @@
+
+
+ WinExe
+ net10.0
+ enable
+ app.manifest
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+ None
+ All
+
+
+
+
diff --git a/NetworkDiagramAvalonia/Program.cs b/NetworkDiagramAvalonia/Program.cs
new file mode 100644
index 0000000..07d791b
--- /dev/null
+++ b/NetworkDiagramAvalonia/Program.cs
@@ -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()
+ .UsePlatformDetect()
+#if DEBUG
+ .WithDeveloperTools()
+#endif
+ .WithInterFont()
+ .LogToTrace();
+}
\ No newline at end of file
diff --git a/NetworkDiagramAvalonia/ViewLocator.cs b/NetworkDiagramAvalonia/ViewLocator.cs
new file mode 100644
index 0000000..a4ada0b
--- /dev/null
+++ b/NetworkDiagramAvalonia/ViewLocator.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using Avalonia.Controls;
+using Avalonia.Controls.Templates;
+using NetworkDiagramAvalonia.ViewModels;
+
+namespace NetworkDiagramAvalonia;
+
+///
+/// Given a view model, returns the corresponding view if possible.
+///
+[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;
+ }
+}
\ No newline at end of file
diff --git a/NetworkDiagramAvalonia/ViewModels/MainWindowViewModel.cs b/NetworkDiagramAvalonia/ViewModels/MainWindowViewModel.cs
new file mode 100644
index 0000000..96086ad
--- /dev/null
+++ b/NetworkDiagramAvalonia/ViewModels/MainWindowViewModel.cs
@@ -0,0 +1,6 @@
+namespace NetworkDiagramAvalonia.ViewModels;
+
+public partial class MainWindowViewModel : ViewModelBase
+{
+ public string Greeting { get; } = "Welcome to Avalonia!";
+}
\ No newline at end of file
diff --git a/NetworkDiagramAvalonia/ViewModels/ViewModelBase.cs b/NetworkDiagramAvalonia/ViewModels/ViewModelBase.cs
new file mode 100644
index 0000000..400f2c5
--- /dev/null
+++ b/NetworkDiagramAvalonia/ViewModels/ViewModelBase.cs
@@ -0,0 +1,7 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace NetworkDiagramAvalonia.ViewModels;
+
+public abstract class ViewModelBase : ObservableObject
+{
+}
\ No newline at end of file
diff --git a/NetworkDiagramAvalonia/Views/MainWindow.axaml b/NetworkDiagramAvalonia/Views/MainWindow.axaml
new file mode 100644
index 0000000..ab55adb
--- /dev/null
+++ b/NetworkDiagramAvalonia/Views/MainWindow.axaml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/NetworkDiagramAvalonia/Views/MainWindow.axaml.cs b/NetworkDiagramAvalonia/Views/MainWindow.axaml.cs
new file mode 100644
index 0000000..7c1c6a4
--- /dev/null
+++ b/NetworkDiagramAvalonia/Views/MainWindow.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace NetworkDiagramAvalonia.Views;
+
+public partial class MainWindow : Window
+{
+ public MainWindow()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/NetworkDiagramAvalonia/app.manifest b/NetworkDiagramAvalonia/app.manifest
new file mode 100644
index 0000000..e12dda8
--- /dev/null
+++ b/NetworkDiagramAvalonia/app.manifest
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+