Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42e93346ab | ||
|
|
9ab26afc16 | ||
|
|
743974a0c1 | ||
|
|
9c6ec489e1 | ||
|
|
b2c7b31eb5 | ||
|
|
5670ad0e64 | ||
|
|
1540be3b79 | ||
|
|
88dd931cb4 | ||
|
|
70b83a5fec | ||
|
|
0682f2b875 | ||
|
|
5dea510bbc |
@@ -138,9 +138,22 @@ jobs:
|
|||||||
- name: Build the installer
|
- name: Build the installer
|
||||||
run: ./Packaging/build-installer.ps1 -Version ${{ steps.version.outputs.plain }}
|
run: ./Packaging/build-installer.ps1 -Version ${{ steps.version.outputs.plain }}
|
||||||
|
|
||||||
# The artifact is where the package waits to be uploaded to Partner Center
|
# The artifact is where the package waits to be uploaded to Partner Center.
|
||||||
|
#
|
||||||
|
# The action comes from this Gitea rather than from github.com, and it is
|
||||||
|
# a fork of actions/upload-artifact with one line changed. Upstream reads
|
||||||
|
# GITHUB_SERVER_URL, takes every host but github.com and its enterprise
|
||||||
|
# ones for a GitHub Enterprise Server — where the v4 artifact backend does
|
||||||
|
# not exist — and throws before it makes a request. Here that backend does
|
||||||
|
# exist and answers, but the check runs on the runner ahead of the network
|
||||||
|
# and never lets it prove itself; nor can the name be corrected from this
|
||||||
|
# file, as the runner sets the GITHUB_ ones over the env of a step. The
|
||||||
|
# fork carries the reasoning in full in its GITEA-PATCH.md.
|
||||||
|
#
|
||||||
|
# The tag is a fixed one. A floating tag is how this broke before: the
|
||||||
|
# action is fetched at run time, and what arrives can change on its own
|
||||||
- name: Keep the package
|
- name: Keep the package
|
||||||
uses: actions/upload-artifact@v4
|
uses: https://git.alrakis.kz/actions/upload-artifact@v4-gitea
|
||||||
with:
|
with:
|
||||||
name: msix-${{ steps.version.outputs.version }}
|
name: msix-${{ steps.version.outputs.version }}
|
||||||
path: artifacts/packages/
|
path: artifacts/packages/
|
||||||
@@ -149,7 +162,7 @@ jobs:
|
|||||||
# The .wixpdb next to each installer is left out on purpose: it is of use
|
# The .wixpdb next to each installer is left out on purpose: it is of use
|
||||||
# only when something has to be traced back to the WiX source
|
# only when something has to be traced back to the WiX source
|
||||||
- name: Keep the installer
|
- name: Keep the installer
|
||||||
uses: actions/upload-artifact@v4
|
uses: https://git.alrakis.kz/actions/upload-artifact@v4-gitea
|
||||||
with:
|
with:
|
||||||
name: installer-${{ steps.version.outputs.plain }}
|
name: installer-${{ steps.version.outputs.plain }}
|
||||||
path: artifacts/installers/*.msi
|
path: artifacts/installers/*.msi
|
||||||
@@ -167,10 +180,8 @@ jobs:
|
|||||||
|
|
||||||
# The GITHUB_ names are what Gitea itself hands to the workflow — its
|
# The GITHUB_ names are what Gitea itself hands to the workflow — its
|
||||||
# actions repeat those of GitHub, and the addresses in them point at
|
# actions repeat those of GitHub, and the addresses in them point at
|
||||||
# this Gitea instance. GITHUB_API_URL used not to reach the steps at
|
# this Gitea instance
|
||||||
# all, so the address is put together from the server one when empty
|
$api = "$env:GITHUB_API_URL/repos/$env:GITHUB_REPOSITORY/releases"
|
||||||
$root = if ($env:GITHUB_API_URL) { $env:GITHUB_API_URL } else { "$env:GITHUB_SERVER_URL/api/v1" }
|
|
||||||
$api = "$root/repos/$env:GITHUB_REPOSITORY/releases"
|
|
||||||
$headers = @{ Authorization = "token $env:GITEA_TOKEN" }
|
$headers = @{ Authorization = "token $env:GITEA_TOKEN" }
|
||||||
|
|
||||||
# Gitea makes a release of its own for a pushed tag, so the release is
|
# Gitea makes a release of its own for a pushed tag, so the release is
|
||||||
|
|||||||
@@ -133,6 +133,34 @@ public sealed partial class EndToEndTests
|
|||||||
Assert.False(launch.Process.HasExited);
|
Assert.False(launch.Process.HasExited);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Exit" in the tray menu ends the application whole: the settings window goes with
|
||||||
|
/// the agent instead of staying on the screen belonging to nothing.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The menu itself is out of reach of a test — it is a <c>TrackPopupMenuEx</c> menu
|
||||||
|
/// with a modal loop of its own — so what is checked is the request the menu makes.
|
||||||
|
/// The settings window here is the real one, started by the agent, and it must be
|
||||||
|
/// listening by the time it is on the screen.
|
||||||
|
/// </remarks>
|
||||||
|
[Fact]
|
||||||
|
public void The_agents_exit_closes_the_settings_window()
|
||||||
|
{
|
||||||
|
using Launch launch = Launch.Start();
|
||||||
|
launch.WaitForSettingsWindow();
|
||||||
|
|
||||||
|
Process settings = Launch.SettingsProcesses().Single();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Assert.True(SettingsCloseSignal.RequestClose(), "the settings window was not listening");
|
||||||
|
Assert.True(settings.WaitForExit(ExitTimeout), "the settings window outlived the agent's exit");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
settings.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>A started application that shuts down together with the check.</summary>
|
/// <summary>A started application that shuts down together with the check.</summary>
|
||||||
private sealed class Launch : IDisposable
|
private sealed class Launch : IDisposable
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -125,5 +125,12 @@ internal sealed class Agent : IDisposable
|
|||||||
|
|
||||||
private void OnOpenRequested(object? sender, EventArgs e) => SettingsLauncher.Open();
|
private void OnOpenRequested(object? sender, EventArgs e) => SettingsLauncher.Open();
|
||||||
|
|
||||||
private void OnExitRequested(object? sender, EventArgs e) => _window.Quit();
|
// "Exit" means the application, not just the background half of it. A settings
|
||||||
|
// window left open would outlive the tray icon it was opened from, so it is asked
|
||||||
|
// to close first — it may be the very window the user is looking at
|
||||||
|
private void OnExitRequested(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
SettingsCloseSignal.RequestClose();
|
||||||
|
_window.Quit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,8 @@
|
|||||||
<PublishTrimmed>false</PublishTrimmed>
|
<PublishTrimmed>false</PublishTrimmed>
|
||||||
<PublishReadyToRun Condition="'$(RuntimeIdentifier)' != ''">true</PublishReadyToRun>
|
<PublishReadyToRun Condition="'$(RuntimeIdentifier)' != ''">true</PublishReadyToRun>
|
||||||
<Version>1.0.0</Version>
|
<Version>1.0.0</Version>
|
||||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
<AssemblyVersion>$(Version)</AssemblyVersion>
|
||||||
<FileVersion>1.0.0.0</FileVersion>
|
<FileVersion>$(Version)</FileVersion>
|
||||||
<Product>CursorLang</Product>
|
<Product>CursorLang</Product>
|
||||||
<Company>Aleksandr Neichev</Company>
|
<Company>Aleksandr Neichev</Company>
|
||||||
<Description>Shows the keyboard layout at the cursor</Description>
|
<Description>Shows the keyboard layout at the cursor</Description>
|
||||||
|
|||||||
@@ -68,42 +68,42 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool atFixedPoint = _settings.PlacementMode == PopupPlacementMode.FixedPoint;
|
PopupWindowNative.Rect? anchor = TryGetAnchor();
|
||||||
|
PopupModeSettings mode = ModeFor(anchor);
|
||||||
|
|
||||||
PopupWindowNative.Rect work = default;
|
PopupWindowNative.Rect work = default;
|
||||||
PopupWindowNative.Rect anchor = default;
|
|
||||||
double scale;
|
double scale;
|
||||||
|
|
||||||
if (atFixedPoint)
|
if (anchor is { } at)
|
||||||
{
|
{
|
||||||
(work, scale) = PopupWindowNative.GetActiveMonitorWorkArea();
|
scale = PopupWindowNative.GetScaleAt(new PopupWindowNative.Point { X = at.Left, Y = at.Top });
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
anchor = GetAnchor();
|
(work, scale) = PopupWindowNative.GetActiveMonitorWorkArea();
|
||||||
scale = PopupWindowNative.GetScaleAt(new PopupWindowNative.Point { X = anchor.Left, Y = anchor.Top });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
EnsureFont(scale);
|
EnsureFont(mode, scale);
|
||||||
|
|
||||||
Size measured = MeasureText(text);
|
Size measured = MeasureText(text);
|
||||||
int width = measured.Width + (2 * PopupLayout.ToPixels(PaddingX, scale));
|
int width = measured.Width + (2 * PopupLayout.ToPixels(PaddingX, scale));
|
||||||
int height = measured.Height + (2 * PopupLayout.ToPixels(PaddingY, scale));
|
int height = measured.Height + (2 * PopupLayout.ToPixels(PaddingY, scale));
|
||||||
|
|
||||||
PopupWindowNative.Point position = atFixedPoint
|
PopupWindowNative.Point position = anchor is { } near
|
||||||
? PopupLayout.OnScreen(
|
? PopupLayout.NearAnchor(
|
||||||
work,
|
near,
|
||||||
_settings.FixedPoint.Position,
|
SideForMode(),
|
||||||
PopupLayout.ToPixels(_settings.FixedPoint.Offset, scale),
|
PopupLayout.ToPixels(mode.Offset, scale),
|
||||||
width,
|
width,
|
||||||
height)
|
height)
|
||||||
: PopupLayout.NearAnchor(
|
: PopupLayout.OnScreen(
|
||||||
anchor,
|
work,
|
||||||
SideForMode(),
|
_settings.FixedPoint.Position,
|
||||||
PopupLayout.ToPixels(_settings.Current.Offset, scale),
|
PopupLayout.ToPixels(mode.Offset, scale),
|
||||||
width,
|
width,
|
||||||
height);
|
height);
|
||||||
|
|
||||||
if (!Draw(text, position, width, height, PopupLayout.ToPixels(CornerRadius, scale)))
|
if (!Draw(mode, text, position, width, height, PopupLayout.ToPixels(CornerRadius, scale)))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -139,7 +139,8 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
|
|||||||
/// kilobytes for the length of one call, the popup is shown rarely, and a cached one
|
/// kilobytes for the length of one call, the popup is shown rarely, and a cached one
|
||||||
/// would have to be rebuilt on every change of size, colour or scale anyway.
|
/// would have to be rebuilt on every change of size, colour or scale anyway.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
private bool Draw(string text, PopupWindowNative.Point at, int width, int height, int radius)
|
private bool Draw(
|
||||||
|
PopupModeSettings mode, string text, PopupWindowNative.Point at, int width, int height, int radius)
|
||||||
{
|
{
|
||||||
IntPtr screen = GdiNative.GetDC(IntPtr.Zero);
|
IntPtr screen = GdiNative.GetDC(IntPtr.Zero);
|
||||||
if (screen == IntPtr.Zero)
|
if (screen == IntPtr.Zero)
|
||||||
@@ -166,8 +167,8 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
|
|||||||
|
|
||||||
GdiNative.SelectObject(memory, surface);
|
GdiNative.SelectObject(memory, surface);
|
||||||
|
|
||||||
Fill(bits, width, height);
|
Fill(mode, bits, width, height);
|
||||||
DrawText(memory, text, width, height);
|
DrawText(mode, memory, text, width, height);
|
||||||
|
|
||||||
// GDI writes nothing into the alpha channel, so the letters it just drew are
|
// GDI writes nothing into the alpha channel, so the letters it just drew are
|
||||||
// sitting at zero alpha and would come out invisible. The inside of the
|
// sitting at zero alpha and would come out invisible. The inside of the
|
||||||
@@ -178,7 +179,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.Current.Opacity * 255), 0, 255);
|
var alpha = (byte)Math.Clamp(Math.Round(mode.Opacity * 255), 0, 255);
|
||||||
|
|
||||||
return WindowNative.SetContent(Handle, at, size, memory, alpha);
|
return WindowNative.SetContent(Handle, at, size, memory, alpha);
|
||||||
}
|
}
|
||||||
@@ -200,9 +201,9 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Fill(IntPtr bits, int width, int height)
|
private static void Fill(PopupModeSettings mode, IntPtr bits, int width, int height)
|
||||||
{
|
{
|
||||||
Color background = _settings.Current.BackgroundColor;
|
Color background = mode.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
|
||||||
@@ -216,7 +217,8 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawText(IntPtr deviceContext, string text, int width, int height)
|
private void DrawText(
|
||||||
|
PopupModeSettings mode, IntPtr deviceContext, string text, int width, int height)
|
||||||
{
|
{
|
||||||
if (_font == IntPtr.Zero || text.Length == 0)
|
if (_font == IntPtr.Zero || text.Length == 0)
|
||||||
{
|
{
|
||||||
@@ -227,7 +229,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.Current.ForegroundColor));
|
GdiNative.SetTextColor(deviceContext, GdiNative.ToColorRef(mode.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 |
|
||||||
@@ -329,18 +331,40 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
|
|||||||
return (int)((alpha << 24) | (red << 16) | (green << 8) | blue);
|
return (int)((alpha << 24) | (red << 16) | (green << 8) | blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The anchor point: the caret in the input field or the mouse cursor. The cursor
|
/// <summary>
|
||||||
// is a rectangle of zero size, so the corner computation is shared by both
|
/// What the popup is placed next to, or <c>null</c> when there is nothing: the fixed
|
||||||
private PopupWindowNative.Rect GetAnchor()
|
/// point mode, and the caret mode where the application reports no caret.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The cursor is a rectangle of zero size, so the corner arithmetic is shared by it
|
||||||
|
/// and the caret.
|
||||||
|
/// </remarks>
|
||||||
|
private PopupWindowNative.Rect? TryGetAnchor() => _settings.PlacementMode switch
|
||||||
{
|
{
|
||||||
if (_settings.PlacementMode == PopupPlacementMode.AtCaret &&
|
PopupPlacementMode.FixedPoint => null,
|
||||||
CaretNative.TryGetCaretRect() is { } caret)
|
PopupPlacementMode.AtCaret => CaretNative.TryGetCaretRect(),
|
||||||
{
|
_ => PopupLayout.AsAnchor(PopupWindowNative.GetCursorPosition()),
|
||||||
return caret;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return PopupLayout.AsAnchor(PopupWindowNative.GetCursorPosition());
|
/// <summary>
|
||||||
}
|
/// The settings the popup is shown with: those of the mode chosen, or those of the
|
||||||
|
/// fixed point when there is no anchor to stand next to.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The caret mode falls back to the fixed point rather than to the mouse cursor:
|
||||||
|
/// the cursor is wherever it was last left — off to a side, on another monitor, or
|
||||||
|
/// over the very text being typed — and a popup that lands there while the eyes are
|
||||||
|
/// on the caret is one that is looked for and not found. The fixed point is always
|
||||||
|
/// in the same place, so it is known where to look.
|
||||||
|
///
|
||||||
|
/// The look comes from the fixed point mode too, not just the place. The two are set
|
||||||
|
/// up together for a reason: the popup by the caret is small and quiet because it
|
||||||
|
/// sits inside a text being read, while the one in the corner of the monitor is
|
||||||
|
/// looked for on purpose and is set larger. Keeping the caret look at the corner
|
||||||
|
/// would put a popup meant to go unnoticed where nothing else draws the eye.
|
||||||
|
/// </remarks>
|
||||||
|
private PopupModeSettings ModeFor(PopupWindowNative.Rect? anchor) =>
|
||||||
|
anchor is null ? _settings.FixedPoint : _settings.Current;
|
||||||
|
|
||||||
// The caret has two sides to choose from and the cursor has six, so each mode names
|
// The caret has two sides to choose from and the cursor has six, so each mode names
|
||||||
// its own side in its own terms
|
// its own side in its own terms
|
||||||
@@ -373,10 +397,10 @@ 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. A switch of the placement
|
// 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
|
// mode counts as a change of the size, since the size belongs to the mode
|
||||||
private void EnsureFont(double scale)
|
private void EnsureFont(PopupModeSettings mode, double scale)
|
||||||
{
|
{
|
||||||
if (_font != IntPtr.Zero &&
|
if (_font != IntPtr.Zero &&
|
||||||
Math.Abs(_fontSize - _settings.Current.FontSize) < 0.01 &&
|
Math.Abs(_fontSize - mode.FontSize) < 0.01 &&
|
||||||
Math.Abs(_fontScale - scale) < 0.01)
|
Math.Abs(_fontScale - scale) < 0.01)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -384,7 +408,7 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
|
|||||||
|
|
||||||
ReleaseFont();
|
ReleaseFont();
|
||||||
|
|
||||||
_fontSize = _settings.Current.FontSize;
|
_fontSize = mode.FontSize;
|
||||||
_fontScale = scale;
|
_fontScale = scale;
|
||||||
_font = GdiNative.CreateFont(_fontSize, scale);
|
_font = GdiNative.CreateFont(_fontSize, scale);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using CursorLang.Core.Services;
|
||||||
|
using CursorLang.Tests.Shared;
|
||||||
|
|
||||||
|
namespace CursorLang.Core.Tests.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The one thing the agent says to the settings window: quit with me.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The event names here are the tests' own. The application's name is fixed, and a test
|
||||||
|
/// listening on it would answer for a settings window someone is using — or, signalling,
|
||||||
|
/// close it.
|
||||||
|
/// </remarks>
|
||||||
|
public sealed class SettingsCloseSignalTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void The_request_reaches_the_settings_window()
|
||||||
|
{
|
||||||
|
string suffix = UniqueSuffix();
|
||||||
|
var signal = new SettingsCloseSignal(suffix);
|
||||||
|
|
||||||
|
ConcurrentQueue<EventArgs> requests = new();
|
||||||
|
signal.CloseRequested += (_, e) => requests.Enqueue(e);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
signal.Listen();
|
||||||
|
|
||||||
|
Assert.True(RequestApart(suffix), "the request found nobody listening");
|
||||||
|
Pump.WaitFor(() => !requests.IsEmpty, "the settings window got the request to close");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
signal.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The usual case: the user quits from the tray with no settings window on the screen
|
||||||
|
[Fact]
|
||||||
|
public void A_request_with_no_settings_window_open_passes_without_consequence()
|
||||||
|
{
|
||||||
|
Assert.False(RequestApart(UniqueSuffix()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The window has closed on its own, and the process is on its way out anyway
|
||||||
|
[Fact]
|
||||||
|
public void No_request_arrives_after_the_window_is_gone()
|
||||||
|
{
|
||||||
|
string suffix = UniqueSuffix();
|
||||||
|
var signal = new SettingsCloseSignal(suffix);
|
||||||
|
|
||||||
|
ConcurrentQueue<EventArgs> requests = new();
|
||||||
|
signal.CloseRequested += (_, e) => requests.Enqueue(e);
|
||||||
|
|
||||||
|
signal.Listen();
|
||||||
|
signal.Dispose();
|
||||||
|
|
||||||
|
RequestApart(suffix);
|
||||||
|
Pump.Pause(TimeSpan.FromMilliseconds(80));
|
||||||
|
|
||||||
|
Assert.Empty(requests);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Listening_twice_leaves_one_listener()
|
||||||
|
{
|
||||||
|
string suffix = UniqueSuffix();
|
||||||
|
var signal = new SettingsCloseSignal(suffix);
|
||||||
|
|
||||||
|
var requests = 0;
|
||||||
|
signal.CloseRequested += (_, _) => Interlocked.Increment(ref requests);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
signal.Listen();
|
||||||
|
signal.Listen();
|
||||||
|
|
||||||
|
Assert.True(RequestApart(suffix));
|
||||||
|
Pump.WaitFor(() => Volatile.Read(ref requests) > 0, "the request arrived");
|
||||||
|
Pump.Pause(TimeSpan.FromMilliseconds(80));
|
||||||
|
|
||||||
|
Assert.Equal(1, Volatile.Read(ref requests));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
signal.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Closing_without_listening_passes_without_consequence()
|
||||||
|
{
|
||||||
|
var signal = new SettingsCloseSignal(UniqueSuffix());
|
||||||
|
|
||||||
|
signal.Dispose();
|
||||||
|
signal.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every test gets a namespace of kernel objects of its own
|
||||||
|
private static string UniqueSuffix() => "." + Guid.NewGuid().ToString("N");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asks for the close the way the agent does it — from another process, and here
|
||||||
|
/// from another thread, which is as foreign as a test can get.
|
||||||
|
/// </summary>
|
||||||
|
private static bool RequestApart(string suffix)
|
||||||
|
{
|
||||||
|
var heard = false;
|
||||||
|
|
||||||
|
Pump.RunApart(() => heard = SettingsCloseSignal.RequestClose(suffix));
|
||||||
|
|
||||||
|
return heard;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -113,7 +113,7 @@ internal static class CaretNative
|
|||||||
private static PopupWindowNative.Rect? TryGetAutomationCaret()
|
private static PopupWindowNative.Rect? TryGetAutomationCaret()
|
||||||
{
|
{
|
||||||
// A hung application must not hang the popup along with it: we wait for the
|
// A hung application must not hang the popup along with it: we wait for the
|
||||||
// answer for a limited time, otherwise we show the popup at the cursor
|
// answer for a limited time, otherwise the popup goes to the fixed point
|
||||||
Task<PopupWindowNative.Rect?> query = Task.Run(QueryAutomationCaret);
|
Task<PopupWindowNative.Rect?> query = Task.Run(QueryAutomationCaret);
|
||||||
return query.Wait(AutomationTimeout) ? query.Result : null;
|
return query.Wait(AutomationTimeout) ? query.Result : null;
|
||||||
}
|
}
|
||||||
@@ -165,7 +165,7 @@ internal static class CaretNative
|
|||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
// Built without UI Automation: Chromium and Electron keep the system caret and MSAA
|
// Built without UI Automation: Chromium and Electron keep the system caret and MSAA
|
||||||
// steps above, and where those stay silent the popup falls back to the cursor
|
// steps above, and where those stay silent the popup falls back to the fixed point
|
||||||
private static PopupWindowNative.Rect? TryGetAutomationCaret() => null;
|
private static PopupWindowNative.Rect? TryGetAutomationCaret() => null;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ public enum PopupPlacementMode
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Next to the caret in the active input field. When the application does not
|
/// Next to the caret in the active input field. When the application does not
|
||||||
/// report its position, the popup is shown at the mouse cursor.
|
/// report its position, the popup is shown as in <see cref="FixedPoint"/> — at the
|
||||||
|
/// place and with the look that mode is set up with.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
AtCaret,
|
AtCaret,
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
namespace CursorLang.Core.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tells the settings window that the agent is quitting and it is to close with it.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The one thing the agent says to the settings window, and the mirror image of
|
||||||
|
/// <see cref="SettingsSignal"/>. "Exit" in the tray menu means the application is done
|
||||||
|
/// with, and a settings window left alone on the screen after it is a window belonging
|
||||||
|
/// to nothing: the tray icon it was opened from is gone, and closing it would be the
|
||||||
|
/// user's only remaining move.
|
||||||
|
///
|
||||||
|
/// A named event rather than a window message, because the agent has no handle to send
|
||||||
|
/// one to: the settings window lives in a process the agent starts and deliberately
|
||||||
|
/// does not keep hold of. The name has no <c>Global</c> prefix, so it lives in the
|
||||||
|
/// session namespace — same reasoning as <see cref="SingleInstanceGate"/>, and the same
|
||||||
|
/// consequence: with fast user switching each user's halves talk to their own.
|
||||||
|
///
|
||||||
|
/// Only the settings window creates the object; the agent opens what is already there
|
||||||
|
/// and stays silent when there is nothing. Were it the other way round, the request
|
||||||
|
/// would sit in an auto-reset event waiting for the next settings window to open and
|
||||||
|
/// close it the moment it did.
|
||||||
|
/// </remarks>
|
||||||
|
internal sealed class SettingsCloseSignal : IDisposable
|
||||||
|
{
|
||||||
|
private const string EventName = "CursorLang.CloseSettings";
|
||||||
|
|
||||||
|
private readonly string _eventName;
|
||||||
|
|
||||||
|
private EventWaitHandle? _request;
|
||||||
|
private RegisteredWaitHandle? _wait;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Listens on the name the two halves agree on.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="nameSuffix">
|
||||||
|
/// A namespace of its own. Empty for the application; the tests pass one so that
|
||||||
|
/// they do not answer for — or worse, close — a settings window someone is using.
|
||||||
|
/// </param>
|
||||||
|
internal SettingsCloseSignal(string nameSuffix = "") => _eventName = EventName + nameSuffix;
|
||||||
|
|
||||||
|
/// <summary>The agent asks for the window to be closed.</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Raised on a thread pool thread, wherever the wait happened to be answered — a
|
||||||
|
/// window obeys only its own, so the handler has to get back to it.
|
||||||
|
/// </remarks>
|
||||||
|
internal event EventHandler? CloseRequested;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asks the settings window of this session, if one is open, to close. Silence is
|
||||||
|
/// a normal answer: most of the time the user quits with no window on the screen.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Whether there was anybody to hear it.</returns>
|
||||||
|
internal static bool RequestClose(string nameSuffix = "")
|
||||||
|
{
|
||||||
|
if (!EventWaitHandle.TryOpenExisting(EventName + nameSuffix, out EventWaitHandle? request))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (request)
|
||||||
|
{
|
||||||
|
return request.Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Starts waiting for the request. Called once, by the settings window.</summary>
|
||||||
|
internal void Listen()
|
||||||
|
{
|
||||||
|
if (_request is not null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_request = new EventWaitHandle(false, EventResetMode.AutoReset, _eventName);
|
||||||
|
|
||||||
|
// As in the gate: the thread pool holds the wait, there is no reason to keep a
|
||||||
|
// thread of our own for a request that may never come
|
||||||
|
_wait = ThreadPool.RegisterWaitForSingleObject(
|
||||||
|
_request,
|
||||||
|
OnCloseSignalled,
|
||||||
|
state: null,
|
||||||
|
Timeout.Infinite,
|
||||||
|
executeOnlyOnce: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_wait?.Unregister(null);
|
||||||
|
_wait = null;
|
||||||
|
|
||||||
|
_request?.Dispose();
|
||||||
|
_request = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCloseSignalled(object? state, bool timedOut) =>
|
||||||
|
CloseRequested?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
using CursorLang.Core.Models;
|
using CursorLang.Core.Models;
|
||||||
using CursorLang.Core.Services;
|
using CursorLang.Core.Services;
|
||||||
using CursorLang.Settings.Tests.Infrastructure;
|
|
||||||
using CursorLang.Settings.ViewModels;
|
using CursorLang.Settings.ViewModels;
|
||||||
using CursorLang.Tests.Shared;
|
using CursorLang.Tests.Shared;
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ public partial class App : Application
|
|||||||
{
|
{
|
||||||
private ServiceProvider? _services;
|
private ServiceProvider? _services;
|
||||||
private SingleInstanceGate? _instanceGate;
|
private SingleInstanceGate? _instanceGate;
|
||||||
|
private SettingsCloseSignal? _closeSignal;
|
||||||
|
|
||||||
protected override void OnStartup(StartupEventArgs e)
|
protected override void OnStartup(StartupEventArgs e)
|
||||||
{
|
{
|
||||||
@@ -42,6 +43,10 @@ public partial class App : Application
|
|||||||
_instanceGate = gate;
|
_instanceGate = gate;
|
||||||
_instanceGate.ActivationRequested += OnActivationRequested;
|
_instanceGate.ActivationRequested += OnActivationRequested;
|
||||||
|
|
||||||
|
_closeSignal = new SettingsCloseSignal();
|
||||||
|
_closeSignal.CloseRequested += OnCloseRequested;
|
||||||
|
_closeSignal.Listen();
|
||||||
|
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
ConfigureServices(services);
|
ConfigureServices(services);
|
||||||
_services = services.BuildServiceProvider();
|
_services = services.BuildServiceProvider();
|
||||||
@@ -61,6 +66,12 @@ public partial class App : Application
|
|||||||
{
|
{
|
||||||
_services?.Dispose();
|
_services?.Dispose();
|
||||||
|
|
||||||
|
if (_closeSignal is not null)
|
||||||
|
{
|
||||||
|
_closeSignal.CloseRequested -= OnCloseRequested;
|
||||||
|
_closeSignal.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
if (_instanceGate is not null)
|
if (_instanceGate is not null)
|
||||||
{
|
{
|
||||||
_instanceGate.ActivationRequested -= OnActivationRequested;
|
_instanceGate.ActivationRequested -= OnActivationRequested;
|
||||||
@@ -95,6 +106,27 @@ public partial class App : Application
|
|||||||
private void OnActivationRequested(object? sender, EventArgs e) =>
|
private void OnActivationRequested(object? sender, EventArgs e) =>
|
||||||
Dispatcher.BeginInvoke(ShowMainWindow);
|
Dispatcher.BeginInvoke(ShowMainWindow);
|
||||||
|
|
||||||
|
// The agent is quitting. Answered on a thread pool thread, and the window is closed
|
||||||
|
// on its own one
|
||||||
|
private void OnCloseRequested(object? sender, EventArgs e) =>
|
||||||
|
Dispatcher.BeginInvoke(CloseMainWindow);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Closes the window the way the title bar button does, so that everything hanging
|
||||||
|
/// off closing happens; <see cref="ShutdownMode.OnMainWindowClose"/> ends the process
|
||||||
|
/// after it. Without a window there is nothing to close and the process simply ends.
|
||||||
|
/// </summary>
|
||||||
|
private void CloseMainWindow()
|
||||||
|
{
|
||||||
|
if (MainWindow is { } window)
|
||||||
|
{
|
||||||
|
window.Close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
private void ShowMainWindow()
|
private void ShowMainWindow()
|
||||||
{
|
{
|
||||||
if (MainWindow is not { } window)
|
if (MainWindow is not { } window)
|
||||||
|
|||||||
@@ -21,8 +21,8 @@
|
|||||||
<PublishTrimmed>false</PublishTrimmed>
|
<PublishTrimmed>false</PublishTrimmed>
|
||||||
<PublishReadyToRun Condition="'$(RuntimeIdentifier)' != ''">true</PublishReadyToRun>
|
<PublishReadyToRun Condition="'$(RuntimeIdentifier)' != ''">true</PublishReadyToRun>
|
||||||
<Version>1.0.0</Version>
|
<Version>1.0.0</Version>
|
||||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
<AssemblyVersion>$(Version)</AssemblyVersion>
|
||||||
<FileVersion>1.0.0.0</FileVersion>
|
<FileVersion>$(Version)</FileVersion>
|
||||||
<Product>CursorLang</Product>
|
<Product>CursorLang</Product>
|
||||||
<Company>Aleksandr Neichev</Company>
|
<Company>Aleksandr Neichev</Company>
|
||||||
<Description>Settings window of CursorLang</Description>
|
<Description>Settings window of CursorLang</Description>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
d:DataContext="{d:DesignInstance Type=vm:SettingsViewModel}"
|
d:DataContext="{d:DesignInstance Type=vm:SettingsViewModel}"
|
||||||
Title="{Binding Title}"
|
Title="{Binding Title}"
|
||||||
Width="1000" SizeToContent="Height" MaxHeight="900"
|
Width="560" SizeToContent="Height" MaxHeight="940"
|
||||||
ResizeMode="CanMinimize"
|
ResizeMode="CanMinimize"
|
||||||
Background="{DynamicResource Theme.WindowBackground}"
|
Background="{DynamicResource Theme.WindowBackground}"
|
||||||
Foreground="{DynamicResource Theme.Foreground}">
|
Foreground="{DynamicResource Theme.Foreground}">
|
||||||
@@ -59,370 +59,348 @@
|
|||||||
</Style>
|
</Style>
|
||||||
</Window.Resources>
|
</Window.Resources>
|
||||||
|
|
||||||
<!-- The settings are laid out in two columns: this way the window fits on the
|
|
||||||
screen entirely and does without scrolling. Scrolling is kept for the case of
|
|
||||||
a large system font, with which the content is taller than the monitor after all.
|
|
||||||
|
|
||||||
The left column holds what the popup does; the right one holds the single
|
|
||||||
section describing how and where the popup looks -->
|
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto" Padding="16">
|
<ScrollViewer VerticalScrollBarVisibility="Auto" Padding="16">
|
||||||
<Grid>
|
<StackPanel>
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<StackPanel>
|
<GroupBox Header="{Binding Localization[SectionInterface]}">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="164" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<GroupBox Header="{Binding Localization[SectionInterface]}">
|
<TextBlock Style="{StaticResource FieldLabel}"
|
||||||
<Grid>
|
Text="{Binding Localization[LanguageLabel]}" />
|
||||||
<Grid.ColumnDefinitions>
|
<ComboBox Grid.Column="1"
|
||||||
<ColumnDefinition Width="164" />
|
ItemsSource="{Binding Localization.AvailableLanguages}"
|
||||||
<ColumnDefinition Width="*" />
|
DisplayMemberPath="DisplayName"
|
||||||
</Grid.ColumnDefinitions>
|
SelectedValuePath="Code"
|
||||||
<Grid.RowDefinitions>
|
SelectedValue="{Binding Settings.Language}" />
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<TextBlock Style="{StaticResource FieldLabel}"
|
<TextBlock Grid.Row="1" Margin="0,12,12,0"
|
||||||
Text="{Binding Localization[LanguageLabel]}" />
|
Style="{StaticResource FieldLabel}"
|
||||||
<ComboBox Grid.Column="1"
|
Text="{Binding Localization[ThemeLabel]}" />
|
||||||
ItemsSource="{Binding Localization.AvailableLanguages}"
|
<ComboBox Grid.Row="1" Grid.Column="1" Margin="0,12,0,0"
|
||||||
DisplayMemberPath="DisplayName"
|
ItemsSource="{Binding Themes}"
|
||||||
SelectedValuePath="Code"
|
ItemTemplate="{StaticResource EnumOptionTemplate}"
|
||||||
SelectedValue="{Binding Settings.Language}" />
|
SelectedValuePath="Value"
|
||||||
|
SelectedValue="{Binding Settings.Theme}" />
|
||||||
|
</Grid>
|
||||||
|
</GroupBox>
|
||||||
|
|
||||||
<TextBlock Grid.Row="1" Margin="0,12,12,0"
|
<GroupBox Header="{Binding Localization[SectionPopup]}">
|
||||||
Style="{StaticResource FieldLabel}"
|
<Grid>
|
||||||
Text="{Binding Localization[ThemeLabel]}" />
|
<Grid.ColumnDefinitions>
|
||||||
<ComboBox Grid.Row="1" Grid.Column="1" Margin="0,12,0,0"
|
<ColumnDefinition Width="164" />
|
||||||
ItemsSource="{Binding Themes}"
|
<ColumnDefinition Width="*" />
|
||||||
ItemTemplate="{StaticResource EnumOptionTemplate}"
|
<ColumnDefinition Width="Auto" />
|
||||||
SelectedValuePath="Value"
|
</Grid.ColumnDefinitions>
|
||||||
SelectedValue="{Binding Settings.Theme}" />
|
<Grid.RowDefinitions>
|
||||||
</Grid>
|
<RowDefinition Height="Auto" />
|
||||||
</GroupBox>
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<GroupBox Header="{Binding Localization[SectionBehavior]}">
|
<TextBlock Style="{StaticResource FieldLabel}">
|
||||||
<Grid>
|
<Run Text="{Binding Localization[PlacementModeLabel], Mode=OneWay}" />
|
||||||
<Grid.ColumnDefinitions>
|
<InlineUIContainer BaselineAlignment="Baseline">
|
||||||
<ColumnDefinition Width="164" />
|
<TextBlock Text="{Binding Localization[MoreInfoLink]}"
|
||||||
<ColumnDefinition Width="*" />
|
Foreground="{DynamicResource Theme.Accent}"
|
||||||
<ColumnDefinition Width="Auto" />
|
TextDecorations="Underline"
|
||||||
</Grid.ColumnDefinitions>
|
Cursor="Help"
|
||||||
<Grid.RowDefinitions>
|
ToolTipService.InitialShowDelay="200"
|
||||||
<RowDefinition Height="Auto" />
|
ToolTipService.ShowDuration="60000">
|
||||||
<RowDefinition Height="Auto" />
|
<TextBlock.ToolTip>
|
||||||
<RowDefinition Height="Auto" />
|
<ToolTip>
|
||||||
<RowDefinition Height="Auto" />
|
<TextBlock TextWrapping="Wrap" MaxWidth="320"
|
||||||
<RowDefinition Height="Auto" />
|
Text="{Binding Localization[PopupLookPerModeHint]}" />
|
||||||
<RowDefinition Height="Auto" />
|
</ToolTip>
|
||||||
</Grid.RowDefinitions>
|
</TextBlock.ToolTip>
|
||||||
|
</TextBlock>
|
||||||
|
</InlineUIContainer>
|
||||||
|
</TextBlock>
|
||||||
|
<ComboBox Grid.Column="1" Grid.ColumnSpan="2"
|
||||||
|
ItemsSource="{Binding PlacementModes}"
|
||||||
|
ItemTemplate="{StaticResource EnumOptionTemplate}"
|
||||||
|
SelectedValuePath="Value"
|
||||||
|
SelectedValue="{Binding Settings.PlacementMode}" />
|
||||||
|
|
||||||
<TextBlock Style="{StaticResource FieldLabel}"
|
<!-- The "at cursor" mode: a side and an offset of its own -->
|
||||||
Text="{Binding Localization[DurationLabel]}" />
|
<TextBlock Grid.Row="1" Margin="0,12,12,0"
|
||||||
<Slider Grid.Column="1" Minimum="200" Maximum="5000" TickFrequency="100"
|
Style="{StaticResource FieldLabel}"
|
||||||
Value="{Binding Settings.DurationMilliseconds}" />
|
Text="{Binding Localization[CursorCornerLabel]}"
|
||||||
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
<TextBlock Style="{StaticResource FieldValue}"
|
Converter={StaticResource EnumToVisibility},
|
||||||
Text="{Binding Settings.DurationMilliseconds, StringFormat={}{0:F0}}" />
|
ConverterParameter=AtCursor}" />
|
||||||
<TextBlock Margin="4,0,0,0"
|
<ComboBox Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
||||||
Foreground="{DynamicResource Theme.SecondaryForeground}"
|
ItemsSource="{Binding AnchorSides}"
|
||||||
Text="{Binding Localization[MillisecondsSuffix]}" />
|
ItemTemplate="{StaticResource EnumOptionTemplate}"
|
||||||
</StackPanel>
|
SelectedValuePath="Value"
|
||||||
|
SelectedValue="{Binding Settings.AtCursor.Side}"
|
||||||
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
|
Converter={StaticResource EnumToVisibility},
|
||||||
|
ConverterParameter=AtCursor}" />
|
||||||
|
|
||||||
<TextBlock Grid.Row="1" Margin="0,12,12,0"
|
<TextBlock Grid.Row="2" Margin="0,12,12,0"
|
||||||
Style="{StaticResource FieldLabel}"
|
Style="{StaticResource FieldLabel}"
|
||||||
Text="{Binding Localization[CapsLockLabel]}" />
|
Text="{Binding Localization[CursorOffsetLabel]}"
|
||||||
<CheckBox Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
IsChecked="{Binding Settings.UseCapsLockHotkey}"
|
Converter={StaticResource EnumToVisibility},
|
||||||
Content="{Binding Localization[CapsLockHotkeyCheck]}" />
|
ConverterParameter=AtCursor}" />
|
||||||
|
<Slider Grid.Row="2" Grid.Column="1" Margin="0,12,0,0"
|
||||||
|
Minimum="0" Maximum="80" TickFrequency="1"
|
||||||
|
Value="{Binding Settings.AtCursor.Offset}"
|
||||||
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
|
Converter={StaticResource EnumToVisibility},
|
||||||
|
ConverterParameter=AtCursor}" />
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="2" Margin="12,12,0,0"
|
||||||
|
Style="{StaticResource FieldValue}"
|
||||||
|
Text="{Binding Settings.AtCursor.Offset, StringFormat={}{0:F0}}"
|
||||||
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
|
Converter={StaticResource EnumToVisibility},
|
||||||
|
ConverterParameter=AtCursor}" />
|
||||||
|
|
||||||
<TextBlock Grid.Row="2" Margin="0,12,12,0"
|
<!-- The "at caret" mode: a side and an offset of its own -->
|
||||||
Style="{StaticResource FieldLabel}"
|
<TextBlock Grid.Row="3" Margin="0,12,12,0"
|
||||||
Text="{Binding Localization[CapsLockHoldLabel]}" />
|
Style="{StaticResource FieldLabel}"
|
||||||
<Slider Grid.Row="2" Grid.Column="1" Margin="0,12,0,0"
|
Text="{Binding Localization[CursorCornerLabel]}"
|
||||||
Minimum="150" Maximum="1500" TickFrequency="50"
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
Value="{Binding Settings.CapsLockHoldMilliseconds}"
|
Converter={StaticResource EnumToVisibility},
|
||||||
IsEnabled="{Binding Settings.UseCapsLockHotkey}" />
|
ConverterParameter=AtCaret}" />
|
||||||
<StackPanel Grid.Row="2" Grid.Column="2" Margin="0,12,0,0"
|
<ComboBox Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
||||||
Orientation="Horizontal" VerticalAlignment="Center">
|
ItemsSource="{Binding CaretSides}"
|
||||||
<TextBlock Style="{StaticResource FieldValue}"
|
ItemTemplate="{StaticResource EnumOptionTemplate}"
|
||||||
Text="{Binding Settings.CapsLockHoldMilliseconds, StringFormat={}{0:F0}}" />
|
SelectedValuePath="Value"
|
||||||
<TextBlock Margin="4,0,0,0"
|
SelectedValue="{Binding Settings.AtCaret.Side}"
|
||||||
Foreground="{DynamicResource Theme.SecondaryForeground}"
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
Text="{Binding Localization[MillisecondsSuffix]}" />
|
Converter={StaticResource EnumToVisibility},
|
||||||
</StackPanel>
|
ConverterParameter=AtCaret}" />
|
||||||
|
|
||||||
<TextBlock Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2"
|
<TextBlock Grid.Row="4" Margin="0,12,12,0"
|
||||||
Margin="0,6,0,0" TextWrapping="Wrap"
|
Style="{StaticResource FieldLabel}"
|
||||||
Foreground="{DynamicResource Theme.SecondaryForeground}">
|
Text="{Binding Localization[CursorOffsetLabel]}"
|
||||||
<Run Text="{Binding Localization[CapsLockHoldHint], Mode=OneWay}" />
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
<InlineUIContainer BaselineAlignment="Baseline">
|
Converter={StaticResource EnumToVisibility},
|
||||||
<!-- The elevation caveat lives in the tooltip to keep the section compact -->
|
ConverterParameter=AtCaret}" />
|
||||||
<TextBlock Text="{Binding Localization[MoreInfoLink]}"
|
<Slider Grid.Row="4" Grid.Column="1" Margin="0,12,0,0"
|
||||||
Foreground="{DynamicResource Theme.Accent}"
|
Minimum="0" Maximum="80" TickFrequency="1"
|
||||||
TextDecorations="Underline"
|
Value="{Binding Settings.AtCaret.Offset}"
|
||||||
Cursor="Help"
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
ToolTipService.InitialShowDelay="200"
|
Converter={StaticResource EnumToVisibility},
|
||||||
ToolTipService.ShowDuration="60000"
|
ConverterParameter=AtCaret}" />
|
||||||
Visibility="{Binding Settings.UseCapsLockHotkey, Converter={StaticResource BooleanToVisibility}}">
|
<TextBlock Grid.Row="4" Grid.Column="2" Margin="12,12,0,0"
|
||||||
<TextBlock.ToolTip>
|
Style="{StaticResource FieldValue}"
|
||||||
<ToolTip>
|
Text="{Binding Settings.AtCaret.Offset, StringFormat={}{0:F0}}"
|
||||||
<TextBlock TextWrapping="Wrap"
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
Text="{Binding Localization[CapsLockElevationHint]}" />
|
Converter={StaticResource EnumToVisibility},
|
||||||
</ToolTip>
|
ConverterParameter=AtCaret}" />
|
||||||
</TextBlock.ToolTip>
|
|
||||||
</TextBlock>
|
|
||||||
</InlineUIContainer>
|
|
||||||
</TextBlock>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="4" Margin="0,12,12,0"
|
<!-- The "fixed point" mode -->
|
||||||
Style="{StaticResource FieldLabel}"
|
<TextBlock Grid.Row="5" Margin="0,12,12,0"
|
||||||
Visibility="{Binding IsStartupAvailable, Converter={StaticResource BooleanToVisibility}}"
|
Style="{StaticResource FieldLabel}"
|
||||||
Text="{Binding Localization[StartupLabel]}" />
|
Text="{Binding Localization[ScreenPositionLabel]}"
|
||||||
<CheckBox Grid.Row="4" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
Visibility="{Binding IsStartupAvailable, Converter={StaticResource BooleanToVisibility}}"
|
Converter={StaticResource EnumToVisibility},
|
||||||
IsEnabled="{Binding CanChangeStartup}"
|
ConverterParameter=FixedPoint}" />
|
||||||
IsChecked="{Binding RunAtStartup}"
|
<ComboBox Grid.Row="5" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
||||||
Content="{Binding Localization[StartupCheck]}" />
|
ItemsSource="{Binding ScreenPositions}"
|
||||||
|
ItemTemplate="{StaticResource EnumOptionTemplate}"
|
||||||
|
SelectedValuePath="Value"
|
||||||
|
SelectedValue="{Binding Settings.FixedPoint.Position}"
|
||||||
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
|
Converter={StaticResource EnumToVisibility},
|
||||||
|
ConverterParameter=FixedPoint}" />
|
||||||
|
|
||||||
<TextBlock Grid.Row="5" Grid.Column="1" Grid.ColumnSpan="2"
|
<TextBlock Grid.Row="6" Margin="0,12,12,0"
|
||||||
Margin="0,6,0,0" TextWrapping="Wrap"
|
Style="{StaticResource FieldLabel}"
|
||||||
Foreground="{DynamicResource Theme.SecondaryForeground}"
|
Text="{Binding Localization[ScreenMarginLabel]}"
|
||||||
Visibility="{Binding IsStartupLocked, Converter={StaticResource BooleanToVisibility}}"
|
IsEnabled="{Binding Settings.FixedPoint.IsAtAnEdge}"
|
||||||
Text="{Binding Localization[StartupLockedHint]}" />
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
</Grid>
|
Converter={StaticResource EnumToVisibility},
|
||||||
</GroupBox>
|
ConverterParameter=FixedPoint}" />
|
||||||
|
<Slider Grid.Row="6" Grid.Column="1" Margin="0,12,0,0"
|
||||||
|
Minimum="0" Maximum="200" TickFrequency="1"
|
||||||
|
Value="{Binding Settings.FixedPoint.Offset}"
|
||||||
|
IsEnabled="{Binding Settings.FixedPoint.IsAtAnEdge}"
|
||||||
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
|
Converter={StaticResource EnumToVisibility},
|
||||||
|
ConverterParameter=FixedPoint}" />
|
||||||
|
<TextBlock Grid.Row="6" Grid.Column="2" Margin="12,12,0,0"
|
||||||
|
Style="{StaticResource FieldValue}"
|
||||||
|
Text="{Binding Settings.FixedPoint.Offset, StringFormat={}{0:F0}}"
|
||||||
|
IsEnabled="{Binding Settings.FixedPoint.IsAtAnEdge}"
|
||||||
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
|
Converter={StaticResource EnumToVisibility},
|
||||||
|
ConverterParameter=FixedPoint}" />
|
||||||
|
|
||||||
</StackPanel>
|
<Border Grid.Row="7" Grid.ColumnSpan="3"
|
||||||
|
Margin="0,16,0,0" Height="1"
|
||||||
|
Background="{DynamicResource Theme.ControlBorder}" />
|
||||||
|
|
||||||
<StackPanel Grid.Column="1" Margin="16,0,0,0">
|
<TextBlock Grid.Row="8" Margin="0,12,12,0"
|
||||||
|
Style="{StaticResource FieldLabel}"
|
||||||
|
Text="{Binding Localization[FontSizeLabel]}" />
|
||||||
|
<Slider Grid.Row="8" Grid.Column="1" Margin="0,12,0,0"
|
||||||
|
Minimum="10" Maximum="72" TickFrequency="1"
|
||||||
|
Value="{Binding Settings.Current.FontSize}" />
|
||||||
|
<TextBlock Grid.Row="8" Grid.Column="2" Margin="12,12,0,0"
|
||||||
|
Style="{StaticResource FieldValue}"
|
||||||
|
Text="{Binding Settings.Current.FontSize, StringFormat={}{0:F0}}" />
|
||||||
|
|
||||||
<!-- Where the popup appears and how it looks are one question for the
|
<TextBlock Grid.Row="9" Margin="0,12,12,0"
|
||||||
user, so both live in a single section: the rule is picked at the
|
Style="{StaticResource FieldLabel}"
|
||||||
top and its result is seen in the preview at the bottom -->
|
Text="{Binding Localization[OpacityLabel]}" />
|
||||||
<GroupBox Header="{Binding Localization[SectionPopup]}">
|
<Slider Grid.Row="9" Grid.Column="1" Margin="0,12,0,0"
|
||||||
<Grid>
|
Minimum="0.1" Maximum="1" TickFrequency="0.05"
|
||||||
<Grid.ColumnDefinitions>
|
Value="{Binding Settings.Current.Opacity}" />
|
||||||
<ColumnDefinition Width="164" />
|
<TextBlock Grid.Row="9" Grid.Column="2" Margin="12,12,0,0"
|
||||||
<ColumnDefinition Width="*" />
|
Style="{StaticResource FieldValue}"
|
||||||
<ColumnDefinition Width="Auto" />
|
Text="{Binding Settings.Current.Opacity, StringFormat={}{0:P0}}" />
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<TextBlock Style="{StaticResource FieldLabel}">
|
<TextBlock Grid.Row="10" Margin="0,12,12,0"
|
||||||
<Run Text="{Binding Localization[PlacementModeLabel], Mode=OneWay}" />
|
Style="{StaticResource FieldLabel}"
|
||||||
<InlineUIContainer BaselineAlignment="Baseline">
|
Text="{Binding Localization[BackgroundColorLabel]}" />
|
||||||
<TextBlock Text="{Binding Localization[MoreInfoLink]}"
|
<ComboBox Grid.Row="10" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
||||||
Foreground="{DynamicResource Theme.Accent}"
|
ItemsSource="{Binding BackgroundPalette}"
|
||||||
TextDecorations="Underline"
|
ItemTemplate="{StaticResource ColorSwatchTemplate}"
|
||||||
Cursor="Help"
|
SelectedItem="{Binding Settings.Current.BackgroundColor}" />
|
||||||
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"
|
|
||||||
ItemsSource="{Binding PlacementModes}"
|
|
||||||
ItemTemplate="{StaticResource EnumOptionTemplate}"
|
|
||||||
SelectedValuePath="Value"
|
|
||||||
SelectedValue="{Binding Settings.PlacementMode}" />
|
|
||||||
|
|
||||||
<!-- The "at cursor" mode: a side and an offset of its own -->
|
<TextBlock Grid.Row="11" Margin="0,12,12,0"
|
||||||
<TextBlock Grid.Row="1" Margin="0,12,12,0"
|
Style="{StaticResource FieldLabel}"
|
||||||
Style="{StaticResource FieldLabel}"
|
Text="{Binding Localization[TextColorLabel]}" />
|
||||||
Text="{Binding Localization[CursorCornerLabel]}"
|
<ComboBox Grid.Row="11" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
ItemsSource="{Binding TextPalette}"
|
||||||
Converter={StaticResource EnumToVisibility},
|
ItemTemplate="{StaticResource ColorSwatchTemplate}"
|
||||||
ConverterParameter=AtCursor}" />
|
SelectedItem="{Binding Settings.Current.ForegroundColor}" />
|
||||||
<ComboBox Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
|
||||||
ItemsSource="{Binding AnchorSides}"
|
|
||||||
ItemTemplate="{StaticResource EnumOptionTemplate}"
|
|
||||||
SelectedValuePath="Value"
|
|
||||||
SelectedValue="{Binding Settings.AtCursor.Side}"
|
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
|
||||||
Converter={StaticResource EnumToVisibility},
|
|
||||||
ConverterParameter=AtCursor}" />
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="2" Margin="0,12,12,0"
|
<TextBlock Grid.Row="12" Margin="0,12,12,0"
|
||||||
Style="{StaticResource FieldLabel}"
|
Style="{StaticResource FieldLabel}"
|
||||||
Text="{Binding Localization[CursorOffsetLabel]}"
|
Text="{Binding Localization[PreviewLabel]}" />
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
<Border Grid.Row="12" Grid.Column="1" Grid.ColumnSpan="2"
|
||||||
Converter={StaticResource EnumToVisibility},
|
Margin="0,12,0,0" Padding="16"
|
||||||
ConverterParameter=AtCursor}" />
|
Height="152" ClipToBounds="True"
|
||||||
<Slider Grid.Row="2" Grid.Column="1" Margin="0,12,0,0"
|
Background="{DynamicResource Theme.SurfaceStrong}" CornerRadius="4"
|
||||||
Minimum="0" Maximum="80" TickFrequency="1"
|
HorizontalAlignment="Stretch">
|
||||||
Value="{Binding Settings.AtCursor.Offset}"
|
<Border CornerRadius="4" Padding="10,4"
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
Converter={StaticResource EnumToVisibility},
|
Opacity="{Binding Settings.Current.Opacity}"
|
||||||
ConverterParameter=AtCursor}" />
|
Background="{Binding Settings.Current.BackgroundColor,
|
||||||
<TextBlock Grid.Row="2" Grid.Column="2" Margin="12,12,0,0"
|
Converter={StaticResource ColorToBrush}}">
|
||||||
Style="{StaticResource FieldValue}"
|
<TextBlock Text="RU" FontWeight="SemiBold"
|
||||||
Text="{Binding Settings.AtCursor.Offset, StringFormat={}{0:F0}}"
|
FontSize="{Binding Settings.Current.FontSize}"
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
Foreground="{Binding Settings.Current.ForegroundColor,
|
||||||
Converter={StaticResource EnumToVisibility},
|
Converter={StaticResource ColorToBrush}}" />
|
||||||
ConverterParameter=AtCursor}" />
|
|
||||||
|
|
||||||
<!-- The "at caret" mode: a side and an offset of its own -->
|
|
||||||
<TextBlock Grid.Row="3" Margin="0,12,12,0"
|
|
||||||
Style="{StaticResource FieldLabel}"
|
|
||||||
Text="{Binding Localization[CursorCornerLabel]}"
|
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
|
||||||
Converter={StaticResource EnumToVisibility},
|
|
||||||
ConverterParameter=AtCaret}" />
|
|
||||||
<ComboBox Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
|
||||||
ItemsSource="{Binding CaretSides}"
|
|
||||||
ItemTemplate="{StaticResource EnumOptionTemplate}"
|
|
||||||
SelectedValuePath="Value"
|
|
||||||
SelectedValue="{Binding Settings.AtCaret.Side}"
|
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
|
||||||
Converter={StaticResource EnumToVisibility},
|
|
||||||
ConverterParameter=AtCaret}" />
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="4" Margin="0,12,12,0"
|
|
||||||
Style="{StaticResource FieldLabel}"
|
|
||||||
Text="{Binding Localization[CursorOffsetLabel]}"
|
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
|
||||||
Converter={StaticResource EnumToVisibility},
|
|
||||||
ConverterParameter=AtCaret}" />
|
|
||||||
<Slider Grid.Row="4" Grid.Column="1" Margin="0,12,0,0"
|
|
||||||
Minimum="0" Maximum="80" TickFrequency="1"
|
|
||||||
Value="{Binding Settings.AtCaret.Offset}"
|
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
|
||||||
Converter={StaticResource EnumToVisibility},
|
|
||||||
ConverterParameter=AtCaret}" />
|
|
||||||
<TextBlock Grid.Row="4" Grid.Column="2" Margin="12,12,0,0"
|
|
||||||
Style="{StaticResource FieldValue}"
|
|
||||||
Text="{Binding Settings.AtCaret.Offset, StringFormat={}{0:F0}}"
|
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
|
||||||
Converter={StaticResource EnumToVisibility},
|
|
||||||
ConverterParameter=AtCaret}" />
|
|
||||||
|
|
||||||
<!-- The "fixed point" mode -->
|
|
||||||
<TextBlock Grid.Row="5" Margin="0,12,12,0"
|
|
||||||
Style="{StaticResource FieldLabel}"
|
|
||||||
Text="{Binding Localization[ScreenPositionLabel]}"
|
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
|
||||||
Converter={StaticResource EnumToVisibility},
|
|
||||||
ConverterParameter=FixedPoint}" />
|
|
||||||
<ComboBox Grid.Row="5" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
|
||||||
ItemsSource="{Binding ScreenPositions}"
|
|
||||||
ItemTemplate="{StaticResource EnumOptionTemplate}"
|
|
||||||
SelectedValuePath="Value"
|
|
||||||
SelectedValue="{Binding Settings.FixedPoint.Position}"
|
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
|
||||||
Converter={StaticResource EnumToVisibility},
|
|
||||||
ConverterParameter=FixedPoint}" />
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="6" Margin="0,12,12,0"
|
|
||||||
Style="{StaticResource FieldLabel}"
|
|
||||||
Text="{Binding Localization[ScreenMarginLabel]}"
|
|
||||||
IsEnabled="{Binding Settings.FixedPoint.IsAtAnEdge}"
|
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
|
||||||
Converter={StaticResource EnumToVisibility},
|
|
||||||
ConverterParameter=FixedPoint}" />
|
|
||||||
<Slider Grid.Row="6" Grid.Column="1" Margin="0,12,0,0"
|
|
||||||
Minimum="0" Maximum="200" TickFrequency="1"
|
|
||||||
Value="{Binding Settings.FixedPoint.Offset}"
|
|
||||||
IsEnabled="{Binding Settings.FixedPoint.IsAtAnEdge}"
|
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
|
||||||
Converter={StaticResource EnumToVisibility},
|
|
||||||
ConverterParameter=FixedPoint}" />
|
|
||||||
<TextBlock Grid.Row="6" Grid.Column="2" Margin="12,12,0,0"
|
|
||||||
Style="{StaticResource FieldValue}"
|
|
||||||
Text="{Binding Settings.FixedPoint.Offset, StringFormat={}{0:F0}}"
|
|
||||||
IsEnabled="{Binding Settings.FixedPoint.IsAtAnEdge}"
|
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
|
||||||
Converter={StaticResource EnumToVisibility},
|
|
||||||
ConverterParameter=FixedPoint}" />
|
|
||||||
|
|
||||||
<!-- A hairline between the two halves of the section: the
|
|
||||||
placement above, the look of the popup below -->
|
|
||||||
<Border Grid.Row="7" Grid.ColumnSpan="3"
|
|
||||||
Margin="0,16,0,0" Height="1"
|
|
||||||
Background="{DynamicResource Theme.ControlBorder}" />
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="8" Margin="0,12,12,0"
|
|
||||||
Style="{StaticResource FieldLabel}"
|
|
||||||
Text="{Binding Localization[FontSizeLabel]}" />
|
|
||||||
<Slider Grid.Row="8" Grid.Column="1" Margin="0,12,0,0"
|
|
||||||
Minimum="10" Maximum="72" TickFrequency="1"
|
|
||||||
Value="{Binding Settings.Current.FontSize}" />
|
|
||||||
<TextBlock Grid.Row="8" Grid.Column="2" Margin="12,12,0,0"
|
|
||||||
Style="{StaticResource FieldValue}"
|
|
||||||
Text="{Binding Settings.Current.FontSize, StringFormat={}{0:F0}}" />
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="9" Margin="0,12,12,0"
|
|
||||||
Style="{StaticResource FieldLabel}"
|
|
||||||
Text="{Binding Localization[OpacityLabel]}" />
|
|
||||||
<Slider Grid.Row="9" Grid.Column="1" Margin="0,12,0,0"
|
|
||||||
Minimum="0.1" Maximum="1" TickFrequency="0.05"
|
|
||||||
Value="{Binding Settings.Current.Opacity}" />
|
|
||||||
<TextBlock Grid.Row="9" Grid.Column="2" Margin="12,12,0,0"
|
|
||||||
Style="{StaticResource FieldValue}"
|
|
||||||
Text="{Binding Settings.Current.Opacity, StringFormat={}{0:P0}}" />
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="10" Margin="0,12,12,0"
|
|
||||||
Style="{StaticResource FieldLabel}"
|
|
||||||
Text="{Binding Localization[BackgroundColorLabel]}" />
|
|
||||||
<ComboBox Grid.Row="10" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
|
||||||
ItemsSource="{Binding BackgroundPalette}"
|
|
||||||
ItemTemplate="{StaticResource ColorSwatchTemplate}"
|
|
||||||
SelectedItem="{Binding Settings.Current.BackgroundColor}" />
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="11" Margin="0,12,12,0"
|
|
||||||
Style="{StaticResource FieldLabel}"
|
|
||||||
Text="{Binding Localization[TextColorLabel]}" />
|
|
||||||
<ComboBox Grid.Row="11" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
|
||||||
ItemsSource="{Binding TextPalette}"
|
|
||||||
ItemTemplate="{StaticResource ColorSwatchTemplate}"
|
|
||||||
SelectedItem="{Binding Settings.Current.ForegroundColor}" />
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="12" Margin="0,12,12,0"
|
|
||||||
Style="{StaticResource FieldLabel}"
|
|
||||||
Text="{Binding Localization[PreviewLabel]}" />
|
|
||||||
<Border Grid.Row="12" Grid.Column="1" Grid.ColumnSpan="2"
|
|
||||||
Margin="0,12,0,0" Padding="16"
|
|
||||||
Height="152" ClipToBounds="True"
|
|
||||||
Background="{DynamicResource Theme.SurfaceStrong}" CornerRadius="4"
|
|
||||||
HorizontalAlignment="Stretch">
|
|
||||||
<Border CornerRadius="4" Padding="10,4"
|
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
|
||||||
Opacity="{Binding Settings.Current.Opacity}"
|
|
||||||
Background="{Binding Settings.Current.BackgroundColor,
|
|
||||||
Converter={StaticResource ColorToBrush}}">
|
|
||||||
<TextBlock Text="RU" FontWeight="SemiBold"
|
|
||||||
FontSize="{Binding Settings.Current.FontSize}"
|
|
||||||
Foreground="{Binding Settings.Current.ForegroundColor,
|
|
||||||
Converter={StaticResource ColorToBrush}}" />
|
|
||||||
</Border>
|
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Border>
|
||||||
</GroupBox>
|
</Grid>
|
||||||
|
</GroupBox>
|
||||||
|
|
||||||
</StackPanel>
|
<GroupBox Header="{Binding Localization[SectionBehavior]}">
|
||||||
</Grid>
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="164" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Style="{StaticResource FieldLabel}"
|
||||||
|
Text="{Binding Localization[DurationLabel]}" />
|
||||||
|
<Slider Grid.Column="1" Minimum="200" Maximum="5000" TickFrequency="100"
|
||||||
|
Value="{Binding Settings.DurationMilliseconds}" />
|
||||||
|
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<TextBlock Style="{StaticResource FieldValue}"
|
||||||
|
Text="{Binding Settings.DurationMilliseconds, StringFormat={}{0:F0}}" />
|
||||||
|
<TextBlock Margin="4,0,0,0"
|
||||||
|
Foreground="{DynamicResource Theme.SecondaryForeground}"
|
||||||
|
Text="{Binding Localization[MillisecondsSuffix]}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Margin="0,12,12,0"
|
||||||
|
Style="{StaticResource FieldLabel}"
|
||||||
|
Text="{Binding Localization[CapsLockLabel]}" />
|
||||||
|
<CheckBox Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
||||||
|
IsChecked="{Binding Settings.UseCapsLockHotkey}"
|
||||||
|
Content="{Binding Localization[CapsLockHotkeyCheck]}" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Margin="0,12,12,0"
|
||||||
|
Style="{StaticResource FieldLabel}"
|
||||||
|
Text="{Binding Localization[CapsLockHoldLabel]}" />
|
||||||
|
<Slider Grid.Row="2" Grid.Column="1" Margin="0,12,0,0"
|
||||||
|
Minimum="150" Maximum="1500" TickFrequency="50"
|
||||||
|
Value="{Binding Settings.CapsLockHoldMilliseconds}"
|
||||||
|
IsEnabled="{Binding Settings.UseCapsLockHotkey}" />
|
||||||
|
<StackPanel Grid.Row="2" Grid.Column="2" Margin="0,12,0,0"
|
||||||
|
Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<TextBlock Style="{StaticResource FieldValue}"
|
||||||
|
Text="{Binding Settings.CapsLockHoldMilliseconds, StringFormat={}{0:F0}}" />
|
||||||
|
<TextBlock Margin="4,0,0,0"
|
||||||
|
Foreground="{DynamicResource Theme.SecondaryForeground}"
|
||||||
|
Text="{Binding Localization[MillisecondsSuffix]}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2"
|
||||||
|
Margin="0,6,0,0" TextWrapping="Wrap"
|
||||||
|
Foreground="{DynamicResource Theme.SecondaryForeground}">
|
||||||
|
<Run Text="{Binding Localization[CapsLockHoldHint], Mode=OneWay}" />
|
||||||
|
<InlineUIContainer BaselineAlignment="Baseline">
|
||||||
|
<!-- The elevation caveat lives in the tooltip to keep the section compact -->
|
||||||
|
<TextBlock Text="{Binding Localization[MoreInfoLink]}"
|
||||||
|
Foreground="{DynamicResource Theme.Accent}"
|
||||||
|
TextDecorations="Underline"
|
||||||
|
Cursor="Help"
|
||||||
|
ToolTipService.InitialShowDelay="200"
|
||||||
|
ToolTipService.ShowDuration="60000"
|
||||||
|
Visibility="{Binding Settings.UseCapsLockHotkey, Converter={StaticResource BooleanToVisibility}}">
|
||||||
|
<TextBlock.ToolTip>
|
||||||
|
<ToolTip>
|
||||||
|
<TextBlock TextWrapping="Wrap"
|
||||||
|
Text="{Binding Localization[CapsLockElevationHint]}" />
|
||||||
|
</ToolTip>
|
||||||
|
</TextBlock.ToolTip>
|
||||||
|
</TextBlock>
|
||||||
|
</InlineUIContainer>
|
||||||
|
</TextBlock>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="4" Margin="0,12,12,0"
|
||||||
|
Style="{StaticResource FieldLabel}"
|
||||||
|
Visibility="{Binding IsStartupAvailable, Converter={StaticResource BooleanToVisibility}}"
|
||||||
|
Text="{Binding Localization[StartupLabel]}" />
|
||||||
|
<CheckBox Grid.Row="4" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
||||||
|
Visibility="{Binding IsStartupAvailable, Converter={StaticResource BooleanToVisibility}}"
|
||||||
|
IsEnabled="{Binding CanChangeStartup}"
|
||||||
|
IsChecked="{Binding RunAtStartup}"
|
||||||
|
Content="{Binding Localization[StartupCheck]}" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="5" Grid.Column="1" Grid.ColumnSpan="2"
|
||||||
|
Margin="0,6,0,0" TextWrapping="Wrap"
|
||||||
|
Foreground="{DynamicResource Theme.SecondaryForeground}"
|
||||||
|
Visibility="{Binding IsStartupLocked, Converter={StaticResource BooleanToVisibility}}"
|
||||||
|
Text="{Binding Localization[StartupLockedHint]}" />
|
||||||
|
</Grid>
|
||||||
|
</GroupBox>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@@ -16,12 +16,25 @@
|
|||||||
<OutputType>Package</OutputType>
|
<OutputType>Package</OutputType>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
MSI validation still assumes an installation for the whole machine.
|
The ICE checks run inside the Windows Installer service, and a build
|
||||||
Installing into the user's own profile trips three of its rules: a
|
agent has no access to it: every single check comes back as WIX0217,
|
||||||
component whose key path is a file (ICE38), a folder it wants listed
|
"The Windows Installer Service could not be accessed", and the build
|
||||||
for removal (ICE64), and a warning that the files will not follow other
|
fails on close to a hundred of them. Since the release is built by the
|
||||||
users of the machine (ICE91). All three describe exactly what was
|
agent, validation cannot be part of it.
|
||||||
intended here, so they are turned off rather than worked around.
|
|
||||||
|
It stays switchable rather than deleted: -p:SuppressValidation=false
|
||||||
|
turns it back on where the service does answer, which is any ordinary
|
||||||
|
desktop machine.
|
||||||
|
-->
|
||||||
|
<SuppressValidation Condition="'$(SuppressValidation)' == ''">true</SuppressValidation>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
What validation says when it does run. MSI still assumes an
|
||||||
|
installation for the whole machine, and installing into the user's own
|
||||||
|
profile trips three of its rules: a component whose key path is a file
|
||||||
|
(ICE38), a folder it wants listed for removal (ICE64), and a warning
|
||||||
|
that the files will not follow other users of the machine (ICE91). All
|
||||||
|
three describe exactly what was intended here.
|
||||||
-->
|
-->
|
||||||
<SuppressIces>ICE38;ICE64;ICE91</SuppressIces>
|
<SuppressIces>ICE38;ICE64;ICE91</SuppressIces>
|
||||||
|
|
||||||
|
|||||||
@@ -136,8 +136,8 @@
|
|||||||
Condition="REMOVE="ALL" AND NOT UPGRADINGPRODUCTCODE" />
|
Condition="REMOVE="ALL" AND NOT UPGRADINGPRODUCTCODE" />
|
||||||
</InstallExecuteSequence>
|
</InstallExecuteSequence>
|
||||||
|
|
||||||
<!-- Offered at the end of the wizard, the way an installer usually does -->
|
<SetProperty Id="WixShellExecTarget" Value="[INSTALLFOLDER]CursorLang.exe"
|
||||||
<Property Id="WixShellExecTarget" Value="[#CursorLang.exe]" />
|
After="CostFinalize" Sequence="ui" />
|
||||||
<CustomAction Id="LaunchApplication" BinaryRef="Wix4UtilCA_$(sys.BUILDARCHSHORT)"
|
<CustomAction Id="LaunchApplication" BinaryRef="Wix4UtilCA_$(sys.BUILDARCHSHORT)"
|
||||||
DllEntry="WixShellExec" Impersonate="yes" Return="ignore" />
|
DllEntry="WixShellExec" Impersonate="yes" Return="ignore" />
|
||||||
|
|
||||||
|
|||||||
+48
-11
@@ -84,20 +84,28 @@ MSIX всегда выполняются в контексте вошедшег
|
|||||||
раскладкой. Без UI, и так должно остаться: всё, что попадёт туда, попадёт и
|
раскладкой. Без UI, и так должно остаться: всё, что попадёт туда, попадёт и
|
||||||
в фоновый процесс.
|
в фоновый процесс.
|
||||||
|
|
||||||
Связь между ними — только `settings.json`. Окно пишет его целиком, во временный
|
Всё, что окно сообщает агенту, идёт только через `settings.json`. Окно пишет его
|
||||||
файл, который одним движением встаёт на место, и посылает агенту
|
целиком, во временный файл, который одним движением встаёт на место, и посылает
|
||||||
зарегистрированное оконное сообщение, по которому тот перечитывает файл.
|
агенту зарегистрированное оконное сообщение, по которому тот перечитывает файл.
|
||||||
Сообщение не несёт данных: пересылка самих изменений лишила бы файл роли
|
Сообщение не несёт данных: пересылка самих изменений лишила бы файл роли
|
||||||
единственного источника правды. Работающий агент необязателен — без него окно
|
единственного источника правды. Работающий агент необязателен — без него окно
|
||||||
работает так же. За файлом никто не следит: писатель у него один, и он сам
|
работает так же. За файлом никто не следит: писатель у него один, и он сам
|
||||||
сообщает о записи.
|
сообщает о записи.
|
||||||
|
|
||||||
|
В обратную сторону агент говорит одно слово, и то последнее: пункт «Выход» в меню
|
||||||
|
трея просит открытое окно настроек закрыться, прежде чем агент уйдёт, — чтобы на
|
||||||
|
экране не осталось окна, которому больше ничего не принадлежит. Просьба идёт
|
||||||
|
именованным событием сессии, а не оконным сообщением: агент запускает этот процесс
|
||||||
|
и намеренно не держит на него ссылки. Чаще всего слушать её некому, и это
|
||||||
|
нормальный ответ.
|
||||||
|
|
||||||
## Трей
|
## Трей
|
||||||
|
|
||||||
Окно настроек — гость на экране, а не само приложение: оно показывается после
|
Окно настроек — гость на экране, а не само приложение: оно показывается после
|
||||||
установки и всякий раз, когда его просят иконка или её меню. Обе кнопки в
|
установки и всякий раз, когда его просят иконка или её меню. Обе кнопки в
|
||||||
заголовке окна означают ровно то, что написано: окно закрывается, а его процесс
|
заголовке окна означают ровно то, что написано: окно закрывается, а его процесс
|
||||||
завершается. Выход из самого приложения — пункт «Выход» в меню трея.
|
завершается. Выход из самого приложения — пункт «Выход» в меню трея, и открытое
|
||||||
|
окно настроек он закрывает вместе с агентом.
|
||||||
|
|
||||||
Меню иконки системное, его рисует Windows. Надписи по-прежнему следуют языку,
|
Меню иконки системное, его рисует Windows. Надписи по-прежнему следуют языку,
|
||||||
выбранному в настройках, а тема до меню больше не дотягивается: меню на WPF
|
выбранному в настройках, а тема до меню больше не дотягивается: меню на WPF
|
||||||
@@ -161,6 +169,24 @@ MSIX всегда выполняются в контексте вошедшег
|
|||||||
на месте, а не исчезает из раздела — появляющаяся и исчезающая строка сдвигала бы
|
на месте, а не исчезает из раздела — появляющаяся и исчезающая строка сдвигала бы
|
||||||
всё, что ниже, при каждой смене места.
|
всё, что ниже, при каждой смене места.
|
||||||
|
|
||||||
|
Положение каретки сообщает не всякое приложение, и тогда рядом с курсором ввода —
|
||||||
|
нигде. Подсказка уходит на фиксированную точку: место, отступ и оформление берутся
|
||||||
|
оттуда, из режима каретки не берётся ничего. Курсор мыши был бы запасным вариантом
|
||||||
|
поближе — и неверным: он там, где его оставили, сбоку или на другом мониторе, тогда
|
||||||
|
как смотрят на каретку. Фиксированная точка всегда на одном и том же месте, и потому
|
||||||
|
известно, куда смотреть, — а вместе с местом должно приходить и оформление: подсказка,
|
||||||
|
настроенная мелкой и незаметной для середины текста, в углу монитора останется
|
||||||
|
незамеченной.
|
||||||
|
|
||||||
|
Какие-то приложения, решает не то, на чём они написаны, а то, что окно о себе
|
||||||
|
сообщает. Обычный элемент Win32 заводит системную каретку, и её читают сразу;
|
||||||
|
Chrome и Electron рисуют свою и сообщают о ней через интерфейсы доступности.
|
||||||
|
Окну, которое рисует текст на голом холсте, сообщать нечего — и оба случая
|
||||||
|
уживаются в одном приложении: редакторы JetBrains отвечают, потому что каретку
|
||||||
|
для них держит их среда выполнения, а панель инструментов, нарисованная Compose,
|
||||||
|
не содержит текстового элемента, который нашли бы UI Automation или Java Access
|
||||||
|
Bridge. Ради такого окна фиксированная точка и нужна.
|
||||||
|
|
||||||
К какому режиму относится оформление, объясняет всплывающая подсказка рядом с самим
|
К какому режиму относится оформление, объясняет всплывающая подсказка рядом с самим
|
||||||
режимом, а не строка текста в разделе: ползунки ниже показывают другие числа после
|
режимом, а не строка текста в разделе: ползунки ниже показывают другие числа после
|
||||||
переключения режима, и это вопрос, который задают один раз.
|
переключения режима, и это вопрос, который задают один раз.
|
||||||
@@ -333,10 +359,21 @@ pwsh -File Packaging\build-installer.ps1 -Version 1.0.1
|
|||||||
снимет: SmartScreen смотрит на репутацию, а у нового сертификата её нет, пока
|
снимет: SmartScreen смотрит на репутацию, а у нового сертификата её нет, пока
|
||||||
приложение не наберёт установок.
|
приложение не наберёт установок.
|
||||||
|
|
||||||
Про проект WiX стоит знать две вещи, прежде чем его править. Он закреплён на WiX
|
Про проект WiX стоит знать две вещи, прежде чем его править.
|
||||||
5, а не на нынешней 7: начиная с шестой версии инструмент требует принимать
|
|
||||||
лицензию Open Source Maintenance Fee — бесплатную при доходе меньше $10 000 в
|
Он закреплён на WiX 5, а не на нынешней 7: начиная с шестой версии инструмент
|
||||||
год, но принимать её должен человек, а не сборочный скрипт. И он отключает три
|
требует принимать лицензию Open Source Maintenance Fee — бесплатную при доходе
|
||||||
проверки ICE: MSI по-прежнему исходит из установки на всю машину, а установка в
|
меньше $10 000 в год, но принимать её должен человек, а не сборочный скрипт.
|
||||||
профиль пользователя нарушает правила, которые описывают ровно то, что здесь и
|
|
||||||
задумано.
|
И он собирается без проверки MSI. Проверки ICE выполняются службой установщика
|
||||||
|
Windows, до которой сборочному агенту не дотянуться: каждая возвращается
|
||||||
|
ошибкой `WIX0217`, и сборка умирает на без малого сотне таких. На обычной
|
||||||
|
машине служба отвечает, и проверка включается одним ключом:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet build Packaging\Installer\CursorLang.wixproj -p:SuppressValidation=false
|
||||||
|
```
|
||||||
|
|
||||||
|
Три правила остаются подавленными и тогда. MSI исходит из установки на всю
|
||||||
|
машину, а установка в профиль пользователя нарушает правила, которые описывают
|
||||||
|
ровно то, что здесь и задумано.
|
||||||
|
|||||||
@@ -81,19 +81,26 @@ the next time the window is asked for.
|
|||||||
the layout tracking. No UI, and it must stay that way — whatever lands there lands
|
the layout tracking. No UI, and it must stay that way — whatever lands there lands
|
||||||
in the background process.
|
in the background process.
|
||||||
|
|
||||||
The connection between the two is `settings.json` and nothing else. The window
|
Everything the window has to say to the agent goes through `settings.json` and
|
||||||
writes it — whole, into a temporary file moved into place in one step — and then
|
nothing else. The window writes it — whole, into a temporary file moved into place in one step — and then
|
||||||
posts a registered window message to the agent, which re-reads. The message carries
|
posts a registered window message to the agent, which re-reads. The message carries
|
||||||
no data: sending the changed values along would make the file stop being the only
|
no data: sending the changed values along would make the file stop being the only
|
||||||
source of truth. An agent that is not running is a normal case — the window works the
|
source of truth. An agent that is not running is a normal case — the window works the
|
||||||
same. Nobody watches the file: it has one writer, and that writer speaks up.
|
same. Nobody watches the file: it has one writer, and that writer speaks up.
|
||||||
|
|
||||||
|
The one word in the other direction is the last one: Exit in the tray menu asks an
|
||||||
|
open settings window to close before the agent goes, so that no window is left
|
||||||
|
belonging to nothing. It travels as a named event of the session rather than a
|
||||||
|
window message — the agent starts that process and deliberately keeps no handle to
|
||||||
|
it. Nobody listening is the usual case, and it is a normal answer.
|
||||||
|
|
||||||
## The tray
|
## The tray
|
||||||
|
|
||||||
The settings window is a guest on the screen rather than the app itself: it shows
|
The settings window is a guest on the screen rather than the app itself: it shows
|
||||||
up after the installation and whenever the icon or its menu is asked for. Both
|
up after the installation and whenever the icon or its menu is asked for. Both
|
||||||
buttons in its title bar mean what they say — the window closes and its process
|
buttons in its title bar mean what they say — the window closes and its process
|
||||||
ends. The way out of the app itself is the Exit item of the tray menu.
|
ends. The way out of the app itself is the Exit item of the tray menu — and it
|
||||||
|
closes an open settings window along with the agent.
|
||||||
|
|
||||||
The menu of the icon is a system one, drawn by Windows. Its captions still follow
|
The menu of the icon is a system one, drawn by Windows. Its captions still follow
|
||||||
the language chosen in the settings, but the theme no longer reaches it: a WPF
|
the language chosen in the settings, but the theme no longer reaches it: a WPF
|
||||||
@@ -156,6 +163,24 @@ 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
|
leaving the section — a row that comes and goes would move everything below it on
|
||||||
every switch of the place.
|
every switch of the place.
|
||||||
|
|
||||||
|
Not every application reports where its caret is, and next to the caret is then
|
||||||
|
nowhere. The popup goes to the fixed point instead — the place, the offset and the
|
||||||
|
look all from that mode, none of them from the caret one. The mouse cursor would be
|
||||||
|
the nearer fallback and is the wrong one: it is wherever it was last left, off to a
|
||||||
|
side or on another monitor, while the eyes are on the caret. The fixed point is
|
||||||
|
always in the same place, so it is known where to look — and the look must come with
|
||||||
|
it, since a popup set small and quiet for the middle of a text goes unnoticed in the
|
||||||
|
corner of a monitor.
|
||||||
|
|
||||||
|
Some applications those are is decided by what a window exposes rather than by
|
||||||
|
what it is written in. A Win32 control creates a system caret and is read at once;
|
||||||
|
Chromium and Electron draw their own and report it over the accessibility
|
||||||
|
interfaces. A window that paints its text into a bare canvas has nothing to report
|
||||||
|
at all — and both kinds live in the same application: the editors of a JetBrains
|
||||||
|
IDE answer, since the runtime keeps a caret for them, while a tool window whose
|
||||||
|
interface is painted by Compose holds no text component for UI Automation or the
|
||||||
|
Java Access Bridge to find. That window is where the fixed point earns its keep.
|
||||||
|
|
||||||
Which mode the look belongs to is explained in a tooltip next to the mode itself
|
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
|
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.
|
after a switch of the mode, and that is a question asked once.
|
||||||
@@ -327,10 +352,21 @@ user has to insist. Buying a certificate would not silence it at once either:
|
|||||||
SmartScreen goes by reputation, and a fresh certificate has none until enough
|
SmartScreen goes by reputation, and a fresh certificate has none until enough
|
||||||
people have installed the application.
|
people have installed the application.
|
||||||
|
|
||||||
Two things about the WiX project are worth knowing before touching it. It pins
|
Two things about the WiX project are worth knowing before touching it.
|
||||||
WiX 5 rather than the current 7: from version 6 the toolset asks every build to
|
|
||||||
accept the Open Source Maintenance Fee licence — free below $10,000 of yearly
|
It pins WiX 5 rather than the current 7: from version 6 the toolset asks every
|
||||||
revenue, but a decision for a person rather than for a build script. And it turns
|
build to accept the Open Source Maintenance Fee licence — free below $10,000 of
|
||||||
off three ICE validation rules: MSI still assumes an installation for the whole
|
yearly revenue, but a decision for a person rather than for a build script.
|
||||||
machine, and installing into the user's own profile trips rules that describe
|
|
||||||
exactly what was intended here.
|
And it builds without MSI validation. The ICE checks run inside the Windows
|
||||||
|
Installer service, which a build agent cannot reach: every check comes back as
|
||||||
|
`WIX0217` and the build dies on close to a hundred of them. On an ordinary
|
||||||
|
desktop machine the service does answer, and validation is one switch away:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet build Packaging\Installer\CursorLang.wixproj -p:SuppressValidation=false
|
||||||
|
```
|
||||||
|
|
||||||
|
Three of its rules stay suppressed even then. MSI assumes an installation for the
|
||||||
|
whole machine, and installing into the user's own profile trips rules that
|
||||||
|
describe exactly what was intended here.
|
||||||
|
|||||||
Reference in New Issue
Block a user