modified caret mode #11
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|
||||||
|
|||||||
@@ -169,6 +169,15 @@ MSIX всегда выполняются в контексте вошедшег
|
|||||||
на месте, а не исчезает из раздела — появляющаяся и исчезающая строка сдвигала бы
|
на месте, а не исчезает из раздела — появляющаяся и исчезающая строка сдвигала бы
|
||||||
всё, что ниже, при каждой смене места.
|
всё, что ниже, при каждой смене места.
|
||||||
|
|
||||||
|
Положение каретки сообщает не всякое приложение, и тогда рядом с курсором ввода —
|
||||||
|
нигде. Подсказка уходит на фиксированную точку: место, отступ и оформление берутся
|
||||||
|
оттуда, из режима каретки не берётся ничего. Курсор мыши был бы запасным вариантом
|
||||||
|
поближе — и неверным: он там, где его оставили, сбоку или на другом мониторе, тогда
|
||||||
|
как смотрят на каретку. Фиксированная точка всегда на одном и том же месте, и потому
|
||||||
|
известно, куда смотреть, — а вместе с местом должно приходить и оформление: подсказка,
|
||||||
|
настроенная мелкой и незаметной для середины текста, в углу монитора останется
|
||||||
|
незамеченной.
|
||||||
|
|
||||||
К какому режиму относится оформление, объясняет всплывающая подсказка рядом с самим
|
К какому режиму относится оформление, объясняет всплывающая подсказка рядом с самим
|
||||||
режимом, а не строка текста в разделе: ползунки ниже показывают другие числа после
|
режимом, а не строка текста в разделе: ползунки ниже показывают другие числа после
|
||||||
переключения режима, и это вопрос, который задают один раз.
|
переключения режима, и это вопрос, который задают один раз.
|
||||||
|
|||||||
@@ -163,6 +163,15 @@ 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.
|
||||||
|
|
||||||
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.
|
||||||
|
|||||||
Reference in New Issue
Block a user