Przeglądaj źródła

Directional guides and other stuff

CPKreuz 2 lat temu
rodzic
commit
946472ca84

+ 16 - 1
src/PixiEditor/Data/Localization/Languages/en.json

@@ -569,5 +569,20 @@
   "USE_SECONDARY_COLOR": "Use secondary color",
   "RIGHT_CLICK_MODE": "Right click mode",
   "ADD_PRIMARY_COLOR_TO_PALETTE": "Add primary color to palette",
-  "ADD_PRIMARY_COLOR_TO_PALETTE_DESCRIPTIVE": "Add primary color to current palette"
+  "ADD_PRIMARY_COLOR_TO_PALETTE_DESCRIPTIVE": "Add primary color to current palette",
+  "COLOR": "Color",
+  "GUIDES_MANAGER_TITLE": "Guides Manager",
+  "SAVE_GUIDES": "Save guides",
+  "OPEN_SAVED_BOOK": "Open saved guides",
+  "OPEN_GUIDES_MANAGER": "Open guides manager",
+  "OPEN_GUIDES_MANAGER_DESCRIPTIVE": "Open guides manager",
+  "LINE_GUIDE": "Line guide",
+  "VERTICAL_GUIDE": "Vertical guide",
+  "HORIZONTAL_GUIDE": "Horizontal guide",
+  "ADD_LINE_GUIDE": "Add line guide",
+  "ADD_LINE_GUIDE_DESCRIPTIVE": "Add line guide",
+  "ADD_VERTICAL_GUIDE": "Add vertical guide",
+  "ADD_VERTICAL_GUIDE_DESCRIPTIVE": "Add vertical guide",
+  "ADD_HORIZONTAL_GUIDE": "Add horizontal guide",
+  "ADD_HORIZONTAL_GUIDE_DESCRIPTIVE": "Add horizontal guide"
 }

BIN
src/PixiEditor/Images/Guides/Book.png


BIN
src/PixiEditor/Images/Guides/HorizontalGuide.png


BIN
src/PixiEditor/Images/Guides/LineGuide.png


BIN
src/PixiEditor/Images/Guides/VerticalGuide.png


+ 21 - 1
src/PixiEditor/Localization/LocalizedString.cs

@@ -28,8 +28,12 @@ public struct LocalizedString
     }
     public string Value { get; private set; }
 
+    public bool IsStatic { get; private set; }
+
     public object[] Parameters { get; set; }
 
+    public LocalizedString() { }
+
     public LocalizedString(string key)
     {
         Key = key;
@@ -41,6 +45,14 @@ public struct LocalizedString
         Key = key;
     }
 
+    public static LocalizedString Static(string value)
+    {
+        var localized = new LocalizedString();
+        localized.IsStatic = true;
+        localized.Value = value;
+        return localized;
+    }
+
     public override string ToString()
     {
         return Value;
@@ -75,7 +87,15 @@ public struct LocalizedString
         return ApplyParameters(ILocalizationProvider.Current.CurrentLanguage.Locale[localizationKey]);
     }
 
