changed settings saving (#3)
Reviewed-on: #3 Co-authored-by: Aleksandr Neychev <alexnejchev73@gmail.com>
This commit was merged in pull request #3.
This commit is contained in:
@@ -91,8 +91,17 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
|
|||||||
|
|
||||||
PopupWindowNative.Point position = atFixedPoint
|
PopupWindowNative.Point position = atFixedPoint
|
||||||
? PopupLayout.OnScreen(
|
? PopupLayout.OnScreen(
|
||||||
work, _settings.ScreenPosition, PopupLayout.ToPixels(_settings.ScreenMargin, scale), width, height)
|
work,
|
||||||
: PopupLayout.NearAnchor(anchor, AnchorSideForMode(), OffsetForMode(scale), width, height);
|
_settings.FixedPoint.Position,
|
||||||
|
PopupLayout.ToPixels(_settings.FixedPoint.Offset, scale),
|
||||||
|
width,
|
||||||
|
height)
|
||||||
|
: PopupLayout.NearAnchor(
|
||||||
|
anchor,
|
||||||
|
SideForMode(),
|
||||||
|
PopupLayout.ToPixels(_settings.Current.Offset, scale),
|
||||||
|
width,
|
||||||
|
height);
|
||||||
|
|
||||||
if (!Draw(text, position, width, height, PopupLayout.ToPixels(CornerRadius, scale)))
|
if (!Draw(text, position, width, height, PopupLayout.ToPixels(CornerRadius, scale)))
|
||||||
{
|
{
|
||||||
@@ -169,7 +178,7 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
|
|||||||
RoundTheCorners(bits, width, height, radius);
|
RoundTheCorners(bits, width, height, radius);
|
||||||
|
|
||||||
var size = new WindowNative.Size { Width = width, Height = height };
|
var size = new WindowNative.Size { Width = width, Height = height };
|
||||||
var alpha = (byte)Math.Clamp(Math.Round(_settings.Opacity * 255), 0, 255);
|
var alpha = (byte)Math.Clamp(Math.Round(_settings.Current.Opacity * 255), 0, 255);
|
||||||
|
|
||||||
return WindowNative.SetContent(Handle, at, size, memory, alpha);
|
return WindowNative.SetContent(Handle, at, size, memory, alpha);
|
||||||
}
|
}
|
||||||
@@ -193,7 +202,7 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
|
|||||||
|
|
||||||
private void Fill(IntPtr bits, int width, int height)
|
private void Fill(IntPtr bits, int width, int height)
|
||||||
{
|
{
|
||||||
Color background = _settings.BackgroundColor;
|
Color background = _settings.Current.BackgroundColor;
|
||||||
|
|
||||||
// Straight into the bitmap rather than through a brush: the pixels have to be
|
// Straight into the bitmap rather than through a brush: the pixels have to be
|
||||||
// written anyway to carry an alpha channel GDI would not touch
|
// written anyway to carry an alpha channel GDI would not touch
|
||||||
@@ -218,7 +227,7 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
|
|||||||
|
|
||||||
IntPtr previousFont = GdiNative.SelectObject(deviceContext, _font);
|
IntPtr previousFont = GdiNative.SelectObject(deviceContext, _font);
|
||||||
GdiNative.SetBkMode(deviceContext, GdiNative.TRANSPARENT);
|
GdiNative.SetBkMode(deviceContext, GdiNative.TRANSPARENT);
|
||||||
GdiNative.SetTextColor(deviceContext, GdiNative.ToColorRef(_settings.ForegroundColor));
|
GdiNative.SetTextColor(deviceContext, GdiNative.ToColorRef(_settings.Current.ForegroundColor));
|
||||||
|
|
||||||
GdiNative.DrawText(deviceContext, text, text.Length, ref bounds,
|
GdiNative.DrawText(deviceContext, text, text.Length, ref bounds,
|
||||||
GdiNative.DT_SINGLELINE | GdiNative.DT_CENTER | GdiNative.DT_VCENTER |
|
GdiNative.DT_SINGLELINE | GdiNative.DT_CENTER | GdiNative.DT_VCENTER |
|
||||||
@@ -333,13 +342,11 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
|
|||||||
return PopupLayout.AsAnchor(PopupWindowNative.GetCursorPosition());
|
return PopupLayout.AsAnchor(PopupWindowNative.GetCursorPosition());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every anchor mode has a side and an offset of its own
|
// The caret has two sides to choose from and the cursor has six, so each mode names
|
||||||
private AnchorSide AnchorSideForMode() =>
|
// its own side in its own terms
|
||||||
_settings.PlacementMode == PopupPlacementMode.AtCaret ? _settings.CaretSide : _settings.CursorSide;
|
private AnchorSide SideForMode() => _settings.PlacementMode == PopupPlacementMode.AtCaret
|
||||||
|
? _settings.AtCaret.Anchor
|
||||||
private int OffsetForMode(double scale) => PopupLayout.ToPixels(
|
: _settings.AtCursor.Side;
|
||||||
_settings.PlacementMode == PopupPlacementMode.AtCaret ? _settings.CaretOffset : _settings.CursorOffset,
|
|
||||||
scale);
|
|
||||||
|
|
||||||
private Size MeasureText(string text)
|
private Size MeasureText(string text)
|
||||||
{
|
{
|
||||||
@@ -364,11 +371,12 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The font is rebuilt only when the size in the settings or the monitor scale
|
// The font is rebuilt only when the size in the settings or the monitor scale
|
||||||
// changes: it is the one expensive thing a show does
|
// changes: it is the one expensive thing a show does. A switch of the placement
|
||||||
|
// mode counts as a change of the size, since the size belongs to the mode
|
||||||
private void EnsureFont(double scale)
|
private void EnsureFont(double scale)
|
||||||
{
|
{
|
||||||
if (_font != IntPtr.Zero &&
|
if (_font != IntPtr.Zero &&
|
||||||
Math.Abs(_fontSize - _settings.FontSize) < 0.01 &&
|
Math.Abs(_fontSize - _settings.Current.FontSize) < 0.01 &&
|
||||||
Math.Abs(_fontScale - scale) < 0.01)
|
Math.Abs(_fontScale - scale) < 0.01)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -376,7 +384,7 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
|
|||||||
|
|
||||||
ReleaseFont();
|
ReleaseFont();
|
||||||
|
|
||||||
_fontSize = _settings.FontSize;
|
_fontSize = _settings.Current.FontSize;
|
||||||
_fontScale = scale;
|
_fontScale = scale;
|
||||||
_font = GdiNative.CreateFont(_fontSize, scale);
|
_font = GdiNative.CreateFont(_fontSize, scale);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,18 +16,115 @@ public sealed class AppSettingsTests
|
|||||||
Assert.Equal("en", settings.Language);
|
Assert.Equal("en", settings.Language);
|
||||||
Assert.Equal(AppTheme.System, settings.Theme);
|
Assert.Equal(AppTheme.System, settings.Theme);
|
||||||
Assert.Equal(PopupPlacementMode.AtCursor, settings.PlacementMode);
|
Assert.Equal(PopupPlacementMode.AtCursor, settings.PlacementMode);
|
||||||
Assert.Equal(AnchorSide.BottomRight, settings.CursorSide);
|
|
||||||
Assert.Equal(16, settings.CursorOffset);
|
|
||||||
Assert.Equal(AnchorSide.BottomRight, settings.CaretSide);
|
|
||||||
Assert.Equal(16, settings.CaretOffset);
|
|
||||||
Assert.Equal(ScreenPosition.BottomRight, settings.ScreenPosition);
|
|
||||||
Assert.Equal(24, settings.ScreenMargin);
|
|
||||||
Assert.Equal(20, settings.FontSize);
|
|
||||||
Assert.Equal(0.9, settings.Opacity);
|
|
||||||
Assert.Equal(500, settings.DurationMilliseconds);
|
Assert.Equal(500, settings.DurationMilliseconds);
|
||||||
Assert.Equal(300, settings.CapsLockHoldMilliseconds);
|
Assert.Equal(300, settings.CapsLockHoldMilliseconds);
|
||||||
Assert.Equal(Color.FromArgb(0x20, 0x20, 0x20), settings.BackgroundColor);
|
}
|
||||||
Assert.Equal(Color.FromArgb(0xFF, 0xFF, 0xFF), settings.ForegroundColor);
|
|
||||||
|
[Fact]
|
||||||
|
public void Every_mode_starts_out_looking_the_same()
|
||||||
|
{
|
||||||
|
var settings = new AppSettings();
|
||||||
|
|
||||||
|
Assert.Equal(AnchorSide.BottomRight, settings.AtCursor.Side);
|
||||||
|
Assert.Equal(16, settings.AtCursor.Offset);
|
||||||
|
Assert.Equal(CaretSide.Right, settings.AtCaret.Side);
|
||||||
|
Assert.Equal(16, settings.AtCaret.Offset);
|
||||||
|
|
||||||
|
// The middle of the monitor, where there is no edge to stand off from
|
||||||
|
Assert.Equal(ScreenPosition.Center, settings.FixedPoint.Position);
|
||||||
|
Assert.Equal(0, settings.FixedPoint.Offset);
|
||||||
|
|
||||||
|
PopupModeSettings[] modes = [settings.AtCursor, settings.AtCaret, settings.FixedPoint];
|
||||||
|
|
||||||
|
Assert.All(modes, mode =>
|
||||||
|
{
|
||||||
|
Assert.Equal(20, mode.FontSize);
|
||||||
|
Assert.Equal(0.9, mode.Opacity);
|
||||||
|
Assert.Equal(Color.FromArgb(0x20, 0x20, 0x20), mode.BackgroundColor);
|
||||||
|
Assert.Equal(Color.FromArgb(0xFF, 0xFF, 0xFF), mode.ForegroundColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Three modes and three sets of settings: none of them is shared
|
||||||
|
Assert.Equal(3, modes.Distinct().Count());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(PopupPlacementMode.AtCursor)]
|
||||||
|
[InlineData(PopupPlacementMode.AtCaret)]
|
||||||
|
[InlineData(PopupPlacementMode.FixedPoint)]
|
||||||
|
public void The_chosen_mode_is_the_one_handed_out(PopupPlacementMode mode)
|
||||||
|
{
|
||||||
|
var settings = new AppSettings { PlacementMode = mode };
|
||||||
|
|
||||||
|
PopupModeSettings expected = mode switch
|
||||||
|
{
|
||||||
|
PopupPlacementMode.AtCaret => settings.AtCaret,
|
||||||
|
PopupPlacementMode.FixedPoint => settings.FixedPoint,
|
||||||
|
_ => settings.AtCursor,
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Same(expected, settings.Current);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The settings of a mode are its own: setting one up leaves the others alone.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This is the whole point of keeping them per mode. A look shared by the modes
|
||||||
|
/// meant setting it up again after every switch, and switching modes to see what
|
||||||
|
/// they do undid what had just been set up.
|
||||||
|
/// </remarks>
|
||||||
|
[Fact]
|
||||||
|
public void Setting_one_mode_up_leaves_the_others_where_they_were()
|
||||||
|
{
|
||||||
|
var settings = new AppSettings();
|
||||||
|
|
||||||
|
settings.AtCaret.FontSize = 12;
|
||||||
|
settings.AtCaret.Side = CaretSide.Left;
|
||||||
|
settings.AtCaret.BackgroundColor = Color.FromArgb(0x11, 0x22, 0x33);
|
||||||
|
|
||||||
|
Assert.Equal(20, settings.AtCursor.FontSize);
|
||||||
|
Assert.Equal(AnchorSide.BottomRight, settings.AtCursor.Side);
|
||||||
|
Assert.Equal(20, settings.FixedPoint.FontSize);
|
||||||
|
Assert.Equal(Color.FromArgb(0x20, 0x20, 0x20), settings.FixedPoint.BackgroundColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The mode in force decides what the look means, so a switch of it is a change of
|
||||||
|
// everything the window shows through the current mode
|
||||||
|
[Fact]
|
||||||
|
public void A_switch_of_the_mode_is_announced_as_a_change_of_the_current_one()
|
||||||
|
{
|
||||||
|
var settings = new AppSettings();
|
||||||
|
List<string?> changed = [];
|
||||||
|
settings.PropertyChanged += (_, e) => changed.Add(e.PropertyName);
|
||||||
|
|
||||||
|
settings.PlacementMode = PopupPlacementMode.FixedPoint;
|
||||||
|
|
||||||
|
Assert.Contains(nameof(AppSettings.PlacementMode), changed);
|
||||||
|
Assert.Contains(nameof(AppSettings.Current), changed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A change inside a mode is passed on as a change of the settings.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The service that writes the file listens to the settings alone. Without this, a
|
||||||
|
/// font size dragged in the window would never reach the disk.
|
||||||
|
/// </remarks>
|
||||||
|
[Fact]
|
||||||
|
public void A_change_inside_a_mode_is_announced_by_the_settings()
|
||||||
|
{
|
||||||
|
var settings = new AppSettings();
|
||||||
|
List<string?> changed = [];
|
||||||
|
settings.PropertyChanged += (_, e) => changed.Add(e.PropertyName);
|
||||||
|
|
||||||
|
settings.AtCaret.FontSize = 42;
|
||||||
|
settings.FixedPoint.Position = ScreenPosition.Bottom;
|
||||||
|
settings.AtCursor.Offset = 8;
|
||||||
|
|
||||||
|
Assert.Contains("AtCaret.FontSize", changed);
|
||||||
|
Assert.Contains("FixedPoint.Position", changed);
|
||||||
|
Assert.Contains("AtCursor.Offset", changed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The app must not change how the system behaves until it is asked to
|
// The app must not change how the system behaves until it is asked to
|
||||||
@@ -82,8 +179,60 @@ public sealed class AppSettingsTests
|
|||||||
Assert.Empty(changed);
|
Assert.Empty(changed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Duration and CapsLockHoldDelay are derived from other settings and have
|
/// <summary>
|
||||||
// no business being in the file
|
/// Pouring another instance in fills the modes in place rather than replacing them.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This is the agent's side of the connection: the fresh values arrive as a freshly
|
||||||
|
/// parsed instance. Replacing the mode objects would leave the popup bound to the
|
||||||
|
/// old ones.
|
||||||
|
/// </remarks>
|
||||||
|
[Fact]
|
||||||
|
public void Taking_other_settings_over_fills_the_modes_that_are_already_there()
|
||||||
|
{
|
||||||
|
var settings = new AppSettings();
|
||||||
|
CursorModeSettings cursor = settings.AtCursor;
|
||||||
|
|
||||||
|
var other = new AppSettings
|
||||||
|
{
|
||||||
|
Language = "ru",
|
||||||
|
Theme = AppTheme.Dark,
|
||||||
|
PlacementMode = PopupPlacementMode.FixedPoint,
|
||||||
|
DurationMilliseconds = 900,
|
||||||
|
UseCapsLockHotkey = true,
|
||||||
|
CapsLockHoldMilliseconds = 450,
|
||||||
|
};
|
||||||
|
|
||||||
|
other.AtCursor.Side = AnchorSide.TopLeft;
|
||||||
|
other.AtCursor.Offset = 5;
|
||||||
|
other.AtCursor.FontSize = 11;
|
||||||
|
other.AtCaret.Side = CaretSide.Left;
|
||||||
|
other.AtCaret.ForegroundColor = Color.FromArgb(0x0A, 0x0B, 0x0C);
|
||||||
|
other.FixedPoint.Position = ScreenPosition.Top;
|
||||||
|
other.FixedPoint.Offset = 64;
|
||||||
|
|
||||||
|
settings.CopyFrom(other);
|
||||||
|
|
||||||
|
Assert.Same(cursor, settings.AtCursor);
|
||||||
|
|
||||||
|
Assert.Equal("ru", settings.Language);
|
||||||
|
Assert.Equal(AppTheme.Dark, settings.Theme);
|
||||||
|
Assert.Equal(PopupPlacementMode.FixedPoint, settings.PlacementMode);
|
||||||
|
Assert.Equal(900, settings.DurationMilliseconds);
|
||||||
|
Assert.True(settings.UseCapsLockHotkey);
|
||||||
|
Assert.Equal(450, settings.CapsLockHoldMilliseconds);
|
||||||
|
|
||||||
|
Assert.Equal(AnchorSide.TopLeft, settings.AtCursor.Side);
|
||||||
|
Assert.Equal(5, settings.AtCursor.Offset);
|
||||||
|
Assert.Equal(11, settings.AtCursor.FontSize);
|
||||||
|
Assert.Equal(CaretSide.Left, settings.AtCaret.Side);
|
||||||
|
Assert.Equal(Color.FromArgb(0x0A, 0x0B, 0x0C), settings.AtCaret.ForegroundColor);
|
||||||
|
Assert.Equal(ScreenPosition.Top, settings.FixedPoint.Position);
|
||||||
|
Assert.Equal(64, settings.FixedPoint.Offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Derived values and the modes handed out for convenience are computed from what
|
||||||
|
// is stored and have no business being in the file themselves
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Derived_values_stay_out_of_the_file()
|
public void Derived_values_stay_out_of_the_file()
|
||||||
{
|
{
|
||||||
@@ -93,10 +242,14 @@ public sealed class AppSettingsTests
|
|||||||
|
|
||||||
Assert.DoesNotContain(nameof(AppSettings.Duration), names);
|
Assert.DoesNotContain(nameof(AppSettings.Duration), names);
|
||||||
Assert.DoesNotContain(nameof(AppSettings.CapsLockHoldDelay), names);
|
Assert.DoesNotContain(nameof(AppSettings.CapsLockHoldDelay), names);
|
||||||
|
Assert.DoesNotContain(nameof(AppSettings.Current), names);
|
||||||
|
|
||||||
// What they are derived from, on the other hand, has to be stored
|
// What they are derived from, on the other hand, has to be stored
|
||||||
Assert.Contains(nameof(AppSettings.DurationMilliseconds), names);
|
Assert.Contains(nameof(AppSettings.DurationMilliseconds), names);
|
||||||
Assert.Contains(nameof(AppSettings.CapsLockHoldMilliseconds), names);
|
Assert.Contains(nameof(AppSettings.CapsLockHoldMilliseconds), names);
|
||||||
|
Assert.Contains(nameof(AppSettings.AtCursor), names);
|
||||||
|
Assert.Contains(nameof(AppSettings.AtCaret), names);
|
||||||
|
Assert.Contains(nameof(AppSettings.FixedPoint), names);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -118,6 +271,11 @@ public sealed class AppSettingsTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Names of the settings the user is able to change.</summary>
|
/// <summary>Names of the settings the user is able to change.</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The modes are handed out rather than assigned — they are filled in place — so
|
||||||
|
/// they are not among these. What is inside them is checked by
|
||||||
|
/// <see cref="PopupModeSettingsTests"/>.
|
||||||
|
/// </remarks>
|
||||||
internal static IEnumerable<string> WritablePropertyNames() =>
|
internal static IEnumerable<string> WritablePropertyNames() =>
|
||||||
typeof(AppSettings).GetProperties()
|
typeof(AppSettings).GetProperties()
|
||||||
.Where(property => property.CanWrite)
|
.Where(property => property.CanWrite)
|
||||||
|
|||||||
@@ -0,0 +1,289 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text.Json;
|
||||||
|
using CursorLang.Core.Models;
|
||||||
|
|
||||||
|
namespace CursorLang.Core.Tests.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The settings of a single placement mode: what the popup looks like there and how
|
||||||
|
/// far from its anchor it sits.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PopupModeSettingsTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void The_cursor_mode_starts_out_below_and_right_of_the_pointer()
|
||||||
|
{
|
||||||
|
var mode = new CursorModeSettings();
|
||||||
|
|
||||||
|
Assert.Equal(AnchorSide.BottomRight, mode.Side);
|
||||||
|
Assert.Equal(16, mode.Offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Next to the caret the popup goes beside it, and to the right by default.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Above or below the caret is where the next line of the text is, so those sides
|
||||||
|
/// are not on offer at all — the type has the two of them and no more.
|
||||||
|
/// </remarks>
|
||||||
|
[Fact]
|
||||||
|
public void The_caret_mode_starts_out_to_the_right_of_the_caret()
|
||||||
|
{
|
||||||
|
var mode = new CaretModeSettings();
|
||||||
|
|
||||||
|
Assert.Equal(CaretSide.Right, mode.Side);
|
||||||
|
Assert.Equal(16, mode.Offset);
|
||||||
|
Assert.Equal([CaretSide.Left, CaretSide.Right], Enum.GetValues<CaretSide>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(CaretSide.Left, AnchorSide.Left)]
|
||||||
|
[InlineData(CaretSide.Right, AnchorSide.Right)]
|
||||||
|
public void The_side_of_the_caret_lines_the_popup_up_with_it(CaretSide side, AnchorSide expected)
|
||||||
|
{
|
||||||
|
var mode = new CaretModeSettings { Side = side };
|
||||||
|
|
||||||
|
Assert.Equal(expected, mode.Anchor);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void A_changed_side_of_the_caret_is_a_change_of_what_it_lines_up_with()
|
||||||
|
{
|
||||||
|
var mode = new CaretModeSettings();
|
||||||
|
List<string?> changed = [];
|
||||||
|
mode.PropertyChanged += (_, e) => changed.Add(e.PropertyName);
|
||||||
|
|
||||||
|
mode.Side = CaretSide.Left;
|
||||||
|
|
||||||
|
Assert.Contains(nameof(CaretModeSettings.Anchor), changed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The fixed point starts out in the middle of the monitor, where there is no edge
|
||||||
|
/// to stand off from.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void The_fixed_point_starts_out_in_the_middle_with_no_offset()
|
||||||
|
{
|
||||||
|
var mode = new FixedPointModeSettings();
|
||||||
|
|
||||||
|
Assert.Equal(ScreenPosition.Center, mode.Position);
|
||||||
|
Assert.Equal(0, mode.Offset);
|
||||||
|
Assert.False(mode.IsAtAnEdge);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A setting that is not shown must not be one that still applies
|
||||||
|
[Fact]
|
||||||
|
public void Moving_the_fixed_point_to_the_middle_drops_the_offset()
|
||||||
|
{
|
||||||
|
var mode = new FixedPointModeSettings { Position = ScreenPosition.TopLeft, Offset = 40 };
|
||||||
|
|
||||||
|
Assert.True(mode.IsAtAnEdge);
|
||||||
|
|
||||||
|
mode.Position = ScreenPosition.Center;
|
||||||
|
|
||||||
|
Assert.Equal(0, mode.Offset);
|
||||||
|
Assert.False(mode.IsAtAnEdge);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(ScreenPosition.TopLeft)]
|
||||||
|
[InlineData(ScreenPosition.Top)]
|
||||||
|
[InlineData(ScreenPosition.TopRight)]
|
||||||
|
[InlineData(ScreenPosition.BottomLeft)]
|
||||||
|
[InlineData(ScreenPosition.Bottom)]
|
||||||
|
[InlineData(ScreenPosition.BottomRight)]
|
||||||
|
public void Away_from_the_middle_the_offset_is_kept(ScreenPosition position)
|
||||||
|
{
|
||||||
|
var mode = new FixedPointModeSettings { Position = position, Offset = 40 };
|
||||||
|
|
||||||
|
Assert.Equal(40, mode.Offset);
|
||||||
|
Assert.True(mode.IsAtAnEdge);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void A_changed_place_of_the_fixed_point_is_a_change_of_having_an_edge()
|
||||||
|
{
|
||||||
|
var mode = new FixedPointModeSettings();
|
||||||
|
List<string?> changed = [];
|
||||||
|
mode.PropertyChanged += (_, e) => changed.Add(e.PropertyName);
|
||||||
|
|
||||||
|
mode.Position = ScreenPosition.Bottom;
|
||||||
|
|
||||||
|
Assert.Contains(nameof(FixedPointModeSettings.IsAtAnEdge), changed);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(WritableProperties), typeof(CursorModeSettings))]
|
||||||
|
[MemberData(nameof(WritableProperties), typeof(CaretModeSettings))]
|
||||||
|
[MemberData(nameof(WritableProperties), typeof(FixedPointModeSettings))]
|
||||||
|
public void A_changed_setting_is_announced_to_subscribers(Type type, string propertyName)
|
||||||
|
{
|
||||||
|
PopupModeSettings mode = Create(type);
|
||||||
|
List<string?> changed = [];
|
||||||
|
mode.PropertyChanged += (_, e) => changed.Add(e.PropertyName);
|
||||||
|
|
||||||
|
SetDifferentValue(mode, propertyName);
|
||||||
|
|
||||||
|
Assert.Contains(propertyName, changed);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(WritableProperties), typeof(CursorModeSettings))]
|
||||||
|
[MemberData(nameof(WritableProperties), typeof(CaretModeSettings))]
|
||||||
|
[MemberData(nameof(WritableProperties), typeof(FixedPointModeSettings))]
|
||||||
|
public void Writing_the_same_value_leaves_subscribers_alone(Type type, string propertyName)
|
||||||
|
{
|
||||||
|
PopupModeSettings mode = Create(type);
|
||||||
|
PropertyInfo property = type.GetProperty(propertyName)!;
|
||||||
|
|
||||||
|
List<string?> changed = [];
|
||||||
|
mode.PropertyChanged += (_, e) => changed.Add(e.PropertyName);
|
||||||
|
|
||||||
|
property.SetValue(mode, property.GetValue(mode));
|
||||||
|
|
||||||
|
Assert.Empty(changed);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void The_cursor_mode_takes_another_one_over_whole()
|
||||||
|
{
|
||||||
|
var mode = new CursorModeSettings();
|
||||||
|
var other = new CursorModeSettings
|
||||||
|
{
|
||||||
|
Side = AnchorSide.Left,
|
||||||
|
Offset = 3,
|
||||||
|
FontSize = 41,
|
||||||
|
Opacity = 0.25,
|
||||||
|
BackgroundColor = Color.FromArgb(0x01, 0x02, 0x03),
|
||||||
|
ForegroundColor = Color.FromArgb(0x04, 0x05, 0x06),
|
||||||
|
};
|
||||||
|
|
||||||
|
mode.CopyFrom(other);
|
||||||
|
|
||||||
|
Assert.Equal(AnchorSide.Left, mode.Side);
|
||||||
|
Assert.Equal(3, mode.Offset);
|
||||||
|
Assert.Equal(41, mode.FontSize);
|
||||||
|
Assert.Equal(0.25, mode.Opacity);
|
||||||
|
Assert.Equal(Color.FromArgb(0x01, 0x02, 0x03), mode.BackgroundColor);
|
||||||
|
Assert.Equal(Color.FromArgb(0x04, 0x05, 0x06), mode.ForegroundColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void The_caret_mode_takes_another_one_over_whole()
|
||||||
|
{
|
||||||
|
var mode = new CaretModeSettings();
|
||||||
|
var other = new CaretModeSettings { Side = CaretSide.Left, Offset = 2, FontSize = 15 };
|
||||||
|
|
||||||
|
mode.CopyFrom(other);
|
||||||
|
|
||||||
|
Assert.Equal(CaretSide.Left, mode.Side);
|
||||||
|
Assert.Equal(2, mode.Offset);
|
||||||
|
Assert.Equal(15, mode.FontSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void The_fixed_point_takes_another_one_over_whole()
|
||||||
|
{
|
||||||
|
var mode = new FixedPointModeSettings();
|
||||||
|
var other = new FixedPointModeSettings
|
||||||
|
{
|
||||||
|
Position = ScreenPosition.Bottom,
|
||||||
|
Offset = 120,
|
||||||
|
FontSize = 60,
|
||||||
|
Opacity = 0.5,
|
||||||
|
BackgroundColor = Color.FromArgb(0x07, 0x08, 0x09),
|
||||||
|
ForegroundColor = Color.FromArgb(0x0A, 0x0B, 0x0C),
|
||||||
|
};
|
||||||
|
|
||||||
|
mode.CopyFrom(other);
|
||||||
|
|
||||||
|
Assert.Equal(ScreenPosition.Bottom, mode.Position);
|
||||||
|
Assert.Equal(120, mode.Offset);
|
||||||
|
Assert.Equal(60, mode.FontSize);
|
||||||
|
Assert.Equal(0.5, mode.Opacity);
|
||||||
|
Assert.Equal(Color.FromArgb(0x07, 0x08, 0x09), mode.BackgroundColor);
|
||||||
|
Assert.Equal(Color.FromArgb(0x0A, 0x0B, 0x0C), mode.ForegroundColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Taking over a mode that sits in the middle takes its lack of an offset over too
|
||||||
|
[Fact]
|
||||||
|
public void Taking_over_a_fixed_point_in_the_middle_leaves_no_offset()
|
||||||
|
{
|
||||||
|
var mode = new FixedPointModeSettings { Position = ScreenPosition.Top, Offset = 32 };
|
||||||
|
|
||||||
|
mode.CopyFrom(new FixedPointModeSettings());
|
||||||
|
|
||||||
|
Assert.Equal(ScreenPosition.Center, mode.Position);
|
||||||
|
Assert.Equal(0, mode.Offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// What a mode works out from what it stores is not stored itself
|
||||||
|
[Fact]
|
||||||
|
public void Derived_values_of_a_mode_stay_out_of_the_file()
|
||||||
|
{
|
||||||
|
List<string> caret = Stored(new CaretModeSettings());
|
||||||
|
List<string> fixedPoint = Stored(new FixedPointModeSettings());
|
||||||
|
|
||||||
|
Assert.DoesNotContain(nameof(CaretModeSettings.Anchor), caret);
|
||||||
|
Assert.DoesNotContain(nameof(FixedPointModeSettings.IsAtAnEdge), fixedPoint);
|
||||||
|
|
||||||
|
Assert.Contains(nameof(CaretModeSettings.Side), caret);
|
||||||
|
Assert.Contains(nameof(FixedPointModeSettings.Position), fixedPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void A_mode_reports_changes_as_INotifyPropertyChanged()
|
||||||
|
{
|
||||||
|
Assert.IsAssignableFrom<INotifyPropertyChanged>(new CursorModeSettings());
|
||||||
|
Assert.IsAssignableFrom<INotifyPropertyChanged>(new CaretModeSettings());
|
||||||
|
Assert.IsAssignableFrom<INotifyPropertyChanged>(new FixedPointModeSettings());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TheoryData<Type, string> WritableProperties(Type type)
|
||||||
|
{
|
||||||
|
var data = new TheoryData<Type, string>();
|
||||||
|
|
||||||
|
foreach (string name in WritablePropertyNames(type))
|
||||||
|
{
|
||||||
|
data.Add(type, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Names of the settings of a mode the user is able to change.</summary>
|
||||||
|
internal static IEnumerable<string> WritablePropertyNames(Type type) =>
|
||||||
|
type.GetProperties().Where(property => property.CanWrite).Select(property => property.Name);
|
||||||
|
|
||||||
|
private static PopupModeSettings Create(Type type) => (PopupModeSettings)Activator.CreateInstance(type)!;
|
||||||
|
|
||||||
|
private static List<string> Stored<TMode>(TMode mode) where TMode : PopupModeSettings
|
||||||
|
{
|
||||||
|
using JsonDocument document = JsonSerializer.SerializeToDocument(mode);
|
||||||
|
|
||||||
|
return [.. document.RootElement.EnumerateObject().Select(property => property.Name)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// A value guaranteed to differ from the current one: each kind of setting has its
|
||||||
|
// own way of differing
|
||||||
|
private static void SetDifferentValue(PopupModeSettings mode, string propertyName)
|
||||||
|
{
|
||||||
|
PropertyInfo property = mode.GetType().GetProperty(propertyName)!;
|
||||||
|
|
||||||
|
object next = property.GetValue(mode) switch
|
||||||
|
{
|
||||||
|
double number => number + 1,
|
||||||
|
Color color => Color.FromArgb((byte)(color.R + 1), color.G, color.B),
|
||||||
|
Enum value => Enum.GetValues(value.GetType())
|
||||||
|
.Cast<object>()
|
||||||
|
.First(other => !Equals(other, value)),
|
||||||
|
|
||||||
|
var other => throw new NotSupportedException($"Unknown kind of setting: {other?.GetType()}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
property.SetValue(mode, next);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -108,6 +108,11 @@ public sealed class StringsTests
|
|||||||
data.Add(key);
|
data.Add(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (string key in EnumKeysOf<CaretSide>())
|
||||||
|
{
|
||||||
|
data.Add(key);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (string key in EnumKeysOf<ScreenPosition>())
|
foreach (string key in EnumKeysOf<ScreenPosition>())
|
||||||
{
|
{
|
||||||
data.Add(key);
|
data.Add(key);
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ public sealed class CapsLockSwitchCoordinatorTests
|
|||||||
int startsBefore = hotkey.StartCalls;
|
int startsBefore = hotkey.StartCalls;
|
||||||
int stopsBefore = hotkey.StopCalls;
|
int stopsBefore = hotkey.StopCalls;
|
||||||
|
|
||||||
settings.FontSize = 44;
|
settings.Current.FontSize = 44;
|
||||||
settings.CapsLockHoldMilliseconds = 700;
|
settings.CapsLockHoldMilliseconds = 700;
|
||||||
|
|
||||||
Assert.Equal(startsBefore, hotkey.StartCalls);
|
Assert.Equal(startsBefore, hotkey.StartCalls);
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public sealed class SettingsServiceTests
|
|||||||
});
|
});
|
||||||
|
|
||||||
Assert.Equal(AppTheme.System, settings.Theme);
|
Assert.Equal(AppTheme.System, settings.Theme);
|
||||||
Assert.Equal(20, settings.FontSize);
|
Assert.Equal(20, settings.Current.FontSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@@ -72,10 +72,10 @@ public sealed class SettingsServiceTests
|
|||||||
using SettingsService service = Create(folder);
|
using SettingsService service = Create(folder);
|
||||||
AppSettings settings = service.Load();
|
AppSettings settings = service.Load();
|
||||||
|
|
||||||
settings.FontSize = 42;
|
|
||||||
settings.Theme = AppTheme.Dark;
|
settings.Theme = AppTheme.Dark;
|
||||||
settings.PlacementMode = PopupPlacementMode.AtCaret;
|
settings.PlacementMode = PopupPlacementMode.AtCaret;
|
||||||
settings.BackgroundColor = Color.FromArgb(0x11, 0x22, 0x33);
|
settings.Current.FontSize = 42;
|
||||||
|
settings.Current.BackgroundColor = Color.FromArgb(0x11, 0x22, 0x33);
|
||||||
settings.UseCapsLockHotkey = true;
|
settings.UseCapsLockHotkey = true;
|
||||||
|
|
||||||
service.Save();
|
service.Save();
|
||||||
@@ -87,10 +87,10 @@ public sealed class SettingsServiceTests
|
|||||||
return service.Load();
|
return service.Load();
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.Equal(42, restored.FontSize);
|
Assert.Equal(42, restored.Current.FontSize);
|
||||||
Assert.Equal(AppTheme.Dark, restored.Theme);
|
Assert.Equal(AppTheme.Dark, restored.Theme);
|
||||||
Assert.Equal(PopupPlacementMode.AtCaret, restored.PlacementMode);
|
Assert.Equal(PopupPlacementMode.AtCaret, restored.PlacementMode);
|
||||||
Assert.Equal(Color.FromArgb(0x11, 0x22, 0x33), restored.BackgroundColor);
|
Assert.Equal(Color.FromArgb(0x11, 0x22, 0x33), restored.Current.BackgroundColor);
|
||||||
Assert.True(restored.UseCapsLockHotkey);
|
Assert.True(restored.UseCapsLockHotkey);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ public sealed class SettingsServiceTests
|
|||||||
using SettingsService service = Create(folder);
|
using SettingsService service = Create(folder);
|
||||||
AppSettings settings = service.Load();
|
AppSettings settings = service.Load();
|
||||||
settings.Theme = AppTheme.Dark;
|
settings.Theme = AppTheme.Dark;
|
||||||
settings.BackgroundColor = Color.FromArgb(0x20, 0x20, 0x20);
|
settings.Current.BackgroundColor = Color.FromArgb(0x20, 0x20, 0x20);
|
||||||
service.Save();
|
service.Save();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ public sealed class SettingsServiceTests
|
|||||||
AppSettings settings = service.Load();
|
AppSettings settings = service.Load();
|
||||||
service.TrackChanges();
|
service.TrackChanges();
|
||||||
|
|
||||||
settings.FontSize = 33;
|
settings.Current.FontSize = 33;
|
||||||
|
|
||||||
// Right after the edit there is nothing on disk yet: the write is deferred
|
// Right after the edit there is nothing on disk yet: the write is deferred
|
||||||
Assert.False(File.Exists(path));
|
Assert.False(File.Exists(path));
|
||||||
@@ -156,7 +156,7 @@ public sealed class SettingsServiceTests
|
|||||||
|
|
||||||
for (int i = 0; i < 10; i++)
|
for (int i = 0; i < 10; i++)
|
||||||
{
|
{
|
||||||
settings.Opacity = 0.5 + (i * 0.01);
|
settings.Current.Opacity = 0.5 + (i * 0.01);
|
||||||
Assert.False(File.Exists(path));
|
Assert.False(File.Exists(path));
|
||||||
Pump.Pause(TimeSpan.FromMilliseconds(10));
|
Pump.Pause(TimeSpan.FromMilliseconds(10));
|
||||||
}
|
}
|
||||||
@@ -188,7 +188,7 @@ public sealed class SettingsServiceTests
|
|||||||
service.TrackChanges();
|
service.TrackChanges();
|
||||||
|
|
||||||
AppSettings settings = service.Load();
|
AppSettings settings = service.Load();
|
||||||
settings.FontSize = 29;
|
settings.Current.FontSize = 29;
|
||||||
|
|
||||||
Pump.WaitFor(() => File.Exists(path), "the settings were written by the timer");
|
Pump.WaitFor(() => File.Exists(path), "the settings were written by the timer");
|
||||||
});
|
});
|
||||||
@@ -230,11 +230,11 @@ public sealed class SettingsServiceTests
|
|||||||
using SettingsService service = Create(folder);
|
using SettingsService service = Create(folder);
|
||||||
AppSettings settings = service.Load();
|
AppSettings settings = service.Load();
|
||||||
|
|
||||||
File.WriteAllText(path, """{"FontSize": 31, "BackgroundColor": "#FF102030"}""");
|
File.WriteAllText(path, """{"AtCursor": {"FontSize": 31, "BackgroundColor": "#FF102030"}}""");
|
||||||
service.Reload();
|
service.Reload();
|
||||||
|
|
||||||
Assert.Equal(31, settings.FontSize);
|
Assert.Equal(31, settings.Current.FontSize);
|
||||||
Assert.Equal(Color.FromArgb(0x10, 0x20, 0x30), settings.BackgroundColor);
|
Assert.Equal(Color.FromArgb(0x10, 0x20, 0x30), settings.Current.BackgroundColor);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,12 +250,12 @@ public sealed class SettingsServiceTests
|
|||||||
{
|
{
|
||||||
using SettingsService service = Create(folder);
|
using SettingsService service = Create(folder);
|
||||||
AppSettings settings = service.Load();
|
AppSettings settings = service.Load();
|
||||||
settings.FontSize = 44;
|
settings.Current.FontSize = 44;
|
||||||
|
|
||||||
File.WriteAllText(path, "not json at all");
|
File.WriteAllText(path, "not json at all");
|
||||||
service.Reload();
|
service.Reload();
|
||||||
|
|
||||||
Assert.Equal(44, settings.FontSize);
|
Assert.Equal(44, settings.Current.FontSize);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,7 +269,7 @@ public sealed class SettingsServiceTests
|
|||||||
SettingsService service = Create(folder);
|
SettingsService service = Create(folder);
|
||||||
AppSettings settings = service.Load();
|
AppSettings settings = service.Load();
|
||||||
service.TrackChanges();
|
service.TrackChanges();
|
||||||
settings.FontSize = 27;
|
settings.Current.FontSize = 27;
|
||||||
|
|
||||||
service.Dispose();
|
service.Dispose();
|
||||||
});
|
});
|
||||||
@@ -295,7 +295,7 @@ public sealed class SettingsServiceTests
|
|||||||
|
|
||||||
string afterDispose = File.ReadAllText(path);
|
string afterDispose = File.ReadAllText(path);
|
||||||
|
|
||||||
settings.FontSize = 99;
|
settings.Current.FontSize = 99;
|
||||||
Pump.Pause(TimeSpan.FromMilliseconds(60));
|
Pump.Pause(TimeSpan.FromMilliseconds(60));
|
||||||
|
|
||||||
Assert.Equal(afterDispose, File.ReadAllText(path));
|
Assert.Equal(afterDispose, File.ReadAllText(path));
|
||||||
@@ -309,7 +309,7 @@ public sealed class SettingsServiceTests
|
|||||||
string inherited = folder.File("inherited.json");
|
string inherited = folder.File("inherited.json");
|
||||||
string own = folder.File("settings.json");
|
string own = folder.File("settings.json");
|
||||||
|
|
||||||
File.WriteAllText(inherited, """{"FontSize": 31, "Language": "ru"}""");
|
File.WriteAllText(inherited, """{"AtCursor": {"FontSize": 31}, "Language": "ru"}""");
|
||||||
|
|
||||||
AppSettings settings = Pump.Run(() =>
|
AppSettings settings = Pump.Run(() =>
|
||||||
{
|
{
|
||||||
@@ -317,7 +317,7 @@ public sealed class SettingsServiceTests
|
|||||||
return service.Load();
|
return service.Load();
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.Equal(31, settings.FontSize);
|
Assert.Equal(31, settings.Current.FontSize);
|
||||||
Assert.Equal("ru", settings.Language);
|
Assert.Equal("ru", settings.Language);
|
||||||
|
|
||||||
// What was taken over is pinned to its new place at once rather than on the first edit
|
// What was taken over is pinned to its new place at once rather than on the first edit
|
||||||
@@ -331,7 +331,7 @@ public sealed class SettingsServiceTests
|
|||||||
{
|
{
|
||||||
using var folder = new TempFolder();
|
using var folder = new TempFolder();
|
||||||
string inherited = folder.File("inherited.json");
|
string inherited = folder.File("inherited.json");
|
||||||
string original = """{"FontSize": 31}""";
|
string original = """{"AtCursor": {"FontSize": 31}}""";
|
||||||
|
|
||||||
File.WriteAllText(inherited, original);
|
File.WriteAllText(inherited, original);
|
||||||
|
|
||||||
@@ -351,8 +351,8 @@ public sealed class SettingsServiceTests
|
|||||||
string own = folder.File("settings.json");
|
string own = folder.File("settings.json");
|
||||||
string inherited = folder.File("inherited.json");
|
string inherited = folder.File("inherited.json");
|
||||||
|
|
||||||
File.WriteAllText(own, """{"FontSize": 12}""");
|
File.WriteAllText(own, """{"AtCursor": {"FontSize": 12}}""");
|
||||||
File.WriteAllText(inherited, """{"FontSize": 31}""");
|
File.WriteAllText(inherited, """{"AtCursor": {"FontSize": 31}}""");
|
||||||
|
|
||||||
AppSettings settings = Pump.Run(() =>
|
AppSettings settings = Pump.Run(() =>
|
||||||
{
|
{
|
||||||
@@ -360,7 +360,7 @@ public sealed class SettingsServiceTests
|
|||||||
return service.Load();
|
return service.Load();
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.Equal(12, settings.FontSize);
|
Assert.Equal(12, settings.Current.FontSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Outside a package both paths are the same, so there is nothing to take over
|
// Outside a package both paths are the same, so there is nothing to take over
|
||||||
@@ -392,14 +392,14 @@ public sealed class SettingsServiceTests
|
|||||||
return service.Load();
|
return service.Load();
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.Equal(20, settings.FontSize);
|
Assert.Equal(20, settings.Current.FontSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Settings_with_unknown_fields_are_still_read()
|
public void Settings_with_unknown_fields_are_still_read()
|
||||||
{
|
{
|
||||||
using var folder = new TempFolder();
|
using var folder = new TempFolder();
|
||||||
File.WriteAllText(folder.File("settings.json"), """{"FontSize": 15, "SomethingNew": true}""");
|
File.WriteAllText(folder.File("settings.json"), """{"AtCursor": {"FontSize": 15}, "SomethingNew": true}""");
|
||||||
|
|
||||||
AppSettings settings = Pump.Run(() =>
|
AppSettings settings = Pump.Run(() =>
|
||||||
{
|
{
|
||||||
@@ -407,7 +407,7 @@ public sealed class SettingsServiceTests
|
|||||||
return service.Load();
|
return service.Load();
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.Equal(15, settings.FontSize);
|
Assert.Equal(15, settings.Current.FontSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@@ -417,7 +417,7 @@ public sealed class SettingsServiceTests
|
|||||||
public void A_colour_is_read_from_its_usual_notation(string stored, byte r, byte g, byte b)
|
public void A_colour_is_read_from_its_usual_notation(string stored, byte r, byte g, byte b)
|
||||||
{
|
{
|
||||||
using var folder = new TempFolder();
|
using var folder = new TempFolder();
|
||||||
File.WriteAllText(folder.File("settings.json"), $$"""{"BackgroundColor": {{stored}}}""");
|
File.WriteAllText(folder.File("settings.json"), $$"""{"AtCursor": {"BackgroundColor": {{stored}} } }""");
|
||||||
|
|
||||||
AppSettings settings = Pump.Run(() =>
|
AppSettings settings = Pump.Run(() =>
|
||||||
{
|
{
|
||||||
@@ -425,7 +425,7 @@ public sealed class SettingsServiceTests
|
|||||||
return service.Load();
|
return service.Load();
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.Equal(Color.FromArgb(r, g, b), settings.BackgroundColor);
|
Assert.Equal(Color.FromArgb(r, g, b), settings.Current.BackgroundColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@@ -435,7 +435,7 @@ public sealed class SettingsServiceTests
|
|||||||
public void An_unintelligible_colour_becomes_black(string stored)
|
public void An_unintelligible_colour_becomes_black(string stored)
|
||||||
{
|
{
|
||||||
using var folder = new TempFolder();
|
using var folder = new TempFolder();
|
||||||
File.WriteAllText(folder.File("settings.json"), $$"""{"BackgroundColor": {{stored}}}""");
|
File.WriteAllText(folder.File("settings.json"), $$"""{"AtCursor": {"BackgroundColor": {{stored}} } }""");
|
||||||
|
|
||||||
AppSettings settings = Pump.Run(() =>
|
AppSettings settings = Pump.Run(() =>
|
||||||
{
|
{
|
||||||
@@ -443,7 +443,7 @@ public sealed class SettingsServiceTests
|
|||||||
return service.Load();
|
return service.Load();
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.Equal(Color.Black, settings.BackgroundColor);
|
Assert.Equal(Color.Black, settings.Current.BackgroundColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The service creates the settings folder itself
|
// The service creates the settings folder itself
|
||||||
@@ -478,7 +478,7 @@ public sealed class SettingsServiceTests
|
|||||||
using SettingsService service = new(path, folder.File("inherited.json"), SaveDelay);
|
using SettingsService service = new(path, folder.File("inherited.json"), SaveDelay);
|
||||||
AppSettings settings = service.Load();
|
AppSettings settings = service.Load();
|
||||||
|
|
||||||
settings.FontSize = 18;
|
settings.Current.FontSize = 18;
|
||||||
service.Save();
|
service.Save();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -534,6 +534,107 @@ public sealed class SettingsServiceTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The modes are sections of their own, and every setting of a mode has to reach the
|
||||||
|
// file inside its own section
|
||||||
|
[Theory]
|
||||||
|
[InlineData(nameof(AppSettings.AtCursor), typeof(CursorModeSettings))]
|
||||||
|
[InlineData(nameof(AppSettings.AtCaret), typeof(CaretModeSettings))]
|
||||||
|
[InlineData(nameof(AppSettings.FixedPoint), typeof(FixedPointModeSettings))]
|
||||||
|
public void Every_setting_of_a_mode_reaches_its_section_of_the_file(string section, Type mode)
|
||||||
|
{
|
||||||
|
using var folder = new TempFolder();
|
||||||
|
|
||||||
|
Pump.Run(() =>
|
||||||
|
{
|
||||||
|
using SettingsService service = Create(folder);
|
||||||
|
_ = service.Load();
|
||||||
|
service.Save();
|
||||||
|
});
|
||||||
|
|
||||||
|
using JsonDocument document = JsonDocument.Parse(File.ReadAllText(folder.File("settings.json")));
|
||||||
|
|
||||||
|
Assert.True(document.RootElement.TryGetProperty(section, out JsonElement stored));
|
||||||
|
|
||||||
|
foreach (string name in PopupModeSettingsTests.WritablePropertyNames(mode))
|
||||||
|
{
|
||||||
|
Assert.True(stored.TryGetProperty(name, out _), $"{section}.{name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The modes are stored apart: what is set up in one is still there after a trip
|
||||||
|
/// through the file and the other two.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void Each_mode_keeps_its_own_settings()
|
||||||
|
{
|
||||||
|
using var folder = new TempFolder();
|
||||||
|
|
||||||
|
Pump.Run(() =>
|
||||||
|
{
|
||||||
|
using SettingsService service = Create(folder);
|
||||||
|
AppSettings settings = service.Load();
|
||||||
|
|
||||||
|
settings.AtCursor.Side = AnchorSide.TopLeft;
|
||||||
|
settings.AtCursor.FontSize = 14;
|
||||||
|
settings.AtCaret.Side = CaretSide.Left;
|
||||||
|
settings.AtCaret.FontSize = 28;
|
||||||
|
settings.AtCaret.ForegroundColor = Color.FromArgb(0x0A, 0x0B, 0x0C);
|
||||||
|
settings.FixedPoint.Position = ScreenPosition.Top;
|
||||||
|
settings.FixedPoint.Offset = 96;
|
||||||
|
settings.FixedPoint.Opacity = 0.4;
|
||||||
|
|
||||||
|
service.Save();
|
||||||
|
});
|
||||||
|
|
||||||
|
AppSettings restored = Pump.Run(() =>
|
||||||
|
{
|
||||||
|
using SettingsService service = Create(folder);
|
||||||
|
return service.Load();
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal(AnchorSide.TopLeft, restored.AtCursor.Side);
|
||||||
|
Assert.Equal(14, restored.AtCursor.FontSize);
|
||||||
|
Assert.Equal(CaretSide.Left, restored.AtCaret.Side);
|
||||||
|
Assert.Equal(28, restored.AtCaret.FontSize);
|
||||||
|
Assert.Equal(Color.FromArgb(0x0A, 0x0B, 0x0C), restored.AtCaret.ForegroundColor);
|
||||||
|
Assert.Equal(ScreenPosition.Top, restored.FixedPoint.Position);
|
||||||
|
Assert.Equal(96, restored.FixedPoint.Offset);
|
||||||
|
Assert.Equal(0.4, restored.FixedPoint.Opacity);
|
||||||
|
|
||||||
|
// What was left alone stays at its default rather than following a neighbour
|
||||||
|
Assert.Equal(0.9, restored.AtCursor.Opacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A change inside a mode is a change of the settings: the file has to follow the
|
||||||
|
// sliders of the look as well as the ones above them
|
||||||
|
[Fact]
|
||||||
|
public void A_change_inside_a_mode_saves_itself()
|
||||||
|
{
|
||||||
|
using var folder = new TempFolder();
|
||||||
|
string path = folder.File("settings.json");
|
||||||
|
|
||||||
|
Pump.Run(() =>
|
||||||
|
{
|
||||||
|
using SettingsService service = Create(folder);
|
||||||
|
AppSettings settings = service.Load();
|
||||||
|
service.TrackChanges();
|
||||||
|
|
||||||
|
settings.AtCaret.FontSize = 37;
|
||||||
|
|
||||||
|
Pump.WaitFor(() => File.Exists(path), "the settings were written by the timer");
|
||||||
|
});
|
||||||
|
|
||||||
|
using JsonDocument document = JsonDocument.Parse(File.ReadAllText(path));
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
37,
|
||||||
|
document.RootElement
|
||||||
|
.GetProperty(nameof(AppSettings.AtCaret))
|
||||||
|
.GetProperty(nameof(PopupModeSettings.FontSize))
|
||||||
|
.GetDouble());
|
||||||
|
}
|
||||||
|
|
||||||
// An ordinary run picks the storage place itself: a package keeps settings
|
// An ordinary run picks the storage place itself: a package keeps settings
|
||||||
// of its own, a separate install keeps them in the user profile
|
// of its own, a separate install keeps them in the user profile
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Drawing;
|
using System.ComponentModel;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
@@ -8,11 +8,10 @@ namespace CursorLang.Core.Models;
|
|||||||
/// The application settings, as the settings window writes them to settings.json.
|
/// The application settings, as the settings window writes them to settings.json.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// The colours are <see cref="System.Drawing.Color"/> rather than
|
/// Everything about the popup — the side, the offset, the font size, the opacity and
|
||||||
/// <c>System.Windows.Media.Color</c>. The former lives in System.Drawing.Primitives,
|
/// both colours — belongs to a placement mode rather than to the application, and is
|
||||||
/// which is part of the base runtime and brings neither WPF nor GDI+ along; the latter
|
/// kept in a <see cref="PopupModeSettings"/> of its own for each of them. So the
|
||||||
/// is WindowsBase, and this type is read by the agent, which must stay clear of it.
|
/// settings of a mode are still there after a trip through the other two.
|
||||||
/// The settings window turns them into brushes in its converters.
|
|
||||||
///
|
///
|
||||||
/// Both processes hold an instance of this, but only the settings window writes: the
|
/// Both processes hold an instance of this, but only the settings window writes: the
|
||||||
/// agent re-reads the file and pours the fresh values into the instance it already has,
|
/// agent re-reads the file and pours the fresh values into the instance it already has,
|
||||||
@@ -32,43 +31,6 @@ public sealed partial class AppSettings : ObservableObject
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private PopupPlacementMode _placementMode = PopupPlacementMode.AtCursor;
|
private PopupPlacementMode _placementMode = PopupPlacementMode.AtCursor;
|
||||||
|
|
||||||
// The side and the offset are stored per mode: the cursor and the caret call for
|
|
||||||
// different settings, and switching the mode does not reset them
|
|
||||||
|
|
||||||
/// <summary>The side of the cursor the popup is put on.</summary>
|
|
||||||
[ObservableProperty]
|
|
||||||
private AnchorSide _cursorSide = AnchorSide.BottomRight;
|
|
||||||
|
|
||||||
/// <summary>The offset from the cursor in WPF units.</summary>
|
|
||||||
[ObservableProperty]
|
|
||||||
private double _cursorOffset = 16;
|
|
||||||
|
|
||||||
/// <summary>The side of the caret the popup is put on.</summary>
|
|
||||||
[ObservableProperty]
|
|
||||||
private AnchorSide _caretSide = AnchorSide.BottomRight;
|
|
||||||
|
|
||||||
/// <summary>The offset from the caret in WPF units.</summary>
|
|
||||||
[ObservableProperty]
|
|
||||||
private double _caretOffset = 16;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The place on the monitor for the <see cref="PopupPlacementMode.FixedPoint"/> mode.
|
|
||||||
/// </summary>
|
|
||||||
[ObservableProperty]
|
|
||||||
private ScreenPosition _screenPosition = ScreenPosition.BottomRight;
|
|
||||||
|
|
||||||
/// <summary>The offset from the monitor edge in WPF units.</summary>
|
|
||||||
[ObservableProperty]
|
|
||||||
private double _screenMargin = 24;
|
|
||||||
|
|
||||||
/// <summary>The size of the layout name in the popup, in WPF units.</summary>
|
|
||||||
[ObservableProperty]
|
|
||||||
private double _fontSize = 20;
|
|
||||||
|
|
||||||
/// <summary>The popup opacity: 1.0 is fully opaque.</summary>
|
|
||||||
[ObservableProperty]
|
|
||||||
private double _opacity = 0.9;
|
|
||||||
|
|
||||||
/// <summary>How long the popup stays on screen, in milliseconds.</summary>
|
/// <summary>How long the popup stays on screen, in milliseconds.</summary>
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private double _durationMilliseconds = 500;
|
private double _durationMilliseconds = 500;
|
||||||
@@ -87,13 +49,44 @@ public sealed partial class AppSettings : ObservableObject
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private double _capsLockHoldMilliseconds = 300;
|
private double _capsLockHoldMilliseconds = 300;
|
||||||
|
|
||||||
/// <summary>The fill of the popup. The opacity is set by <see cref="Opacity"/>.</summary>
|
public AppSettings()
|
||||||
[ObservableProperty]
|
{
|
||||||
private Color _backgroundColor = Color.FromArgb(0xFF, 0x20, 0x20, 0x20);
|
AtCursor.PropertyChanged += OnModeChanged;
|
||||||
|
AtCaret.PropertyChanged += OnModeChanged;
|
||||||
|
FixedPoint.PropertyChanged += OnModeChanged;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>The colour of the layout name in the popup.</summary>
|
/// <summary>The <see cref="PopupPlacementMode.AtCursor"/> mode.</summary>
|
||||||
[ObservableProperty]
|
/// <remarks>
|
||||||
private Color _foregroundColor = Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF);
|
/// The modes are handed out rather than replaced — the instance is the same for the
|
||||||
|
/// life of the settings, so a binding and a subscription to it hold. That is what
|
||||||
|
/// the creation handling is for: without it the deserializer would want a setter,
|
||||||
|
/// and reading the file would swap the object everything is bound to.
|
||||||
|
/// </remarks>
|
||||||
|
[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
|
||||||
|
public CursorModeSettings AtCursor { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>The <see cref="PopupPlacementMode.AtCaret"/> mode.</summary>
|
||||||
|
[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
|
||||||
|
public CaretModeSettings AtCaret { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>The <see cref="PopupPlacementMode.FixedPoint"/> mode.</summary>
|
||||||
|
[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
|
||||||
|
public FixedPointModeSettings FixedPoint { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>The settings of the mode currently chosen.</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The settings window binds the look through here, and the popup asks for it here
|
||||||
|
/// too: what is on screen is always the settings of the mode in force. A change of
|
||||||
|
/// <see cref="PlacementMode"/> announces this as changed, so the bindings follow.
|
||||||
|
/// </remarks>
|
||||||
|
[JsonIgnore]
|
||||||
|
public PopupModeSettings Current => PlacementMode switch
|
||||||
|
{
|
||||||
|
PopupPlacementMode.AtCaret => AtCaret,
|
||||||
|
PopupPlacementMode.FixedPoint => FixedPoint,
|
||||||
|
_ => AtCursor,
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary><see cref="DurationMilliseconds"/> as a <see cref="TimeSpan"/>.</summary>
|
/// <summary><see cref="DurationMilliseconds"/> as a <see cref="TimeSpan"/>.</summary>
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
@@ -110,25 +103,43 @@ public sealed partial class AppSettings : ObservableObject
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// This is how the agent learns about an edit: the settings window is a separate
|
/// This is how the agent learns about an edit: the settings window is a separate
|
||||||
/// process, so the fresh values arrive as a freshly parsed instance and are poured
|
/// process, so the fresh values arrive as a freshly parsed instance and are poured
|
||||||
/// into the one everything is already bound to, rather than replacing it.
|
/// into the one everything is already bound to, rather than replacing it. The modes
|
||||||
|
/// are filled the same way, and for the same reason.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public void CopyFrom(AppSettings other)
|
public void CopyFrom(AppSettings other)
|
||||||
{
|
{
|
||||||
Language = other.Language;
|
Language = other.Language;
|
||||||
Theme = other.Theme;
|
Theme = other.Theme;
|
||||||
PlacementMode = other.PlacementMode;
|
PlacementMode = other.PlacementMode;
|
||||||
CursorSide = other.CursorSide;
|
|
||||||
CursorOffset = other.CursorOffset;
|
|
||||||
CaretSide = other.CaretSide;
|
|
||||||
CaretOffset = other.CaretOffset;
|
|
||||||
ScreenPosition = other.ScreenPosition;
|
|
||||||
ScreenMargin = other.ScreenMargin;
|
|
||||||
FontSize = other.FontSize;
|
|
||||||
Opacity = other.Opacity;
|
|
||||||
DurationMilliseconds = other.DurationMilliseconds;
|
DurationMilliseconds = other.DurationMilliseconds;
|
||||||
UseCapsLockHotkey = other.UseCapsLockHotkey;
|
UseCapsLockHotkey = other.UseCapsLockHotkey;
|
||||||
CapsLockHoldMilliseconds = other.CapsLockHoldMilliseconds;
|
CapsLockHoldMilliseconds = other.CapsLockHoldMilliseconds;
|
||||||
BackgroundColor = other.BackgroundColor;
|
|
||||||
ForegroundColor = other.ForegroundColor;
|
AtCursor.CopyFrom(other.AtCursor);
|
||||||
|
AtCaret.CopyFrom(other.AtCaret);
|
||||||
|
FixedPoint.CopyFrom(other.FixedPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The mode in force decides what the look means, so a switch of the mode is a
|
||||||
|
// change of everything bound through Current
|
||||||
|
partial void OnPlacementModeChanged(PopupPlacementMode value) => OnPropertyChanged(nameof(Current));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Passes a change made inside a mode on as a change of the settings.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A mode is an object of its own, so an edit in it is not an edit of this one as
|
||||||
|
/// far as <see cref="INotifyPropertyChanged"/> goes. The settings window binds
|
||||||
|
/// through the path and hears the mode itself, but the service that writes the file
|
||||||
|
/// listens to the settings alone — and without this a font size dragged in the
|
||||||
|
/// window would never reach the disk. The name says where the change happened:
|
||||||
|
/// "AtCaret.FontSize".
|
||||||
|
/// </remarks>
|
||||||
|
private void OnModeChanged(object? sender, PropertyChangedEventArgs e) =>
|
||||||
|
OnPropertyChanged($"{NameOfMode(sender)}.{e.PropertyName}");
|
||||||
|
|
||||||
|
private string NameOfMode(object? mode) =>
|
||||||
|
ReferenceEquals(mode, AtCaret) ? nameof(AtCaret)
|
||||||
|
: ReferenceEquals(mode, FixedPoint) ? nameof(FixedPoint)
|
||||||
|
: nameof(AtCursor);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
using System.Drawing;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
namespace CursorLang.Core.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The settings of one placement mode: where the popup goes and how it looks there.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Every mode keeps a set of its own. The popup next to the caret sits inside a text
|
||||||
|
/// being read and is wanted small and quiet; the one in the corner of the monitor is
|
||||||
|
/// looked for on purpose and is wanted large. A look shared by the modes meant setting
|
||||||
|
/// it up again after every switch — and switching modes to see what they do undid what
|
||||||
|
/// had just been set up.
|
||||||
|
///
|
||||||
|
/// The colours are <see cref="System.Drawing.Color"/> for the same reason as in
|
||||||
|
/// <see cref="AppSettings"/>: the agent reads them and must stay clear of WPF.
|
||||||
|
/// </remarks>
|
||||||
|
public abstract partial class PopupModeSettings : ObservableObject
|
||||||
|
{
|
||||||
|
/// <summary>The distance from whatever the popup is placed by, in WPF units.</summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
private double _offset = 16;
|
||||||
|
|
||||||
|
/// <summary>The size of the layout name in the popup, in WPF units.</summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
private double _fontSize = 20;
|
||||||
|
|
||||||
|
/// <summary>The popup opacity: 1.0 is fully opaque.</summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
private double _opacity = 0.9;
|
||||||
|
|
||||||
|
/// <summary>The fill of the popup. The opacity is set by <see cref="Opacity"/>.</summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
private Color _backgroundColor = Color.FromArgb(0xFF, 0x20, 0x20, 0x20);
|
||||||
|
|
||||||
|
/// <summary>The colour of the layout name in the popup.</summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
private Color _foregroundColor = Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Takes the offset and the look of another mode over. What tells the modes apart —
|
||||||
|
/// the side, the place on the monitor — is copied by the mode itself.
|
||||||
|
/// </summary>
|
||||||
|
protected void CopyLookFrom(PopupModeSettings other)
|
||||||
|
{
|
||||||
|
Offset = other.Offset;
|
||||||
|
FontSize = other.FontSize;
|
||||||
|
Opacity = other.Opacity;
|
||||||
|
BackgroundColor = other.BackgroundColor;
|
||||||
|
ForegroundColor = other.ForegroundColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The mode that puts the popup next to the mouse cursor.</summary>
|
||||||
|
public sealed partial class CursorModeSettings : PopupModeSettings
|
||||||
|
{
|
||||||
|
/// <summary>The corner or the side of the cursor the popup is put on.</summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
private AnchorSide _side = AnchorSide.BottomRight;
|
||||||
|
|
||||||
|
/// <summary>Takes the values of another mode of the same kind over.</summary>
|
||||||
|
public void CopyFrom(CursorModeSettings other)
|
||||||
|
{
|
||||||
|
CopyLookFrom(other);
|
||||||
|
Side = other.Side;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The mode that puts the popup next to the caret of the active input field.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class CaretModeSettings : PopupModeSettings
|
||||||
|
{
|
||||||
|
/// <summary>The side of the caret the popup is put on: only left or right.</summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
private CaretSide _side = CaretSide.Right;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The side as the layout arithmetic wants it. Both of them line the popup up with
|
||||||
|
/// the caret rather than putting it above or below.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public AnchorSide Anchor => Side == CaretSide.Left ? AnchorSide.Left : AnchorSide.Right;
|
||||||
|
|
||||||
|
/// <summary>Takes the values of another mode of the same kind over.</summary>
|
||||||
|
public void CopyFrom(CaretModeSettings other)
|
||||||
|
{
|
||||||
|
CopyLookFrom(other);
|
||||||
|
Side = other.Side;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The side decides what the popup is lined up against, so it is a change of that too
|
||||||
|
partial void OnSideChanged(CaretSide value) => OnPropertyChanged(nameof(Anchor));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The mode that puts the popup at a fixed place of the monitor holding the active
|
||||||
|
/// window. Here <see cref="PopupModeSettings.Offset"/> is the distance from the edge
|
||||||
|
/// of the monitor.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// In the middle of the monitor there is no edge to keep a distance from, so the offset
|
||||||
|
/// is zero there and the settings window does not offer it. Choosing the middle puts it
|
||||||
|
/// back to zero rather than remembering it for later: a setting that is not shown must
|
||||||
|
/// not be one that still applies.
|
||||||
|
/// </remarks>
|
||||||
|
public sealed partial class FixedPointModeSettings : PopupModeSettings
|
||||||
|
{
|
||||||
|
/// <summary>The place on the monitor the popup is put at.</summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
private ScreenPosition _position = ScreenPosition.Center;
|
||||||
|
|
||||||
|
public FixedPointModeSettings()
|
||||||
|
{
|
||||||
|
// The middle is the default, and it has no edge to stand off from
|
||||||
|
Offset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Whether the popup is put at an edge of the monitor rather than in its middle.</summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public bool IsAtAnEdge => Position != ScreenPosition.Center;
|
||||||
|
|
||||||
|
/// <summary>Takes the values of another mode of the same kind over.</summary>
|
||||||
|
public void CopyFrom(FixedPointModeSettings other)
|
||||||
|
{
|
||||||
|
CopyLookFrom(other);
|
||||||
|
Position = other.Position;
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnPositionChanged(ScreenPosition value)
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(IsAtAnEdge));
|
||||||
|
|
||||||
|
if (value == ScreenPosition.Center)
|
||||||
|
{
|
||||||
|
Offset = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ public enum PopupPlacementMode
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Which side of the cursor or the caret to show the popup on.
|
/// Which side of the cursor to show the popup on.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum AnchorSide
|
public enum AnchorSide
|
||||||
{
|
{
|
||||||
@@ -31,6 +31,21 @@ public enum AnchorSide
|
|||||||
BottomRight,
|
BottomRight,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Which side of the caret to show the popup on.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Only the two sides, unlike <see cref="AnchorSide"/>. The caret stands in a line of
|
||||||
|
/// text being written, and above or below it is exactly where the next line is: the
|
||||||
|
/// popup would cover what is being read. To the side it covers nothing, and the line
|
||||||
|
/// it lines up with is the one the caret is in.
|
||||||
|
/// </remarks>
|
||||||
|
public enum CaretSide
|
||||||
|
{
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The place on the monitor for the <see cref="PopupPlacementMode.FixedPoint"/> mode.
|
/// The place on the monitor for the <see cref="PopupPlacementMode.FixedPoint"/> mode.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -121,6 +121,12 @@
|
|||||||
<data name="AnchorSide_Right" xml:space="preserve">
|
<data name="AnchorSide_Right" xml:space="preserve">
|
||||||
<value>Right</value>
|
<value>Right</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="CaretSide_Left" xml:space="preserve">
|
||||||
|
<value>Left</value>
|
||||||
|
</data>
|
||||||
|
<data name="CaretSide_Right" xml:space="preserve">
|
||||||
|
<value>Right</value>
|
||||||
|
</data>
|
||||||
<data name="CursorOffsetLabel" xml:space="preserve">
|
<data name="CursorOffsetLabel" xml:space="preserve">
|
||||||
<value>Offset</value>
|
<value>Offset</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -151,6 +157,9 @@
|
|||||||
<data name="ScreenMarginLabel" xml:space="preserve">
|
<data name="ScreenMarginLabel" xml:space="preserve">
|
||||||
<value>Margin from screen edge</value>
|
<value>Margin from screen edge</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="PopupLookPerModeHint" xml:space="preserve">
|
||||||
|
<value>The look below is remembered for the chosen placement mode alone.</value>
|
||||||
|
</data>
|
||||||
<data name="FontSizeLabel" xml:space="preserve">
|
<data name="FontSizeLabel" xml:space="preserve">
|
||||||
<value>Font size</value>
|
<value>Font size</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -121,6 +121,12 @@
|
|||||||
<data name="AnchorSide_Right" xml:space="preserve">
|
<data name="AnchorSide_Right" xml:space="preserve">
|
||||||
<value>Справа</value>
|
<value>Справа</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="CaretSide_Left" xml:space="preserve">
|
||||||
|
<value>Слева</value>
|
||||||
|
</data>
|
||||||
|
<data name="CaretSide_Right" xml:space="preserve">
|
||||||
|
<value>Справа</value>
|
||||||
|
</data>
|
||||||
<data name="CursorOffsetLabel" xml:space="preserve">
|
<data name="CursorOffsetLabel" xml:space="preserve">
|
||||||
<value>Отступ</value>
|
<value>Отступ</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -151,6 +157,9 @@
|
|||||||
<data name="ScreenMarginLabel" xml:space="preserve">
|
<data name="ScreenMarginLabel" xml:space="preserve">
|
||||||
<value>Отступ от края экрана</value>
|
<value>Отступ от края экрана</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="PopupLookPerModeHint" xml:space="preserve">
|
||||||
|
<value>Оформление ниже запоминается только для выбранного режима.</value>
|
||||||
|
</data>
|
||||||
<data name="FontSizeLabel" xml:space="preserve">
|
<data name="FontSizeLabel" xml:space="preserve">
|
||||||
<value>Размер шрифта</value>
|
<value>Размер шрифта</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ public sealed class ThemeServiceTests
|
|||||||
{
|
{
|
||||||
using var service = new ThemeService(settings, static () => AppTheme.Dark);
|
using var service = new ThemeService(settings, static () => AppTheme.Dark);
|
||||||
|
|
||||||
settings.FontSize = 40;
|
settings.Current.FontSize = 40;
|
||||||
settings.Language = "ru";
|
settings.Language = "ru";
|
||||||
|
|
||||||
Assert.Equal(AppTheme.Light, service.CurrentTheme);
|
Assert.Equal(AppTheme.Light, service.CurrentTheme);
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ public sealed class SettingsViewModelTests
|
|||||||
var localization = new FakeLocalizationService();
|
var localization = new FakeLocalizationService();
|
||||||
using var viewModel = Create(settings, localization);
|
using var viewModel = Create(settings, localization);
|
||||||
|
|
||||||
settings.FontSize = 30;
|
settings.Current.FontSize = 30;
|
||||||
|
|
||||||
Assert.Equal("en", localization.CurrentLanguage);
|
Assert.Equal("en", localization.CurrentLanguage);
|
||||||
}
|
}
|
||||||
@@ -59,6 +59,18 @@ public sealed class SettingsViewModelTests
|
|||||||
Assert.Equal(Enum.GetValues<ScreenPosition>(), viewModel.ScreenPositions.Select(option => option.Value));
|
Assert.Equal(Enum.GetValues<ScreenPosition>(), viewModel.ScreenPositions.Select(option => option.Value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Next to the caret the popup only goes beside it: above or below is where the next
|
||||||
|
// line of the text is
|
||||||
|
[Fact]
|
||||||
|
public void The_caret_is_offered_two_sides_and_no_more()
|
||||||
|
{
|
||||||
|
using SettingsViewModel viewModel = Create();
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
[CaretSide.Left, CaretSide.Right],
|
||||||
|
viewModel.CaretSides.Select(option => option.Value));
|
||||||
|
}
|
||||||
|
|
||||||
// A caption key is built from the type name and the value
|
// A caption key is built from the type name and the value
|
||||||
[Fact]
|
[Fact]
|
||||||
public void The_option_captions_come_from_the_resources()
|
public void The_option_captions_come_from_the_resources()
|
||||||
@@ -85,6 +97,7 @@ public sealed class SettingsViewModelTests
|
|||||||
Assert.Same(first, viewModel.Themes[0]);
|
Assert.Same(first, viewModel.Themes[0]);
|
||||||
Assert.Equal("ru:AppTheme_System", first.Display);
|
Assert.Equal("ru:AppTheme_System", first.Display);
|
||||||
Assert.Equal("ru:AnchorSide_TopLeft", viewModel.AnchorSides[0].Display);
|
Assert.Equal("ru:AnchorSide_TopLeft", viewModel.AnchorSides[0].Display);
|
||||||
|
Assert.Equal("ru:CaretSide_Left", viewModel.CaretSides[0].Display);
|
||||||
Assert.Equal("ru:ScreenPosition_TopLeft", viewModel.ScreenPositions[0].Display);
|
Assert.Equal("ru:ScreenPosition_TopLeft", viewModel.ScreenPositions[0].Display);
|
||||||
Assert.Equal("ru:PopupPlacementMode_AtCursor", viewModel.PlacementModes[0].Display);
|
Assert.Equal("ru:PopupPlacementMode_AtCursor", viewModel.PlacementModes[0].Display);
|
||||||
}
|
}
|
||||||
@@ -114,8 +127,8 @@ public sealed class SettingsViewModelTests
|
|||||||
var settings = new AppSettings();
|
var settings = new AppSettings();
|
||||||
using var viewModel = Create(settings);
|
using var viewModel = Create(settings);
|
||||||
|
|
||||||
Assert.Contains(settings.BackgroundColor, viewModel.BackgroundPalette);
|
Assert.Contains(settings.Current.BackgroundColor, viewModel.BackgroundPalette);
|
||||||
Assert.Contains(settings.ForegroundColor, viewModel.TextPalette);
|
Assert.Contains(settings.Current.ForegroundColor, viewModel.TextPalette);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -124,7 +124,8 @@ public sealed class MainWindowTests
|
|||||||
{
|
{
|
||||||
Sta.Run(() =>
|
Sta.Run(() =>
|
||||||
{
|
{
|
||||||
var settings = new AppSettings { FontSize = 33 };
|
var settings = new AppSettings();
|
||||||
|
settings.Current.FontSize = 33;
|
||||||
using SettingsViewModel viewModel = CreateViewModel(settings);
|
using SettingsViewModel viewModel = CreateViewModel(settings);
|
||||||
|
|
||||||
Open(viewModel, window =>
|
Open(viewModel, window =>
|
||||||
@@ -133,6 +134,178 @@ public sealed class MainWindowTests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The preview follows the mode: switching it shows the settings of the new one.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The look is bound through the current mode, so a switch has to reach the window.
|
||||||
|
/// Bound to a mode by name, the sliders would keep showing the cursor mode whatever
|
||||||
|
/// was chosen above them.
|
||||||
|
/// </remarks>
|
||||||
|
[Fact]
|
||||||
|
public void A_switch_of_the_mode_shows_the_settings_of_that_mode()
|
||||||
|
{
|
||||||
|
Sta.Run(() =>
|
||||||
|
{
|
||||||
|
var settings = new AppSettings();
|
||||||
|
settings.AtCursor.FontSize = 24;
|
||||||
|
settings.FixedPoint.FontSize = 56;
|
||||||
|
|
||||||
|
using SettingsViewModel viewModel = CreateViewModel(settings);
|
||||||
|
|
||||||
|
Open(viewModel, window =>
|
||||||
|
{
|
||||||
|
Assert.Contains(FindAll<TextBlock>(window), text => Math.Abs(text.FontSize - 24) < 0.001);
|
||||||
|
|
||||||
|
settings.PlacementMode = PopupPlacementMode.FixedPoint;
|
||||||
|
window.UpdateLayout();
|
||||||
|
|
||||||
|
Assert.Contains(FindAll<TextBlock>(window), text => Math.Abs(text.FontSize - 56) < 0.001);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// In the middle of the monitor the offset from the edge cannot be set, but it stays
|
||||||
|
/// in place.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// There is no edge to stand off from there, so the setting goes grey. Taking the row
|
||||||
|
/// out of the section instead would move everything below it on every switch of the
|
||||||
|
/// place — the section is not to jump about.
|
||||||
|
/// </remarks>
|
||||||
|
[Fact]
|
||||||
|
public void In_the_middle_of_the_monitor_the_offset_from_the_edge_goes_grey()
|
||||||
|
{
|
||||||
|
Sta.Run(() =>
|
||||||
|
{
|
||||||
|
var settings = new AppSettings { PlacementMode = PopupPlacementMode.FixedPoint };
|
||||||
|
using SettingsViewModel viewModel = CreateViewModel(settings);
|
||||||
|
|
||||||
|
Open(viewModel, window =>
|
||||||
|
{
|
||||||
|
Slider offset = Assert.Single(FindSlidersBoundTo(window, "Settings.FixedPoint.Offset"));
|
||||||
|
|
||||||
|
Assert.True(offset.IsVisible);
|
||||||
|
Assert.False(offset.IsEnabled);
|
||||||
|
|
||||||
|
// And it looks the part: the template draws from brushes that know
|
||||||
|
// nothing of the state, so the style has to fade it
|
||||||
|
Assert.True(offset.Opacity < 1);
|
||||||
|
|
||||||
|
settings.FixedPoint.Position = ScreenPosition.TopRight;
|
||||||
|
window.UpdateLayout();
|
||||||
|
|
||||||
|
Assert.True(offset.IsVisible);
|
||||||
|
Assert.True(offset.IsEnabled);
|
||||||
|
Assert.Equal(1, offset.Opacity);
|
||||||
|
|
||||||
|
settings.FixedPoint.Position = ScreenPosition.Center;
|
||||||
|
window.UpdateLayout();
|
||||||
|
|
||||||
|
Assert.True(offset.IsVisible);
|
||||||
|
Assert.False(offset.IsEnabled);
|
||||||
|
Assert.True(offset.Opacity < 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Another mode has no fixed point at all, and its offset has no place on screen
|
||||||
|
[Fact]
|
||||||
|
public void Away_from_the_fixed_point_its_offset_is_not_shown()
|
||||||
|
{
|
||||||
|
Sta.Run(() =>
|
||||||
|
{
|
||||||
|
var settings = new AppSettings { PlacementMode = PopupPlacementMode.AtCursor };
|
||||||
|
using SettingsViewModel viewModel = CreateViewModel(settings);
|
||||||
|
|
||||||
|
Open(viewModel, window =>
|
||||||
|
Assert.All(FindSlidersBoundTo(window, "Settings.FixedPoint.Offset"), slider =>
|
||||||
|
Assert.False(slider.IsVisible)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// What the look belongs to is explained in a tooltip next to the mode.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The sliders of the look show other numbers after a switch of the mode, and that
|
||||||
|
/// has to be explained. A line of text saying so would sit in the section forever,
|
||||||
|
/// while the question is asked once.
|
||||||
|
/// </remarks>
|
||||||
|
[Fact]
|
||||||
|
public void The_window_explains_that_the_look_belongs_to_the_mode()
|
||||||
|
{
|
||||||
|
Sta.Run(() =>
|
||||||
|
{
|
||||||
|
using SettingsViewModel viewModel = CreateViewModel();
|
||||||
|
|
||||||
|
Open(viewModel, window =>
|
||||||
|
{
|
||||||
|
// The text of a tooltip is bound but not computed until the tooltip is
|
||||||
|
// shown, so what it is bound to is what gets checked
|
||||||
|
List<TextBlock> hints =
|
||||||
|
[
|
||||||
|
.. FindAll<TextBlock>(window)
|
||||||
|
.Where(text => text.ToolTip is ToolTip tip &&
|
||||||
|
tip.Content is TextBlock content &&
|
||||||
|
PathOf(content, TextBlock.TextProperty) == "Localization[PopupLookPerModeHint]"),
|
||||||
|
];
|
||||||
|
|
||||||
|
TextBlock link = Assert.Single(hints);
|
||||||
|
|
||||||
|
Assert.Equal("en:MoreInfoLink", link.Text);
|
||||||
|
Assert.True(link.IsVisible);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Beside the caret and nowhere else: the list offers the two sides it has
|
||||||
|
[Fact]
|
||||||
|
public void The_caret_is_offered_two_sides_in_the_window()
|
||||||
|
{
|
||||||
|
Sta.Run(() =>
|
||||||
|
{
|
||||||
|
var settings = new AppSettings { PlacementMode = PopupPlacementMode.AtCaret };
|
||||||
|
using SettingsViewModel viewModel = CreateViewModel(settings);
|
||||||
|
|
||||||
|
Open(viewModel, window =>
|
||||||
|
{
|
||||||
|
ComboBox sides = Assert.Single(
|
||||||
|
FindAll<ComboBox>(window),
|
||||||
|
box => BindingOperations.GetBinding(box, Selector.SelectedValueProperty)?.Path.Path
|
||||||
|
== "Settings.AtCaret.Side");
|
||||||
|
|
||||||
|
Assert.Equal(2, sides.Items.Count);
|
||||||
|
|
||||||
|
sides.SelectedValue = CaretSide.Left;
|
||||||
|
|
||||||
|
Assert.Equal(CaretSide.Left, settings.AtCaret.Side);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// The sliders of the look write into the mode chosen above them
|
||||||
|
[Fact]
|
||||||
|
public void The_look_is_edited_in_the_mode_that_is_chosen()
|
||||||
|
{
|
||||||
|
Sta.Run(() =>
|
||||||
|
{
|
||||||
|
var settings = new AppSettings { PlacementMode = PopupPlacementMode.AtCaret };
|
||||||
|
using SettingsViewModel viewModel = CreateViewModel(settings);
|
||||||
|
|
||||||
|
Open(viewModel, window =>
|
||||||
|
{
|
||||||
|
Slider slider = Assert.Single(FindSlidersBoundTo(window, "Settings.Current.FontSize"));
|
||||||
|
|
||||||
|
slider.Value = 44;
|
||||||
|
|
||||||
|
Assert.Equal(44, settings.AtCaret.FontSize);
|
||||||
|
Assert.Equal(20, settings.AtCursor.FontSize);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void The_tooltip_colours_are_shown_as_swatches()
|
public void The_tooltip_colours_are_shown_as_swatches()
|
||||||
{
|
{
|
||||||
@@ -144,7 +317,7 @@ public sealed class MainWindowTests
|
|||||||
// The chosen colour is shown as a swatch with a caption — in the
|
// The chosen colour is shown as a swatch with a caption — in the
|
||||||
// same notation the settings file uses
|
// same notation the settings file uses
|
||||||
System.Drawing.Color chosen = viewModel.BackgroundPalette[2];
|
System.Drawing.Color chosen = viewModel.BackgroundPalette[2];
|
||||||
settings.BackgroundColor = chosen;
|
settings.Current.BackgroundColor = chosen;
|
||||||
|
|
||||||
string expected = $"#{chosen.R:X2}{chosen.G:X2}{chosen.B:X2}";
|
string expected = $"#{chosen.R:X2}{chosen.G:X2}{chosen.B:X2}";
|
||||||
|
|
||||||
@@ -359,6 +532,13 @@ public sealed class MainWindowTests
|
|||||||
FindAll<Button>(root).Where(button =>
|
FindAll<Button>(root).Where(button =>
|
||||||
BindingOperations.GetBinding(button, ButtonBase.CommandProperty)?.Path.Path == path);
|
BindingOperations.GetBinding(button, ButtonBase.CommandProperty)?.Path.Path == path);
|
||||||
|
|
||||||
|
private static string? PathOf(DependencyObject element, DependencyProperty property) =>
|
||||||
|
BindingOperations.GetBinding(element, property)?.Path.Path;
|
||||||
|
|
||||||
|
private static IEnumerable<Slider> FindSlidersBoundTo(DependencyObject root, string path) =>
|
||||||
|
FindAll<Slider>(root).Where(slider =>
|
||||||
|
BindingOperations.GetBinding(slider, RangeBase.ValueProperty)?.Path.Path == path);
|
||||||
|
|
||||||
private static IEnumerable<CheckBox> FindStartupCheckBoxes(DependencyObject root) =>
|
private static IEnumerable<CheckBox> FindStartupCheckBoxes(DependencyObject root) =>
|
||||||
FindCheckBoxesBoundTo(root, nameof(SettingsViewModel.RunAtStartup));
|
FindCheckBoxesBoundTo(root, nameof(SettingsViewModel.RunAtStartup));
|
||||||
|
|
||||||
|
|||||||
@@ -337,6 +337,11 @@
|
|||||||
</ControlTemplate>
|
</ControlTemplate>
|
||||||
</Setter.Value>
|
</Setter.Value>
|
||||||
</Setter>
|
</Setter>
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.5" />
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!-- ======================= ProgressBar ======================= -->
|
<!-- ======================= ProgressBar ======================= -->
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ public sealed partial class SettingsViewModel : ObservableObject, IDisposable
|
|||||||
Themes = CreateOptions<AppTheme>();
|
Themes = CreateOptions<AppTheme>();
|
||||||
PlacementModes = CreateOptions<PopupPlacementMode>();
|
PlacementModes = CreateOptions<PopupPlacementMode>();
|
||||||
AnchorSides = CreateOptions<AnchorSide>();
|
AnchorSides = CreateOptions<AnchorSide>();
|
||||||
|
CaretSides = CreateOptions<CaretSide>();
|
||||||
ScreenPositions = CreateOptions<ScreenPosition>();
|
ScreenPositions = CreateOptions<ScreenPosition>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +108,9 @@ public sealed partial class SettingsViewModel : ObservableObject, IDisposable
|
|||||||
|
|
||||||
public IReadOnlyList<EnumOption<AnchorSide>> AnchorSides { get; }
|
public IReadOnlyList<EnumOption<AnchorSide>> AnchorSides { get; }
|
||||||
|
|
||||||
|
/// <summary>The sides on offer next to the caret: there the popup only goes beside it.</summary>
|
||||||
|
public IReadOnlyList<EnumOption<CaretSide>> CaretSides { get; }
|
||||||
|
|
||||||
public IReadOnlyList<EnumOption<ScreenPosition>> ScreenPositions { get; }
|
public IReadOnlyList<EnumOption<ScreenPosition>> ScreenPositions { get; }
|
||||||
|
|
||||||
/// <summary>Whether to show the startup setting.</summary>
|
/// <summary>Whether to show the startup setting.</summary>
|
||||||
@@ -187,6 +191,7 @@ public sealed partial class SettingsViewModel : ObservableObject, IDisposable
|
|||||||
Translate(Themes);
|
Translate(Themes);
|
||||||
Translate(PlacementModes);
|
Translate(PlacementModes);
|
||||||
Translate(AnchorSides);
|
Translate(AnchorSides);
|
||||||
|
Translate(CaretSides);
|
||||||
Translate(ScreenPositions);
|
Translate(ScreenPositions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,11 @@
|
|||||||
<!-- The caption column is narrow, and a translation may not fit into it:
|
<!-- The caption column is narrow, and a translation may not fit into it:
|
||||||
such a caption is better wrapped onto a second line than cut off -->
|
such a caption is better wrapped onto a second line than cut off -->
|
||||||
<Setter Property="TextWrapping" Value="Wrap" />
|
<Setter Property="TextWrapping" Value="Wrap" />
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.5" />
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
</Style>
|
</Style>
|
||||||
<Style TargetType="TextBlock" x:Key="FieldValue">
|
<Style TargetType="TextBlock" x:Key="FieldValue">
|
||||||
<Setter Property="VerticalAlignment" Value="Center" />
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
@@ -46,6 +51,11 @@
|
|||||||
<Setter Property="MinWidth" Value="48" />
|
<Setter Property="MinWidth" Value="48" />
|
||||||
<Setter Property="TextAlignment" Value="Right" />
|
<Setter Property="TextAlignment" Value="Right" />
|
||||||
<Setter Property="Foreground" Value="{DynamicResource Theme.SecondaryForeground}" />
|
<Setter Property="Foreground" Value="{DynamicResource Theme.SecondaryForeground}" />
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.5" />
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
</Style>
|
</Style>
|
||||||
</Window.Resources>
|
</Window.Resources>
|
||||||
|
|
||||||
@@ -282,8 +292,24 @@
|
|||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<TextBlock Style="{StaticResource FieldLabel}"
|
<TextBlock Style="{StaticResource FieldLabel}">
|
||||||
Text="{Binding Localization[PlacementModeLabel]}" />
|
<Run Text="{Binding Localization[PlacementModeLabel], Mode=OneWay}" />
|
||||||
|
<InlineUIContainer BaselineAlignment="Baseline">
|
||||||
|
<TextBlock Text="{Binding Localization[MoreInfoLink]}"
|
||||||
|
Foreground="{DynamicResource Theme.Accent}"
|
||||||
|
TextDecorations="Underline"
|
||||||
|
Cursor="Help"
|
||||||
|
ToolTipService.InitialShowDelay="200"
|
||||||
|
ToolTipService.ShowDuration="60000">
|
||||||
|
<TextBlock.ToolTip>
|
||||||
|
<ToolTip>
|
||||||
|
<TextBlock TextWrapping="Wrap" MaxWidth="320"
|
||||||
|
Text="{Binding Localization[PopupLookPerModeHint]}" />
|
||||||
|
</ToolTip>
|
||||||
|
</TextBlock.ToolTip>
|
||||||
|
</TextBlock>
|
||||||
|
</InlineUIContainer>
|
||||||
|
</TextBlock>
|
||||||
<ComboBox Grid.Column="1" Grid.ColumnSpan="2"
|
<ComboBox Grid.Column="1" Grid.ColumnSpan="2"
|
||||||
ItemsSource="{Binding PlacementModes}"
|
ItemsSource="{Binding PlacementModes}"
|
||||||
ItemTemplate="{StaticResource EnumOptionTemplate}"
|
ItemTemplate="{StaticResource EnumOptionTemplate}"
|
||||||
@@ -301,7 +327,7 @@
|
|||||||
ItemsSource="{Binding AnchorSides}"
|
ItemsSource="{Binding AnchorSides}"
|
||||||
ItemTemplate="{StaticResource EnumOptionTemplate}"
|
ItemTemplate="{StaticResource EnumOptionTemplate}"
|
||||||
SelectedValuePath="Value"
|
SelectedValuePath="Value"
|
||||||
SelectedValue="{Binding Settings.CursorSide}"
|
SelectedValue="{Binding Settings.AtCursor.Side}"
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
Converter={StaticResource EnumToVisibility},
|
Converter={StaticResource EnumToVisibility},
|
||||||
ConverterParameter=AtCursor}" />
|
ConverterParameter=AtCursor}" />
|
||||||
@@ -314,13 +340,13 @@
|
|||||||
ConverterParameter=AtCursor}" />
|
ConverterParameter=AtCursor}" />
|
||||||
<Slider Grid.Row="2" Grid.Column="1" Margin="0,12,0,0"
|
<Slider Grid.Row="2" Grid.Column="1" Margin="0,12,0,0"
|
||||||
Minimum="0" Maximum="80" TickFrequency="1"
|
Minimum="0" Maximum="80" TickFrequency="1"
|
||||||
Value="{Binding Settings.CursorOffset}"
|
Value="{Binding Settings.AtCursor.Offset}"
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
Converter={StaticResource EnumToVisibility},
|
Converter={StaticResource EnumToVisibility},
|
||||||
ConverterParameter=AtCursor}" />
|
ConverterParameter=AtCursor}" />
|
||||||
<TextBlock Grid.Row="2" Grid.Column="2" Margin="12,12,0,0"
|
<TextBlock Grid.Row="2" Grid.Column="2" Margin="12,12,0,0"
|
||||||
Style="{StaticResource FieldValue}"
|
Style="{StaticResource FieldValue}"
|
||||||
Text="{Binding Settings.CursorOffset, StringFormat={}{0:F0}}"
|
Text="{Binding Settings.AtCursor.Offset, StringFormat={}{0:F0}}"
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
Converter={StaticResource EnumToVisibility},
|
Converter={StaticResource EnumToVisibility},
|
||||||
ConverterParameter=AtCursor}" />
|
ConverterParameter=AtCursor}" />
|
||||||
@@ -333,10 +359,10 @@
|
|||||||
Converter={StaticResource EnumToVisibility},
|
Converter={StaticResource EnumToVisibility},
|
||||||
ConverterParameter=AtCaret}" />
|
ConverterParameter=AtCaret}" />
|
||||||
<ComboBox Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
<ComboBox Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
||||||
ItemsSource="{Binding AnchorSides}"
|
ItemsSource="{Binding CaretSides}"
|
||||||
ItemTemplate="{StaticResource EnumOptionTemplate}"
|
ItemTemplate="{StaticResource EnumOptionTemplate}"
|
||||||
SelectedValuePath="Value"
|
SelectedValuePath="Value"
|
||||||
SelectedValue="{Binding Settings.CaretSide}"
|
SelectedValue="{Binding Settings.AtCaret.Side}"
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
Converter={StaticResource EnumToVisibility},
|
Converter={StaticResource EnumToVisibility},
|
||||||
ConverterParameter=AtCaret}" />
|
ConverterParameter=AtCaret}" />
|
||||||
@@ -349,13 +375,13 @@
|
|||||||
ConverterParameter=AtCaret}" />
|
ConverterParameter=AtCaret}" />
|
||||||
<Slider Grid.Row="4" Grid.Column="1" Margin="0,12,0,0"
|
<Slider Grid.Row="4" Grid.Column="1" Margin="0,12,0,0"
|
||||||
Minimum="0" Maximum="80" TickFrequency="1"
|
Minimum="0" Maximum="80" TickFrequency="1"
|
||||||
Value="{Binding Settings.CaretOffset}"
|
Value="{Binding Settings.AtCaret.Offset}"
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
Converter={StaticResource EnumToVisibility},
|
Converter={StaticResource EnumToVisibility},
|
||||||
ConverterParameter=AtCaret}" />
|
ConverterParameter=AtCaret}" />
|
||||||
<TextBlock Grid.Row="4" Grid.Column="2" Margin="12,12,0,0"
|
<TextBlock Grid.Row="4" Grid.Column="2" Margin="12,12,0,0"
|
||||||
Style="{StaticResource FieldValue}"
|
Style="{StaticResource FieldValue}"
|
||||||
Text="{Binding Settings.CaretOffset, StringFormat={}{0:F0}}"
|
Text="{Binding Settings.AtCaret.Offset, StringFormat={}{0:F0}}"
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
Converter={StaticResource EnumToVisibility},
|
Converter={StaticResource EnumToVisibility},
|
||||||
ConverterParameter=AtCaret}" />
|
ConverterParameter=AtCaret}" />
|
||||||
@@ -371,7 +397,7 @@
|
|||||||
ItemsSource="{Binding ScreenPositions}"
|
ItemsSource="{Binding ScreenPositions}"
|
||||||
ItemTemplate="{StaticResource EnumOptionTemplate}"
|
ItemTemplate="{StaticResource EnumOptionTemplate}"
|
||||||
SelectedValuePath="Value"
|
SelectedValuePath="Value"
|
||||||
SelectedValue="{Binding Settings.ScreenPosition}"
|
SelectedValue="{Binding Settings.FixedPoint.Position}"
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
Converter={StaticResource EnumToVisibility},
|
Converter={StaticResource EnumToVisibility},
|
||||||
ConverterParameter=FixedPoint}" />
|
ConverterParameter=FixedPoint}" />
|
||||||
@@ -379,18 +405,21 @@
|
|||||||
<TextBlock Grid.Row="6" Margin="0,12,12,0"
|
<TextBlock Grid.Row="6" Margin="0,12,12,0"
|
||||||
Style="{StaticResource FieldLabel}"
|
Style="{StaticResource FieldLabel}"
|
||||||
Text="{Binding Localization[ScreenMarginLabel]}"
|
Text="{Binding Localization[ScreenMarginLabel]}"
|
||||||
|
IsEnabled="{Binding Settings.FixedPoint.IsAtAnEdge}"
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
Converter={StaticResource EnumToVisibility},
|
Converter={StaticResource EnumToVisibility},
|
||||||
ConverterParameter=FixedPoint}" />
|
ConverterParameter=FixedPoint}" />
|
||||||
<Slider Grid.Row="6" Grid.Column="1" Margin="0,12,0,0"
|
<Slider Grid.Row="6" Grid.Column="1" Margin="0,12,0,0"
|
||||||
Minimum="0" Maximum="200" TickFrequency="1"
|
Minimum="0" Maximum="200" TickFrequency="1"
|
||||||
Value="{Binding Settings.ScreenMargin}"
|
Value="{Binding Settings.FixedPoint.Offset}"
|
||||||
|
IsEnabled="{Binding Settings.FixedPoint.IsAtAnEdge}"
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
Converter={StaticResource EnumToVisibility},
|
Converter={StaticResource EnumToVisibility},
|
||||||
ConverterParameter=FixedPoint}" />
|
ConverterParameter=FixedPoint}" />
|
||||||
<TextBlock Grid.Row="6" Grid.Column="2" Margin="12,12,0,0"
|
<TextBlock Grid.Row="6" Grid.Column="2" Margin="12,12,0,0"
|
||||||
Style="{StaticResource FieldValue}"
|
Style="{StaticResource FieldValue}"
|
||||||
Text="{Binding Settings.ScreenMargin, StringFormat={}{0:F0}}"
|
Text="{Binding Settings.FixedPoint.Offset, StringFormat={}{0:F0}}"
|
||||||
|
IsEnabled="{Binding Settings.FixedPoint.IsAtAnEdge}"
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
Converter={StaticResource EnumToVisibility},
|
Converter={StaticResource EnumToVisibility},
|
||||||
ConverterParameter=FixedPoint}" />
|
ConverterParameter=FixedPoint}" />
|
||||||
@@ -406,20 +435,20 @@
|
|||||||
Text="{Binding Localization[FontSizeLabel]}" />
|
Text="{Binding Localization[FontSizeLabel]}" />
|
||||||
<Slider Grid.Row="8" Grid.Column="1" Margin="0,12,0,0"
|
<Slider Grid.Row="8" Grid.Column="1" Margin="0,12,0,0"
|
||||||
Minimum="10" Maximum="72" TickFrequency="1"
|
Minimum="10" Maximum="72" TickFrequency="1"
|
||||||
Value="{Binding Settings.FontSize}" />
|
Value="{Binding Settings.Current.FontSize}" />
|
||||||
<TextBlock Grid.Row="8" Grid.Column="2" Margin="12,12,0,0"
|
<TextBlock Grid.Row="8" Grid.Column="2" Margin="12,12,0,0"
|
||||||
Style="{StaticResource FieldValue}"
|
Style="{StaticResource FieldValue}"
|
||||||
Text="{Binding Settings.FontSize, StringFormat={}{0:F0}}" />
|
Text="{Binding Settings.Current.FontSize, StringFormat={}{0:F0}}" />
|
||||||
|
|
||||||
<TextBlock Grid.Row="9" Margin="0,12,12,0"
|
<TextBlock Grid.Row="9" Margin="0,12,12,0"
|
||||||
Style="{StaticResource FieldLabel}"
|
Style="{StaticResource FieldLabel}"
|
||||||
Text="{Binding Localization[OpacityLabel]}" />
|
Text="{Binding Localization[OpacityLabel]}" />
|
||||||
<Slider Grid.Row="9" Grid.Column="1" Margin="0,12,0,0"
|
<Slider Grid.Row="9" Grid.Column="1" Margin="0,12,0,0"
|
||||||
Minimum="0.1" Maximum="1" TickFrequency="0.05"
|
Minimum="0.1" Maximum="1" TickFrequency="0.05"
|
||||||
Value="{Binding Settings.Opacity}" />
|
Value="{Binding Settings.Current.Opacity}" />
|
||||||
<TextBlock Grid.Row="9" Grid.Column="2" Margin="12,12,0,0"
|
<TextBlock Grid.Row="9" Grid.Column="2" Margin="12,12,0,0"
|
||||||
Style="{StaticResource FieldValue}"
|
Style="{StaticResource FieldValue}"
|
||||||
Text="{Binding Settings.Opacity, StringFormat={}{0:P0}}" />
|
Text="{Binding Settings.Current.Opacity, StringFormat={}{0:P0}}" />
|
||||||
|
|
||||||
<TextBlock Grid.Row="10" Margin="0,12,12,0"
|
<TextBlock Grid.Row="10" Margin="0,12,12,0"
|
||||||
Style="{StaticResource FieldLabel}"
|
Style="{StaticResource FieldLabel}"
|
||||||
@@ -427,7 +456,7 @@
|
|||||||
<ComboBox Grid.Row="10" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
<ComboBox Grid.Row="10" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
||||||
ItemsSource="{Binding BackgroundPalette}"
|
ItemsSource="{Binding BackgroundPalette}"
|
||||||
ItemTemplate="{StaticResource ColorSwatchTemplate}"
|
ItemTemplate="{StaticResource ColorSwatchTemplate}"
|
||||||
SelectedItem="{Binding Settings.BackgroundColor}" />
|
SelectedItem="{Binding Settings.Current.BackgroundColor}" />
|
||||||
|
|
||||||
<TextBlock Grid.Row="11" Margin="0,12,12,0"
|
<TextBlock Grid.Row="11" Margin="0,12,12,0"
|
||||||
Style="{StaticResource FieldLabel}"
|
Style="{StaticResource FieldLabel}"
|
||||||
@@ -435,14 +464,11 @@
|
|||||||
<ComboBox Grid.Row="11" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
<ComboBox Grid.Row="11" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
||||||
ItemsSource="{Binding TextPalette}"
|
ItemsSource="{Binding TextPalette}"
|
||||||
ItemTemplate="{StaticResource ColorSwatchTemplate}"
|
ItemTemplate="{StaticResource ColorSwatchTemplate}"
|
||||||
SelectedItem="{Binding Settings.ForegroundColor}" />
|
SelectedItem="{Binding Settings.Current.ForegroundColor}" />
|
||||||
|
|
||||||
<TextBlock Grid.Row="12" Margin="0,12,12,0"
|
<TextBlock Grid.Row="12" Margin="0,12,12,0"
|
||||||
Style="{StaticResource FieldLabel}"
|
Style="{StaticResource FieldLabel}"
|
||||||
Text="{Binding Localization[PreviewLabel]}" />
|
Text="{Binding Localization[PreviewLabel]}" />
|
||||||
<!-- The height of the area is sized for the largest font
|
|
||||||
available: the sample text is fully visible at any
|
|
||||||
position of the slider -->
|
|
||||||
<Border Grid.Row="12" Grid.Column="1" Grid.ColumnSpan="2"
|
<Border Grid.Row="12" Grid.Column="1" Grid.ColumnSpan="2"
|
||||||
Margin="0,12,0,0" Padding="16"
|
Margin="0,12,0,0" Padding="16"
|
||||||
Height="152" ClipToBounds="True"
|
Height="152" ClipToBounds="True"
|
||||||
@@ -450,12 +476,12 @@
|
|||||||
HorizontalAlignment="Stretch">
|
HorizontalAlignment="Stretch">
|
||||||
<Border CornerRadius="4" Padding="10,4"
|
<Border CornerRadius="4" Padding="10,4"
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
Opacity="{Binding Settings.Opacity}"
|
Opacity="{Binding Settings.Current.Opacity}"
|
||||||
Background="{Binding Settings.BackgroundColor,
|
Background="{Binding Settings.Current.BackgroundColor,
|
||||||
Converter={StaticResource ColorToBrush}}">
|
Converter={StaticResource ColorToBrush}}">
|
||||||
<TextBlock Text="RU" FontWeight="SemiBold"
|
<TextBlock Text="RU" FontWeight="SemiBold"
|
||||||
FontSize="{Binding Settings.FontSize}"
|
FontSize="{Binding Settings.Current.FontSize}"
|
||||||
Foreground="{Binding Settings.ForegroundColor,
|
Foreground="{Binding Settings.Current.ForegroundColor,
|
||||||
Converter={StaticResource ColorToBrush}}" />
|
Converter={StaticResource ColorToBrush}}" />
|
||||||
</Border>
|
</Border>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@@ -143,6 +143,28 @@ MSIX всегда выполняются в контексте вошедшег
|
|||||||
быть установлены рядом, и приложение не вправе удалять настройки, которые ему не
|
быть установлены рядом, и приложение не вправе удалять настройки, которые ему не
|
||||||
принадлежат.
|
принадлежат.
|
||||||
|
|
||||||
|
Всё, что относится к всплывающей подсказке, принадлежит режиму размещения, а не
|
||||||
|
приложению, и файл хранит по разделу на каждый режим — `AtCursor`, `AtCaret` и
|
||||||
|
`FixedPoint`. В каждом лежат сторона, отступ, размер шрифта, прозрачность и оба
|
||||||
|
цвета. Подсказка рядом с курсором ввода находится внутри читаемого текста, и её
|
||||||
|
хотят видеть небольшой и незаметной; ту, что в углу монитора, ищут намеренно, и её
|
||||||
|
хотят видеть крупной. Общее для всех режимов оформление приходилось настраивать
|
||||||
|
заново после каждого переключения, поэтому окно настроек показывает оформление
|
||||||
|
выбранного выше режима и записывает только в него.
|
||||||
|
|
||||||
|
Сторона означает в каждом режиме своё, и у каждого для неё собственный тип. Рядом с
|
||||||
|
курсором мыши это любой из шести углов и сторон. Рядом с курсором ввода — только
|
||||||
|
слева или справа: сверху и снизу от каретки находится следующая строка текста, и
|
||||||
|
подсказка закрывала бы читаемое. Для фиксированной точки это место на мониторе, а
|
||||||
|
отступ — расстояние от его края: в середине края нет, поэтому выбор середины
|
||||||
|
сбрасывает отступ в нуль, а окно настроек показывает его неактивным. Отступ остаётся
|
||||||
|
на месте, а не исчезает из раздела — появляющаяся и исчезающая строка сдвигала бы
|
||||||
|
всё, что ниже, при каждой смене места.
|
||||||
|
|
||||||
|
К какому режиму относится оформление, объясняет всплывающая подсказка рядом с самим
|
||||||
|
режимом, а не строка текста в разделе: ползунки ниже показывают другие числа после
|
||||||
|
переключения режима, и это вопрос, который задают один раз.
|
||||||
|
|
||||||
## Обновления
|
## Обновления
|
||||||
|
|
||||||
Проверка выполняется при открытии окна настроек, а не при включении машины:
|
Проверка выполняется при открытии окна настроек, а не при включении машины:
|
||||||
|
|||||||
@@ -138,6 +138,28 @@ install and copies them over. The original file stays where it is: both builds
|
|||||||
may be installed side by side, and the app has no business deleting settings it
|
may be installed side by side, and the app has no business deleting settings it
|
||||||
does not own.
|
does not own.
|
||||||
|
|
||||||
|
Everything about the popup belongs to a placement mode rather than to the app, and
|
||||||
|
the file keeps a section per mode — `AtCursor`, `AtCaret` and `FixedPoint`. Each of
|
||||||
|
them holds the side, the offset, the font size, the opacity and both colours. The
|
||||||
|
popup next to the caret sits inside a text being read and is wanted small and quiet;
|
||||||
|
the one in the corner of the monitor is looked for on purpose and is wanted large. A
|
||||||
|
look shared by the modes meant setting it up again after every switch, so the settings
|
||||||
|
window shows the look of the mode chosen above it and writes into that mode alone.
|
||||||
|
|
||||||
|
What the side means differs by mode, and each of them has a type saying so. Next to
|
||||||
|
the cursor it is any of the six corners and sides. Next to the caret it is left or
|
||||||
|
right and nothing else — above or below the caret is exactly where the next line of
|
||||||
|
the text is, so the popup would cover what is being read. For the fixed point it is a
|
||||||
|
place on the monitor, and the offset is the distance from its edge: in the middle
|
||||||
|
there is no edge to stand off from, so choosing the middle puts the offset back to
|
||||||
|
zero and the settings window shows it greyed out. It stays in place rather than
|
||||||
|
leaving the section — a row that comes and goes would move everything below it on
|
||||||
|
every switch of the place.
|
||||||
|
|
||||||
|
Which mode the look belongs to is explained in a tooltip next to the mode itself
|
||||||
|
rather than by a line of text in the section: the sliders below show other numbers
|
||||||
|
after a switch of the mode, and that is a question asked once.
|
||||||
|
|
||||||
## Updates
|
## Updates
|
||||||
|
|
||||||
The check runs when the settings window is opened, not when the machine is
|
The check runs when the settings window is opened, not when the machine is
|
||||||
|
|||||||
Reference in New Issue
Block a user