-    private string GetLongString(int length) => string.Join(' ', Enumerable.Repeat("LaLaLaLaLa", length));
+    private string GetLongString(int length)
+    {
+        if (IsStatic)
+        {
+            return Key;
+        }
+
+        return string.Join(' ', Enumerable.Repeat("LaLaLaLaLa", length));
+    }
 
     private string ApplyParameters(string value)
     {

+ 158 - 0
src/PixiEditor/Models/DataHolders/Guides/DirectionalGuide.cs

@@ -0,0 +1,158 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Media;
+using PixiEditor.ViewModels.SubViewModels.Document;
+using PixiEditor.Views.UserControls.Guides;
+
+namespace PixiEditor.Models.DataHolders.Guides;
+
+internal class DirectionalGuide : Guide
+{
+    private double offset;
+    private bool isDragging;
+    private Color color;
+    private Direction direction;
+
+    private bool IsVertical => direction == Direction.Vertical;
+
+    public override Control SettingsControl { get; }
+
+    public override string TypeNameKey => IsVertical ? "VERTICAL_GUIDE" : "HORIZONTAL_GUIDE";
+
+    public override string IconPath
+    {
+        get
+        {
+            var name = IsVertical ? "VerticalGuide" : "HorizontalGuide";
+            return $"/Images/Guides/{name}.png";
+        }
+    }
+
+    public double Offset
+    {
+        get => offset;
+        set
+        {
+            if (SetProperty(ref offset, value))
+            {
+                InvalidateVisual();
+            }
+        }
+    }
+
+    public Color Color
+    {
+        get => color;
+        set
+        {
+            if (SetProperty(ref color, value))
+            {
+                InvalidateVisual();
+            }
+        }
+    }
+
+    public string OffsetName => IsVertical ? "X" : "Y";
+
+    public DirectionalGuide(DocumentViewModel document, Direction direction) : base(document)
+    {
+        Color = Colors.CadetBlue;
+        this.direction = direction;
+        SettingsControl = new DirectionalGuideSettings(this);
+    }
+
+    public override void Draw(DrawingContext context, GuideRenderer renderer)
+    {
+        var pen = new Pen(new SolidColorBrush(Color), renderer.ScreenUnit * (IsEditing ? 3 : 1.5d));
+
+        var pointA = IsVertical ? new Point(Offset, 0) : new Point(0, Offset);
+        var pointB = IsVertical ? new Point(Offset, Document.SizeBindable.Y) : new Point(Document.SizeBindable.X, Offset);
+
+        context.DrawLine(pen, pointA, pointB);
+    }
+
+    protected override void RendererAttached(GuideRenderer renderer)
+    {
+        renderer.MouseEnter += Renderer_MouseEnter;
+        renderer.MouseLeave += Renderer_MouseLeave;
+
+        renderer.MouseLeftButtonDown += Renderer_MouseLeftButtonDown;
+        renderer.MouseMove += Renderer_MouseMove;
+        renderer.MouseLeftButtonUp += Renderer_MouseLeftButtonUp;
+    }
+
+    private void Renderer_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
+    {
+        if (IsEditing)
+        {
+            var renderer = (GuideRenderer)sender;
+            renderer.Cursor = direction == Direction.Vertical ? Cursors.ScrollWE : Cursors.ScrollNS;
+        }
+    }
+
+    private void Renderer_MouseLeave(object sender, MouseEventArgs e)
+    {
+        var renderer = (GuideRenderer)sender;
+        renderer.Cursor = null;
+    }
+
+    private void Renderer_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
+    {
+        if (!IsEditing)
+        {
+            return;
+        }
+
+        var renderer = (GuideRenderer)sender;
+        e.Handled = true;
+        isDragging = true;
+        Mouse.Capture(renderer);
+    }
+
+    private void Renderer_MouseMove(object sender, MouseEventArgs e)
+    {
+        if (!isDragging)
+        {
+            return;
+        }
+
+        e.Handled = true;
+
+        var renderer = (GuideRenderer)sender;
+        var position = e.GetPosition(renderer);
+
+        var offset = IsVertical ? position.X : position.Y;
+        if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
+        {
+            Offset = Math.Round(offset, MidpointRounding.AwayFromZero);
+        }
+        else
+        {
+            Offset = Math.Round(offset * 2, MidpointRounding.AwayFromZero) / 2;
+        }
+    }
+
+    private void Renderer_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
+    {
+        if (!isDragging)
+        {
+            return;
+        }
+
+        Mouse.Capture(null);
+        isDragging = false;
+        e.Handled = true;
+    }
+
+    public enum Direction
+    {
+        Vertical,
+        Horizontal
+    }
+}

+ 3 - 2
src/PixiEditor/Models/DataHolders/Guides/Guide.cs

@@ -6,6 +6,7 @@ using System.Threading.Tasks;
 using System.Windows.Controls;
 using System.Windows.Media;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Localization;
 using PixiEditor.ViewModels.SubViewModels.Document;
 using PixiEditor.Views.UserControls;
 using PixiEditor.Views.UserControls.Guides;
@@ -57,7 +58,7 @@ namespace PixiEditor.Models.DataHolders.Guides
             }
         }
 
-        public bool ShowEditable
+        public bool IsEditing
         {
             get => showEditable;
             set
@@ -75,7 +76,7 @@ namespace PixiEditor.Models.DataHolders.Guides
 
         public virtual string IconPath => $"/Images/Guides/{GetType().Name}.png";
 
-        public string DisplayName => !string.IsNullOrWhiteSpace(Name) ? Name : TypeNameKey;
+        public LocalizedString DisplayName => !string.IsNullOrWhiteSpace(Name) ? LocalizedString.Static(Name) : TypeNameKey;
 
         protected IReadOnlyCollection<GuideRenderer> Renderers => renderers;
 

+ 211 - 15
src/PixiEditor/Models/DataHolders/Guides/LineGuide.cs

@@ -5,27 +5,51 @@ using System.Text;
 using System.Threading.Tasks;
 using System.Windows;
 using System.Windows.Controls;
+using System.Windows.Input;
 using System.Windows.Media;
-using Hardware.Info;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.ViewModels.SubViewModels.Document;
 using PixiEditor.Views.UserControls;
 using PixiEditor.Views.UserControls.Guides;
+using PixiEditor.Views.UserControls.Overlays.TransformOverlay;
 
 namespace PixiEditor.Models.DataHolders.Guides
 {
     internal class LineGuide : Guide
     {
         private VecD position;
+        private bool? isMoving;
         private double rotation;
         private Color color;
+        private double x;
+        private double y;
+        private Pen blackPen = new Pen(Brushes.Black, 1);
+        private GuideRenderer focusedRenderer;
 
-        public VecD Position
+        private PathGeometry rotateCursorGeometry = new()
         {
-            get => position;
+            Figures = (PathFigureCollection?)new PathFigureCollectionConverter()
+            .ConvertFrom("M -1.26 -0.455 Q 0 0.175 1.26 -0.455 L 1.12 -0.735 L 2.1 -0.7 L 1.54 0.105 L 1.4 -0.175 Q 0 0.525 -1.4 -0.175 L -1.54 0.105 L -2.1 -0.7 L -1.12 -0.735 Z"),
+        };
+
+        public double X
+        {
+            get => x;
+            set
+            {
+                if (SetProperty(ref x, value))
+                {
+                    InvalidateVisual();
+                }
+            }
+        }
+
+        public double Y
+        {
+            get => y;
             set
             {
-                if (SetProperty(ref position, value))
+                if (SetProperty(ref y, value))
                 {
                     InvalidateVisual();
                 }
@@ -69,32 +93,204 @@ namespace PixiEditor.Models.DataHolders.Guides
         public override void Draw(DrawingContext context, GuideRenderer renderer)
         {
             var documentSize = Document.SizeBindable;
-            var m = Math.Tan(Rotation);
 
             var penThickness = renderer.ScreenUnit;
 
             var brush = new SolidColorBrush(Color);
-            var pen = new Pen(brush, penThickness * 1.5d);
             var points = GetIntersectionsInside(documentSize);
-            context.DrawLine(pen, points.Item1, points.Item2);
 
-            if (ShowExtended || ShowEditable)
+            if (ShowExtended || IsEditing)
+            {
+                var scale = IsEditing ? 2 : 1;
+                var pen = new Pen(brush, penThickness * 2 * scale);
+                context.DrawLine(pen, points.Item1, points.Item2);
+                context.DrawEllipse(Brushes.Aqua, null, new Point(X, Y), penThickness * 3 * scale, penThickness * 3 * scale);
+            }
+            else
+            {
+                var pen = new Pen(brush, penThickness * 1.5d);
+                context.DrawLine(pen, points.Item1, points.Item2);
+            }
+
+            blackPen.Thickness = penThickness;
+            if (renderer == focusedRenderer)
+            {
+                context.DrawGeometry(Brushes.White, blackPen, rotateCursorGeometry);
+            }
+        }
+
+        private bool UpdateRotationCursor(VecD mousePos, bool show, GuideRenderer renderer)
+        {
+            if (!show)
+            {
+                rotateCursorGeometry.Transform = new ScaleTransform(0, 0);
+                return false;
+            }
+            else
             {
-                var scale = ShowEditable ? 6 : 3;
-                context.DrawEllipse(Brushes.Aqua, null, new Point(Position.X, Position.Y), penThickness * scale, penThickness * scale);
+                var matrix = new TranslateTransform(mousePos.X, mousePos.Y).Value;
+                var vec = mousePos - new VecD(X, Y);
+                matrix.RotateAt(vec.Angle * 180 / Math.PI - 90, mousePos.X, mousePos.Y);
+                matrix.ScaleAt(8 / renderer.ZoomboxScale, 8 / renderer.ZoomboxScale, mousePos.X, mousePos.Y);
+                vec = vec.Normalize();
+                matrix.Translate(vec.X, vec.Y);
+                rotateCursorGeometry.Transform = new MatrixTransform(matrix);
+                return true;
             }
         }
 
+        protected override void RendererAttached(GuideRenderer renderer)
+        {
+            renderer.MouseEnter += Renderer_MouseEnter;
+            renderer.MouseLeave += Renderer_MouseLeave;
+
+            renderer.MouseLeftButtonDown += Renderer_MouseLeftButtonDown;
+            renderer.MouseMove += Renderer_MouseMove;
+            renderer.MouseLeftButtonUp += Renderer_MouseLeftButtonUp;
+
+            renderer.MouseWheel += Renderer_MouseWheel;
+        }
+
+        private void Renderer_MouseEnter(object sender, MouseEventArgs e)
+        {
+            if (!IsEditing)
+            {
+                return;
+            }
+
+            e.Handled = true;
+            focusedRenderer = (GuideRenderer)sender;
+            var mousePos = e.GetPosition(focusedRenderer);
+            var shouldMove = ShouldMove(focusedRenderer, mousePos);
+            UpdateRotationCursor(TransformHelper.ToVecD(mousePos), !shouldMove, focusedRenderer);
+
+            if (shouldMove)
+            {
+                focusedRenderer.Cursor = Cursors.Cross;
+            }
+            else
+            {
+                focusedRenderer.Cursor = Cursors.None;
+            }
+
+            focusedRenderer.InvalidateVisual();
+        }
+
+        private void Renderer_MouseLeave(object sender, MouseEventArgs e)
+        {
+            focusedRenderer = (GuideRenderer)sender;
+            focusedRenderer.Cursor = null;
+            focusedRenderer.InvalidateVisual();
+            focusedRenderer = null;
+        }
+
+        private void Renderer_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
+        {
+            if (!IsEditing)
+            {
+                return;
+            }
+
+            e.Handled = true;
+            focusedRenderer = (GuideRenderer)sender;
+            Mouse.Capture(focusedRenderer);
+
+            var mousePos = e.GetPosition(focusedRenderer);
+            isMoving = ShouldMove(focusedRenderer, mousePos);
+        }
+
+        private void Renderer_MouseMove(object sender, MouseEventArgs e)
+        {
+            if (!IsEditing)
+            {
+                return;
+            }
+
+            var focusedRenderer = (GuideRenderer)sender;
+            var mousePos = e.GetPosition(focusedRenderer);
+            var x = mousePos.X;
+            var y = mousePos.Y;
+            var vecD = new VecD(x, y);
+
+            var shouldMove = ShouldMove(focusedRenderer, mousePos);
+
+            if (shouldMove)
+            {
+                focusedRenderer.Cursor = Cursors.Cross;
+            }
+            else
+            {
+                focusedRenderer.Cursor = Cursors.None;
+            }
+
+
+            if (isMoving == null)
+            {
+                UpdateRotationCursor(vecD, !shouldMove, focusedRenderer);
+                return;
+            }
+
+            focusedRenderer.InvalidateVisual();
+
+            if (isMoving.Value)
+            {
+                if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
+                {
+                    X = Math.Round(x, MidpointRounding.AwayFromZero);
+                    Y = Math.Round(y, MidpointRounding.AwayFromZero);
+                }
+                else
+                {
+                    X = Math.Round(x * 2, MidpointRounding.AwayFromZero) / 2;
+                    Y = Math.Round(y * 2, MidpointRounding.AwayFromZero) / 2;
+                }
+            }
+            else
+            {
+                Rotation = Math.Round((vecD - new VecD(X, Y)).Angle * -1 * (180 / Math.PI), 1, MidpointRounding.AwayFromZero);
+                UpdateRotationCursor(vecD, true, focusedRenderer);
+            }
+        }
+
+        private void Renderer_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
+        {
+            if (isMoving == null)
+            {
+                return;
+            }
+
+            Mouse.Capture(null);
+            e.Handled = true;
+            focusedRenderer = null;
+            isMoving = null;
+            var renderer = (GuideRenderer)sender;
+            renderer.Cursor = null;
+            renderer.InvalidateVisual();
+        }
+
+        private void Renderer_MouseWheel(object sender, MouseWheelEventArgs e)
+        {
+            var renderer = (GuideRenderer)sender;
+
+            if (ShouldMove(renderer, e.GetPosition(renderer)))
+            {
+                e.Handled = true;
+                Rotation += e.Delta / (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.LeftShift) ? 50 : 10);
+            }
+        }
+
+        private bool ShouldMove(GuideRenderer renderer, Point mousePos) => (mousePos - new Point(X, Y)).Length < 10 * renderer.ScreenUnit;
+
         private Point[] GetIntersections(VecI size)
         {
             var points = new Point[4];
 
-            var m = Math.Tan(Rotation);
+            var m = Math.Tan(Rotation * (Math.PI / 180));
 
-            points[0] = new Point(0, m * Position.X + Position.Y);
-            points[1] = new Point(Position.X + Position.Y / m, 0);
-            points[2] = new Point(size.X, -m * size.X + m * Position.X + Position.Y);
-            points[3] = new Point(Position.X + Position.Y / m - size.Y / m, size.Y);
+            points[0] = new Point(0, m * X + Y);
+            points[1] = new Point(X + Y / m, 0);
+            points[2] = new Point(size.X, -m * size.X + m * X + Y);
+            points[3] = new Point(X + Y / m - size.Y / m, size.Y);
 
             return points;
         }

+ 6 - 0
src/PixiEditor/PixiEditor.csproj

@@ -169,6 +169,7 @@
 		<None Remove="Images\Add-reference.png" />
 		<None Remove="Images\AnchorDot.png" />
 		<None Remove="Images\Arrow-right.png" />
+		<None Remove="Images\book.png" />
 		<None Remove="Images\Check-square.png" />
 		<None Remove="Images\CheckerTile.png" />
 		<None Remove="Images\ChevronDown.png" />
@@ -191,7 +192,9 @@
 		<None Remove="Images\Folder-add.png" />
 		<None Remove="Images\Folder.png" />
 		<None Remove="Images\Globe.png" />
+		<None Remove="Images\Guides\HorizontalGuide.png" />
 		<None Remove="Images\Guides\LineGuide.png" />
+		<None Remove="Images\Guides\VerticalGuide.png" />
 		<None Remove="Images\hard-drive.png" />
 		<None Remove="Images\Layer-add.png" />
 		<None Remove="Images\Lock-alpha.png" />
@@ -265,6 +268,7 @@
 		<Resource Include="Images\Add-reference.png" />
 		<Resource Include="Images\AnchorDot.png" />
 		<Resource Include="Images\Arrow-right.png" />
+		<Resource Include="Images\Guides\Book.png" />
 		<Resource Include="Images\Check-square.png" />
 		<Resource Include="Images\CheckerTile.png" />
 		<Resource Include="Images\ChevronDown.png" />
@@ -288,7 +292,9 @@
 		<Resource Include="Images\Folder-add.png" />
 		<Resource Include="Images\Folder.png" />
 		<Resource Include="Images\Globe.png" />
+		<Resource Include="Images\Guides\HorizontalGuide.png" />
 		<Resource Include="Images\Guides\LineGuide.png" />
+		<Resource Include="Images\Guides\VerticalGuide.png" />
 		<Resource Include="Images\hard-drive.png" />
 		<Resource Include="Images\LanguageFlags\uk.png" />
 		<Resource Include="Images\Layer-add.png" />

+ 36 - 1
src/PixiEditor/Styles/ThemeStyle.xaml

@@ -1,5 +1,6 @@
 <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
-                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+                    xmlns:views="clr-namespace:PixiEditor.Views">
 
     <Style TargetType="{x:Type Control}">
         <Setter Property="Foreground" Value="White" />
@@ -371,6 +372,40 @@
         </Style.Triggers>
     </Style>
 
+    <Style TargetType="TextBox" x:Key="PlaceholderTextBox" BasedOn="{StaticResource DarkTextBoxStyle}">
+        <Setter Property="Template">
+            <Setter.Value>
+                <ControlTemplate TargetType="TextBox">
+                    <Grid>
+                        <Border Background="{TemplateBinding Background}"
+                                BorderBrush="{TemplateBinding BorderBrush}"
+                                BorderThickness="{TemplateBinding BorderThickness}"/>
+
+                        <TextBlock x:Name="PlaceholderText"
+                                       views:Translator.Key="{TemplateBinding Tag}"
+                                       Foreground="Gray"
+                                       Margin="5,0,0,0"
+                                       VerticalAlignment="Center"
+                                       Visibility="Collapsed"/>
+
+                        <ScrollViewer x:Name="PART_ContentHost"
+                                          HorizontalScrollBarVisibility="Hidden"
+                                          VerticalScrollBarVisibility="Hidden"/>
+                    </Grid>
+
+                    <ControlTemplate.Triggers>
+                        <Trigger Property="Text" Value="">
+                            <Setter TargetName="PlaceholderText" Property="Visibility" Value="Visible"/>
+                        </Trigger>
+                        <Trigger Property="Text" Value="{x:Null}">
+                            <Setter TargetName="PlaceholderText" Property="Visibility" Value="Visible"/>
+                        </Trigger>
+                    </ControlTemplate.Triggers>
+                </ControlTemplate>
+            </Setter.Value>
+        </Setter>
+    </Style>
+
     <Style TargetType="Button" x:Key="OpacityButtonStyle">
         <Setter Property="OverridesDefaultStyle" Value="True" />
         <Setter Property="Focusable" Value="False" />

+ 23 - 3
src/PixiEditor/ViewModels/SubViewModels/Main/GuidesViewModel.cs

@@ -6,6 +6,7 @@ using System.Threading.Tasks;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.DataHolders.Guides;
 using PixiEditor.Views.Dialogs.Guides;
+using Direction = PixiEditor.Models.DataHolders.Guides.DirectionalGuide.Direction;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
 
@@ -26,17 +27,36 @@ internal class GuidesViewModel : SubViewModel<ViewModelMain>
 
         guideManager.Show();
         guideManager.Activate();
+        guideManager.SelectGuide(openAt);
     }
 
-    [Command.Basic("PixiEditor.Guides.AddLineGuide", "ADD_LINE_GUIDE", "ADD_LINE_GUIDE_DESCRIPTIVE", CanExecute = "PixiEditor.HasDocument")]
+    [Command.Basic("PixiEditor.Guides.AddLineGuide", "ADD_LINE_GUIDE", "ADD_LINE_GUIDE_DESCRIPTIVE", CanExecute = "PixiEditor.HasDocument", IconPath = "Guides/LineGuide.png")]
     public void AddLineGuide()
     {
         var document = Owner.DocumentManagerSubViewModel.ActiveDocument;
 
+        var position = document.SizeBindable / 2;
         var guide = new LineGuide(document)
         {
-            Position = document.SizeBindable / 2,
-            Rotation = Math.PI / 4
+            X = position.X,
+            Y = position.Y,
+            Rotation = 45
+        };
+
+        document.Guides.Add(guide);
+        OpenGuideManager(^0);
+    }
+
+    [Command.Basic("PixiEditor.Guides.AddVerticalGuide", Direction.Vertical, "ADD_VERTICAL_GUIDE", "ADD_VERTICAL_GUIDE_DESCRIPTIVE", CanExecute = "PixiEditor.HasDocument", IconPath = "Guides/VerticalGuide.png")]
+    [Command.Basic("PixiEditor.Guides.AddHorizontalGuide", Direction.Horizontal, "ADD_HORIZONTAL_GUIDE", "ADD_HORIZONTAL_GUIDE_DESCRIPTIVE", CanExecute = "PixiEditor.HasDocument", IconPath = "Guides/HorizontalGuide.png")]
+    public void AddDirectionalGuide(Direction direction)
+    {
+        var document = Owner.DocumentManagerSubViewModel.ActiveDocument;
+
+        var documentSize = direction == Direction.Vertical ? document.SizeBindable.X : document.SizeBindable.Y;
+        var guide = new DirectionalGuide(document, direction)
+        {
+            Offset = documentSize / 2
         };
 
         document.Guides.Add(guide);

+ 64 - 8
src/PixiEditor/Views/Dialogs/Guides/GuidesManager.xaml

@@ -39,15 +39,20 @@
                 <ColumnDefinition/>
             </Grid.ColumnDefinitions>
 
-            <Border Margin="20" Background="{StaticResource DarkerAccentColor}" CornerRadius="5" BorderThickness="1" BorderBrush="{StaticResource MainColor}">
+            <Border Margin="20" Background="{StaticResource DarkerAccentColor}" 
+                    CornerRadius="5" BorderThickness="1" BorderBrush="{StaticResource MainColor}"
+                    MouseLeftButtonDown="Border_MouseLeftButtonDown">
                 <Grid>
                     <Grid.RowDefinitions>
                         <RowDefinition/>
-                        <RowDefinition Height="25"/>
+                        <RowDefinition Height="30"/>
                     </Grid.RowDefinitions>
 
-                    <ListView ItemsSource="{Binding ActiveDocument.Guides, Source={vm:MainVM DocumentManagerSVM}}" Margin="0,5"
-                              Background="Transparent" BorderThickness="0" x:Name="guideList">
+                    <ListView ItemsSource="{Binding ActiveDocument.Guides, Source={vm:MainVM DocumentManagerSVM}}"
+                              Margin="0,5"
+                              SelectionChanged="GuideSelectionChanged"
+                              Background="Transparent" BorderThickness="0" x:Name="guideList"
+                              SelectionMode="Single">
                         <ListView.ItemContainerStyle>
                             <Style TargetType="ListViewItem">
                                 <Setter Property="BorderBrush" Value="Transparent" />
@@ -94,7 +99,7 @@
                                     </Grid.ColumnDefinitions>
 
                                     <Image Source="{Binding IconPath}"/>
-                                    <TextBlock Grid.Column="1" Text="{Binding DisplayName}" Margin="5,0,0,0"/>
+                                    <TextBlock Grid.Column="1" views:Translator.LocalizedString="{Binding DisplayName, Mode=OneWay}" Margin="5,0,0,0"/>
                                     <StackPanel Grid.Column="2"></StackPanel>
                                 </Grid>
                             </DataTemplate>
@@ -103,9 +108,60 @@
 
                     <Border Grid.Row="1" BorderThickness="0,1,0,0" BorderBrush="{StaticResource MainColor}">
                         <DockPanel>
-                            <Button DockPanel.Dock="Left" Width="25"
-                                    Background="Transparent" Foreground="White"
-                                    Command="{cmds:Command PixiEditor.Guides.AddLineGuide}">LG</Button>
+                            <DockPanel.Resources>
+                                <Style TargetType="Button">
+                                    <Setter Property="Background" Value="Transparent"/>
+                                    <Setter Property="Foreground" Value="White"/>
+                                    <Setter Property="Width" Value="30"/>
+                                    <Setter Property="Padding" Value="5"/>
+                                    <Setter Property="BorderThickness" Value="0"/>
+
+                                    <Setter Property="Template">
+                                        <Setter.Value>
+                                            <ControlTemplate TargetType="Button">
+                                                <Border Background="{TemplateBinding Background}"
+                                                        BorderBrush="{TemplateBinding BorderBrush}"
+                                                        BorderThickness="{TemplateBinding BorderThickness}"
+                                                        Padding="{TemplateBinding Padding}"
+                                                        CornerRadius="{TemplateBinding Tag}">
+
+                                                    <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
+                                                </Border>
+                                            </ControlTemplate>
+                                        </Setter.Value>
+                                    </Setter>
+
+                                    <Style.Triggers>
+                                        <Trigger Property="IsMouseOver" Value="True">
+                                            <Setter Property="Background" Value="#15FFFFFF"/>
+                                        </Trigger>
+                                    </Style.Triggers>
+                                </Style>
+                            </DockPanel.Resources>
+
+                            <Button DockPanel.Dock="Left"
+                                    Command="{cmds:Command PixiEditor.Guides.AddLineGuide}" Tag="0,0,0,4.5"
+                                    views:Translator.TooltipKey="LINE_GUIDE">
+                                <Image Source="/Images/Guides/LineGuide.png"/>
+                            </Button>
+                            <Button DockPanel.Dock="Left"
+                                    Command="{cmds:Command PixiEditor.Guides.AddVerticalGuide}"
+                                    views:Translator.TooltipKey="VERTICAL_GUIDE">
+                                <Image Source="/Images/Guides/VerticalGuide.png"/>
+                            </Button>
+                            <Button DockPanel.Dock="Left"
+                                    Command="{cmds:Command PixiEditor.Guides.AddHorizontalGuide}"
+                                    views:Translator.TooltipKey="HORIZONTAL_GUIDE">
+                                <Image Source="/Images/Guides/HorizontalGuide.png"/>
+                            </Button>
+                            <Button DockPanel.Dock="Right"  Tag="0,0,4.5,0"
+                                    views:Translator.TooltipKey="SAVE_GUIDES">
+                                <Image Source="/Images/Guides/Book.png"/>
+                            </Button>
+                            <Button DockPanel.Dock="Right" HorizontalAlignment="Right"
+                                    views:Translator.TooltipKey="OPEN_SAVED_GUIDES">
+                                <Image Source="/Images/Save.png"/>
+                            </Button>
                         </DockPanel>
                     </Border>
                 </Grid>

+ 82 - 0
src/PixiEditor/Views/Dialogs/Guides/GuidesManager.xaml.cs

@@ -11,6 +11,9 @@ using System.Windows.Input;
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
 using System.Windows.Shapes;
+using PixiEditor.Models.Controllers;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.DataHolders.Guides;
 
 namespace PixiEditor.Views.Dialogs.Guides;
 /// <summary>
@@ -20,7 +23,10 @@ public partial class GuidesManager : Window
 {
     public GuidesManager()
     {
+        Owner = Application.Current.MainWindow;
         InitializeComponent();
+        PreviewKeyDown += OnKeyDown;
+        KeyUp += OnKeyUp;
     }
 
     private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
@@ -32,4 +38,80 @@ public partial class GuidesManager : Window
     {
         Hide();
     }
+
+    public void SelectGuide(Index guideIndex)
+    {
+        var guides = (WpfObservableRangeCollection<Guide>)guideList.ItemsSource;
+        guideList.SelectedIndex = guideIndex.GetOffset(ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument.Guides.Count);
+    }
+
+    private void GuideSelectionChanged(object sender, SelectionChangedEventArgs e)
+    {
+        if (e.RemovedItems.Count == 1)
+        {
+            var oldGuide = (Guide)e.RemovedItems[0];
+            oldGuide.IsEditing = false;
+        }
+
+
+        if (e.AddedItems.Count == 1)
+        {
+            var newGuide = (Guide)e.AddedItems[0];
+            newGuide.IsEditing = true;
+        }
+    }
+
+    private void Border_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
+    {
+        guideList.SelectedIndex = -1;
+    }
+
+    private void OnKeyDown(object sender, KeyEventArgs e)
+    {
+        if (!IsSupportedKey(e.Key))
+        {
+            return;
+        }
+
+        ShortcutController.BlockShortcutExecution("GuidesManager");
+        e.Handled = true;
+        var document = ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument;
+
+        switch (e.Key)
+        {
+            case Key.Delete when guideList.SelectedItem != null:
+                document.Guides.Remove((Guide)guideList.SelectedItem);
+                guideList.SelectedIndex = document.Guides.Count - 1;
+                break;
+            case Key.Up:
+                var iU = guideList.SelectedIndex - 1;
+                if (iU < 0)
+                {
+                    iU = document.Guides.Count - 1;
+                }
+                guideList.SelectedIndex = iU;
+                break;
+            case Key.Down:
+                var iD = guideList.SelectedIndex + 1;
+                if (iD >= guideList.Items.Count)
+                {
+                    iD = 0;
+                }
+                guideList.SelectedIndex = iD;
+                break;
+        }
+    }
+
+    private void OnKeyUp(object sender, KeyEventArgs e)
+    {
+        if (!IsSupportedKey(e.Key))
+        {
+            return;
+        }
+
+        ShortcutController.UnblockShortcutExecution("GuidesManager");
+        e.Handled = true;
+    }
+
+    private bool IsSupportedKey(Key key) => key == Key.Delete || key == Key.Up || key == Key.Down;
 }

+ 11 - 1
src/PixiEditor/Views/Translator.cs

@@ -115,6 +115,13 @@ public class Translator : UIElement
 
     private static void LocalizedStringPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
     {
+        var newValue = (LocalizedString)e.NewValue;
+
+        if (newValue.IsStatic)
+        {
+            d.SetValue(ValueProperty, ((LocalizedString)e.NewValue).Value);
+        }
+
         d.SetValue(KeyProperty, ((LocalizedString)e.NewValue).Key);
     }
 
@@ -180,7 +187,10 @@ public class Translator : UIElement
         }
         #endif
 
-        d.SetValue(ValueProperty, localizedString.Value);
+        if (localizedString.Key != null)
+        {
+            d.SetValue(ValueProperty, localizedString.Value);
+        }
     }
 
     public static void SetKey(DependencyObject element, string value)

+ 58 - 0
src/PixiEditor/Views/UserControls/Guides/DirectionalGuideSettings.xaml

@@ -0,0 +1,58 @@
+<UserControl x:Class="PixiEditor.Views.UserControls.Guides.DirectionalGuideSettings"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
+             xmlns:local="clr-namespace:PixiEditor.Views.UserControls.Guides"
+             xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
+             xmlns:behaviors="clr-namespace:PixiEditor.Helpers.Behaviours"
+             xmlns:colorpicker="clr-namespace:ColorPicker;assembly=ColorPicker"
+             xmlns:views="clr-namespace:PixiEditor.Views"
+             xmlns:uc="clr-namespace:PixiEditor.Views.UserControls"
+             mc:Ignorable="d" 
+             Foreground="White"
+             d:DesignHeight="450" d:DesignWidth="800">
+    <StackPanel Grid.IsSharedSizeScope="true">
+        <StackPanel.Resources>
+            <Style TargetType="Grid" x:Key="Child">
+                <Setter Property="Margin" Value="0,0,0,10"/>
+            </Style>
+        </StackPanel.Resources>
+
+        <Grid Style="{StaticResource Child}">
+            <Grid.ColumnDefinitions>
+                <ColumnDefinition Width="Auto" SharedSizeGroup="width" MinWidth="70"/>
+                <ColumnDefinition Width="Auto" MinWidth="200"/>
+            </Grid.ColumnDefinitions>
+            <TextBlock views:Translator.Key="NAME" Margin="0,0,10,0" TextAlignment="Right"/>
+            <TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
+                     Style="{StaticResource PlaceholderTextBox}" Tag="{Binding TypeNameKey}" Grid.Column="1">
+                <i:Interaction.Behaviors>
+                    <behaviors:GlobalShortcutFocusBehavior/>
+                    <behaviors:TextBoxFocusBehavior 
+                        SelectOnMouseClick="{Binding BehaveLikeSmallEmbeddedField, ElementName=uc}" 
+                        ConfirmOnEnter="{Binding BehaveLikeSmallEmbeddedField, ElementName=uc}"
+                        DeselectOnFocusLoss="True"/>
+                </i:Interaction.Behaviors>
+            </TextBox>
+        </Grid>
+
+        <Grid Style="{StaticResource Child}">
+            <Grid.ColumnDefinitions>
+                <ColumnDefinition Width="Auto" SharedSizeGroup="width"/>
+                <ColumnDefinition/>
+            </Grid.ColumnDefinitions>
+            <TextBlock views:Translator.Key="COLOR" Margin="0,0,10,0" TextAlignment="Right"/>
+            <colorpicker:PortableColorPicker Grid.Column="1" SelectedColor="{Binding Color, Mode=TwoWay}" Width="30" HorizontalAlignment="Left"/>
+        </Grid>
+
+        <Grid Style="{StaticResource Child}">
+            <Grid.ColumnDefinitions>
+                <ColumnDefinition Width="Auto" SharedSizeGroup="width"/>
+                <ColumnDefinition Width="Auto"/>
+            </Grid.ColumnDefinitions>
+            <TextBlock Text="{Binding OffsetName, FallbackValue=Offset}" Margin="0,0,10,0" TextAlignment="Right"/>
+            <uc:NumberInput Margin="0,0,10,0" Grid.Column="1" Value="{Binding Offset, Mode=TwoWay}" Width="50" HorizontalAlignment="Left"/>
+        </Grid>
+    </StackPanel>
+</UserControl>

+ 28 - 0
src/PixiEditor/Views/UserControls/Guides/DirectionalGuideSettings.xaml.cs

@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Navigation;
+using System.Windows.Shapes;
+using PixiEditor.Models.DataHolders.Guides;
+
+namespace PixiEditor.Views.UserControls.Guides;
+/// <summary>
+/// Interaction logic for DirectionalGuideSettings.xaml
+/// </summary>
+public partial class DirectionalGuideSettings : UserControl
+{
+    internal DirectionalGuideSettings(DirectionalGuide guide)
+    {
+        DataContext = guide;
+        InitializeComponent();
+    }
+}

+ 56 - 11
src/PixiEditor/Views/UserControls/Guides/LineGuideSettings.xaml

@@ -5,23 +5,68 @@
              xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
              xmlns:gModels="clr-namespace:PixiEditor.Models.DataHolders.Guides"
              xmlns:local="clr-namespace:PixiEditor.Views.UserControls.Guides"
+             xmlns:views="clr-namespace:PixiEditor.Views"
              xmlns:colorpicker="clr-namespace:ColorPicker;assembly=ColorPicker"
              xmlns:behaviors="clr-namespace:PixiEditor.Helpers.Behaviours"
              xmlns:i="http://schemas.microsoft.com/xaml/behaviors" xmlns:uc="clr-namespace:PixiEditor.Views.UserControls"
-             mc:Ignorable="d" 
+             mc:Ignorable="d"
+             Foreground="White"
              d:DesignHeight="450" d:DesignWidth="800">
-    <StackPanel>
-        <TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
-                 Style="{StaticResource DarkTextBoxStyle}">
-            <i:Interaction.Behaviors>
-                <behaviors:GlobalShortcutFocusBehavior/>
-                <behaviors:TextBoxFocusBehavior 
+    <StackPanel Grid.IsSharedSizeScope="true">
+        <StackPanel.Resources>
+            <Style TargetType="Grid" x:Key="Child">
+                <Setter Property="Margin" Value="0,0,0,10"/>
+            </Style>
+        </StackPanel.Resources>
+        
+        <Grid Style="{StaticResource Child}">
+            <Grid.ColumnDefinitions>
+                <ColumnDefinition Width="Auto" SharedSizeGroup="width" MinWidth="70"/>
+                <ColumnDefinition Width="Auto" MinWidth="200"/>
+            </Grid.ColumnDefinitions>
+            <TextBlock views:Translator.Key="NAME" Margin="0,0,10,0" TextAlignment="Right"/>
+            <TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
+                     Style="{StaticResource PlaceholderTextBox}" Tag="{Binding TypeNameKey}" Grid.Column="1">
+                <i:Interaction.Behaviors>
+                    <behaviors:GlobalShortcutFocusBehavior/>
+                    <behaviors:TextBoxFocusBehavior 
                         SelectOnMouseClick="{Binding BehaveLikeSmallEmbeddedField, ElementName=uc}" 
                         ConfirmOnEnter="{Binding BehaveLikeSmallEmbeddedField, ElementName=uc}"
                         DeselectOnFocusLoss="True"/>
-            </i:Interaction.Behaviors>
-        </TextBox>
-        <colorpicker:PortableColorPicker SelectedColor="{Binding Color, Mode=TwoWay}" Width="40" Height="25"/>
-        <uc:NumberInput Value="{Binding Rotation, Mode=TwoWay}"/>
+                </i:Interaction.Behaviors>
+            </TextBox>
+        </Grid>
+
+        <Grid Style="{StaticResource Child}">
+            <Grid.ColumnDefinitions>
+                <ColumnDefinition Width="Auto" SharedSizeGroup="width"/>
+                <ColumnDefinition/>
+            </Grid.ColumnDefinitions>
+            <TextBlock views:Translator.Key="COLOR" Margin="0,0,10,0" TextAlignment="Right"/>
+            <colorpicker:PortableColorPicker Grid.Column="1" SelectedColor="{Binding Color, Mode=TwoWay}" Width="30" HorizontalAlignment="Left"/>
+        </Grid>
+
+        <Grid Style="{StaticResource Child}">
+            <Grid.ColumnDefinitions>
+                <ColumnDefinition Width="Auto" SharedSizeGroup="width"/>
+                <ColumnDefinition Width="Auto"/>
+                <ColumnDefinition Width="Auto"/>
+                <ColumnDefinition Width="Auto"/>
+            </Grid.ColumnDefinitions>
+            <TextBlock Text="X" Margin="0,0,10,0" TextAlignment="Right"/>
+            <uc:NumberInput Margin="0,0,10,0" Grid.Column="1" Value="{Binding X, Mode=TwoWay}" Width="50" HorizontalAlignment="Left"/>
+
+            <TextBlock Grid.Column="2" Text="Y" Margin="0,0,10,0"/>
+            <uc:NumberInput Grid.Column="3" Value="{Binding Y, Mode=TwoWay}" Width="50" HorizontalAlignment="Left"/>
+        </Grid>
+        
+        <Grid>
+            <Grid.ColumnDefinitions>
+                <ColumnDefinition Width="Auto" SharedSizeGroup="width"/>
+                <ColumnDefinition/>
+            </Grid.ColumnDefinitions>
+            <TextBlock views:Translator.Key="ROTATION" Margin="0,0,10,0" TextAlignment="Right"/>
+            <uc:NumberInput Grid.Column="1" Value="{Binding Rotation, Mode=TwoWay}" Width="50" HorizontalAlignment="Left"/>
+        </Grid>
     </StackPanel>
 </UserControl>