Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4eb9f6c671 | |||
| a830f55575 | |||
| 07e31228d7 |
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
cmake_minimum_required(VERSION 3.24)
|
cmake_minimum_required(VERSION 3.24)
|
||||||
project(KSwitcher VERSION 0.6.14 LANGUAGES CXX RC)
|
project(KSwitcher VERSION 0.6.17 LANGUAGES CXX RC)
|
||||||
if(NOT WIN32)
|
if(NOT WIN32)
|
||||||
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
|
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -160,7 +160,11 @@ Hold Ctrl to start drawing over an existing object. Line style applies to outlin
|
|||||||
for shapes.
|
for shapes.
|
||||||
**Freehand line (F)** draws an opaque curve by following the mouse. The curve is one object:
|
**Freehand line (F)** draws an opaque curve by following the mouse. The curve is one object:
|
||||||
move it, change its color or thickness, clone it, or drag its endpoints. Hold Ctrl to start a new
|
move it, change its color or thickness, clone it, or drag its endpoints. Hold Ctrl to start a new
|
||||||
stroke over an existing object. The **Line style** selector supports Solid, Dotted, Dashed and
|
stroke over an existing object. Hold **Shift** while drawing to constrain the current segment
|
||||||
|
to a horizontal, vertical or 45-degree diagonal line; release Shift to resume freehand drawing in the same stroke.
|
||||||
|
While holding Shift, moving more than 50 image pixels away from the current line selects a new
|
||||||
|
direction at the last drawn endpoint, preserving earlier segments. Shift can be pressed and
|
||||||
|
released repeatedly within one stroke. The **Line style** selector supports Solid, Dotted, Dashed and
|
||||||
Dash-dot; **Ends** supports an arrow at the end, start, both ends, or no arrows. These settings
|
Dash-dot; **Ends** supports an arrow at the end, start, both ends, or no arrows. These settings
|
||||||
apply to selected straight/freehand lines and set defaults for subsequent lines. Both are disabled
|
apply to selected straight/freehand lines and set defaults for subsequent lines. Both are disabled
|
||||||
for numbered markers and highlights unless a line drawing tool is active. Undo/Redo includes style changes.
|
for numbered markers and highlights unless a line drawing tool is active. Undo/Redo includes style changes.
|
||||||
@@ -224,7 +228,10 @@ thumbnails and source labels aligned with the scaled fonts and spacing.
|
|||||||
- Clipboard snapshot/restoration with concurrent-copy, foreground and input-version checks.
|
- Clipboard snapshot/restoration with concurrent-copy, foreground and input-version checks.
|
||||||
- Case cycling, Russian/Ukrainian transliteration, verified undo and repeated conversion.
|
- Case cycling, Russian/Ukrainian transliteration, verified undo and repeated conversion.
|
||||||
- Text snippets across layouts, multiline insertion and a verified `{cursor}` position;
|
- Text snippets across layouts, multiline insertion and a verified `{cursor}` position;
|
||||||
`{{cursor}}` inserts the literal marker. Snippets are stored as plaintext settings, as in [Typlo](https://github.com/bitcodepro/Typlo).
|
`{enter}` presses Enter after insertion and cursor placement (once per marker), before buffered typing resumes.
|
||||||
|
For example, `rider .{enter}` inserts `rider .` and presses Enter.
|
||||||
|
Automatic correction is suppressed during snippet expansion and for typing buffered by that expansion; the global setting is preserved.
|
||||||
|
`{{cursor}}` and `{{enter}}` insert literal markers. Snippets are stored as plaintext settings, as in [Typlo](https://github.com/bitcodepro/Typlo).
|
||||||
- Clipboard history for text, HTML/RTF, file lists, DIB and PNG images; plain-text paste;
|
- Clipboard history for text, HTML/RTF, file lists, DIB and PNG images; plain-text paste;
|
||||||
full-text search, pinning, deletion, full text viewing and image previews.
|
full-text search, pinning, deletion, full text viewing and image previews.
|
||||||
- Image thumbnails and text/file icons appear directly in the history list. Only visible rows
|
- Image thumbnails and text/file icons appear directly in the history list. Only visible rows
|
||||||
|
|||||||
Binary file not shown.
+6
-8
@@ -1,17 +1,15 @@
|
|||||||
# Latest Windows release
|
# Latest Windows release
|
||||||
|
|
||||||
## v0.6.14 — 2026-10-30
|
## v0.6.17 — 2026-10-30
|
||||||
|
|
||||||
## Changes
|
## Changes
|
||||||
- Restore resizing for the automatic-correction settings dialog with an adaptive layout.
|
- Hold Shift with the screenshot pencil to draw horizontal, vertical or 45-degree diagonal segments. Release Shift to continue freehand within the same stroke.
|
||||||
- Open the dialog with larger personal-dictionary fields. The three dictionaries share additional width evenly and receive additional height as the window grows.
|
- Move more than 50 image pixels away from the active line to change direction while preserving earlier segments. Smooth diagonal gestures use the actual mouse trajectory instead of producing stair steps.
|
||||||
- Expand exclusion lists and punctuation fields horizontally, and move lower settings and action buttons with the window while preserving spacing.
|
- Keep diagonal strokes inside image boundaries without changing their angle. Ctrl retains its existing draw-over-objects behavior; undo/redo treats the complete stroke as one object.
|
||||||
- Retain scrolling when the dialog must fit a small screen.
|
- Disable snippet fields, Delete and marker insertion until a snippet is selected or added. Deleting the selected snippet clears and disables the editor.
|
||||||
|
|
||||||
Includes the earlier automatic-correction and log-viewer improvements. Existing modern releases update directly to this version without installing intermediate releases.
|
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
220 native regression checks, 472 input/UI checks including dictionary sizing and footer placement, screenshot editor and notification registration checks, 45 updater checks and updater progress checks. The resized dialog was also confirmed in the development build by the user.
|
Passed 223 native regression checks, 472 input/UI checks, screenshot editor UI and notification registration checks, 48 updater checks, updater progress checks, and 45 isolated configurator process checks. Screenshot UI coverage includes smooth diagonal gestures in all four directions, direction changes, image boundaries, Shift transitions, Ctrl behavior and undo/redo. Configurator checks cover disabled snippet editing, Add, Delete, reselection and preservation of snippet content.
|
||||||
|
|
||||||
**Upgrade:** choose Update in the tray menu. Legacy v0.3.0–v0.4.1 clients first receive the v0.4.2 compatibility bridge, then the current full release. v0.2.3 and earlier require manual installation.
|
**Upgrade:** choose Update in the tray menu. Legacy v0.3.0–v0.4.1 clients first receive the v0.4.2 compatibility bridge, then the current full release. v0.2.3 and earlier require manual installation.
|
||||||
For manual installation, exit KSwitcher and extract the entire ZIP into its installation folder. Keep personal `data/` and customized dictionaries.
|
For manual installation, exit KSwitcher and extract the entire ZIP into its installation folder. Keep personal `data/` and customized dictionaries.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
4939b83b2cd641e969107091012fe12340b09d73ec1b7f7292c6809d226e05f5 KSwitcher-win-x64.zip
|
d5813a42105a2aebb5b6ef414c0546a44f18e4d7f3d45e0f32830097781b9677 KSwitcher-win-x64.zip
|
||||||
|
|||||||
+3
-3
@@ -9,8 +9,8 @@ IDC_SCREENSHOT_PENCIL CURSOR "icons/lucide/pencil.cur"
|
|||||||
1 RT_MANIFEST "app.manifest"
|
1 RT_MANIFEST "app.manifest"
|
||||||
201 RCDATA "update.ps1"
|
201 RCDATA "update.ps1"
|
||||||
1 VERSIONINFO
|
1 VERSIONINFO
|
||||||
FILEVERSION 0,6,14,0
|
FILEVERSION 0,6,17,0
|
||||||
PRODUCTVERSION 0,6,14,0
|
PRODUCTVERSION 0,6,17,0
|
||||||
FILEOS VOS_NT_WINDOWS32
|
FILEOS VOS_NT_WINDOWS32
|
||||||
FILETYPE VFT_APP
|
FILETYPE VFT_APP
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -20,7 +20,7 @@ BEGIN
|
|||||||
BEGIN
|
BEGIN
|
||||||
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
|
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
|
||||||
VALUE "ProductName", "KSwitcher"
|
VALUE "ProductName", "KSwitcher"
|
||||||
VALUE "FileVersion", "0.6.14"
|
VALUE "FileVersion", "0.6.17"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
BLOCK "VarFileInfo"
|
BLOCK "VarFileInfo"
|
||||||
|
|||||||
+58
-17
@@ -12,6 +12,11 @@ namespace ks
|
|||||||
App *App::current = nullptr;
|
App *App::current = nullptr;
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
ULONG_PTR snippetReplayMarker()
|
||||||
|
{
|
||||||
|
static const char marker{};
|
||||||
|
return reinterpret_cast<ULONG_PTR>(&marker);
|
||||||
|
}
|
||||||
std::vector<std::pair<int, std::wstring>> shortcuts(const Settings &s)
|
std::vector<std::pair<int, std::wstring>> shortcuts(const Settings &s)
|
||||||
{
|
{
|
||||||
std::vector<std::pair<int, std::wstring>> out{{Toggle, s.toggle},
|
std::vector<std::pair<int, std::wstring>> out{{Toggle, s.toggle},
|
||||||
@@ -566,7 +571,8 @@ void App::keyboard(const KBDLLHOOKSTRUCT &k, UINT mods)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
run.record(ch, fg, layout, GetTickCount64());
|
run.record(ch, fg, layout, GetTickCount64());
|
||||||
if (settings.autoCorrect && mods == 0 && ch == L' ' && run.keys.size() >= (size_t)settings.autoMinLength + 1 &&
|
if (settings.autoCorrect && !snippetTyping && k.dwExtraInfo != snippetReplayMarker() &&
|
||||||
|
mods == 0 && ch == L' ' && run.keys.size() >= (size_t)settings.autoMinLength + 1 &&
|
||||||
run.keys.size() <= 41 && !busy && !run.overflow)
|
run.keys.size() <= 41 && !busy && !run.overflow)
|
||||||
{
|
{
|
||||||
for (int source = 0; source < (int)layouts.size(); ++source)
|
for (int source = 0; source < (int)layouts.size(); ++source)
|
||||||
@@ -619,7 +625,18 @@ void App::keyboard(const KBDLLHOOKSTRUCT &k, UINT mods)
|
|||||||
for (auto &trigger : variants)
|
for (auto &trigger : variants)
|
||||||
if (keys.size() >= trigger.size() && keys.ends_with(trigger))
|
if (keys.size() >= trigger.size() && keys.ends_with(trigger))
|
||||||
{
|
{
|
||||||
PostMessageW(window, SnippetMessage, (WPARAM)i, 0);
|
// Keep subsequent typing out of the selection/paste transaction.
|
||||||
|
typingTarget = fg;
|
||||||
|
snippetPending = (int)i;
|
||||||
|
snippetTyping = true;
|
||||||
|
busy = true;
|
||||||
|
if (!PostMessageW(window, SnippetMessage, (WPARAM)i, (LPARAM)inputVersion.load()))
|
||||||
|
{
|
||||||
|
typingTarget = nullptr;
|
||||||
|
snippetPending = -1;
|
||||||
|
snippetTyping = false;
|
||||||
|
busy = false;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -693,12 +710,13 @@ bool App::handleKeyboard(const KBDLLHOOKSTRUCT &key, bool down, UINT mods)
|
|||||||
return true;
|
return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
bool App::beginTypingDeferral(HWND target, uint64_t version)
|
bool App::beginTypingDeferral(HWND target, uint64_t version, bool snippet)
|
||||||
{
|
{
|
||||||
std::lock_guard lock(stateMutex);
|
std::lock_guard lock(stateMutex);
|
||||||
if (inputVersion != version || GetForegroundWindow() != target || !modifiersReleased())
|
if (inputVersion != version || GetForegroundWindow() != target || !modifiersReleased())
|
||||||
return false;
|
return false;
|
||||||
typingTarget = target;
|
typingTarget = target;
|
||||||
|
snippetTyping = snippet;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
void App::replayTyping()
|
void App::replayTyping()
|
||||||
@@ -710,7 +728,12 @@ void App::replayTyping()
|
|||||||
target = typingTarget;
|
target = typingTarget;
|
||||||
typingTarget = nullptr;
|
typingTarget = nullptr;
|
||||||
autoPending = false;
|
autoPending = false;
|
||||||
|
snippetPending = -1;
|
||||||
inputs.swap(deferredTyping);
|
inputs.swap(deferredTyping);
|
||||||
|
if (snippetTyping)
|
||||||
|
for (auto &input : inputs)
|
||||||
|
input.ki.dwExtraInfo = snippetReplayMarker();
|
||||||
|
snippetTyping = false;
|
||||||
}
|
}
|
||||||
busy = false;
|
busy = false;
|
||||||
if (inputs.empty())
|
if (inputs.empty())
|
||||||
@@ -826,14 +849,19 @@ void App::transform(int command, int snippet)
|
|||||||
L" version=" + std::to_wstring(currentVersion) +
|
L" version=" + std::to_wstring(currentVersion) +
|
||||||
L" repeat=" + std::to_wstring(repeatAttempts));
|
L" repeat=" + std::to_wstring(repeatAttempts));
|
||||||
}
|
}
|
||||||
const bool preDeferred = command == AutoCorrect && autoPending;
|
const bool preDeferred = (command == AutoCorrect && autoPending) ||
|
||||||
|
(snippet >= 0 && snippetPending == snippet);
|
||||||
if (busy.exchange(true) && !preDeferred)
|
if (busy.exchange(true) && !preDeferred)
|
||||||
{
|
{
|
||||||
if (manual)
|
if (manual)
|
||||||
diagnosticEvent(L"Transform outcome id=" + std::to_wstring(attempt) + L" reason=busy", true);
|
diagnosticEvent(L"Transform outcome id=" + std::to_wstring(attempt) + L" reason=busy", true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (preDeferred) autoPending = false;
|
if (preDeferred)
|
||||||
|
{
|
||||||
|
autoPending = false;
|
||||||
|
snippetPending = -1;
|
||||||
|
}
|
||||||
Run recent;
|
Run recent;
|
||||||
std::optional<LastTransform> previous;
|
std::optional<LastTransform> previous;
|
||||||
Settings config;
|
Settings config;
|
||||||
@@ -904,7 +932,7 @@ void App::transform(int command, int snippet)
|
|||||||
reason != L"paste-sent-unverified" && reason != L"layout-only" &&
|
reason != L"paste-sent-unverified" && reason != L"layout-only" &&
|
||||||
reason != L"unchanged-expected");
|
reason != L"unchanged-expected");
|
||||||
}
|
}
|
||||||
} audit{*this, target, version, attempt, requestedAt, manual, reason, text, result, expected, hint, to};
|
} audit{*this, target, version, attempt, requestedAt, manual || snippet >= 0, reason, text, result, expected, hint, to};
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
auto valid = [&]
|
auto valid = [&]
|
||||||
@@ -946,9 +974,9 @@ void App::transform(int command, int snippet)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
reason = L"clipboard-snapshot";
|
reason = L"clipboard-snapshot";
|
||||||
if (command == AutoCorrect)
|
if (command == AutoCorrect || snippet >= 0)
|
||||||
{
|
{
|
||||||
if (!beginTypingDeferral(target, version))
|
if (!beginTypingDeferral(target, version, snippet >= 0))
|
||||||
return;
|
return;
|
||||||
reset.deferred = true;
|
reset.deferred = true;
|
||||||
}
|
}
|
||||||
@@ -1043,13 +1071,11 @@ void App::transform(int command, int snippet)
|
|||||||
tail.keys = tail.keys.substr(tail.keys.size() - sn.trigger.size());
|
tail.keys = tail.keys.substr(tail.keys.size() - sn.trigger.size());
|
||||||
if (!tx.select(sn.trigger.size()))
|
if (!tx.select(sn.trigger.size()))
|
||||||
return;
|
return;
|
||||||
|
autoSelected = true;
|
||||||
|
reason = L"snippet-verification-mismatch";
|
||||||
text = tx.copy();
|
text = tx.copy();
|
||||||
if (tail.match(text, available, physicalLayout()) < 0)
|
if (tail.match(text, available, physicalLayout()) < 0)
|
||||||
{
|
|
||||||
if (tx.valid())
|
|
||||||
chord(VK_RIGHT);
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
result = parseTemplate(sn.text).text;
|
result = parseTemplate(sn.text).text;
|
||||||
automatic = true;
|
automatic = true;
|
||||||
}
|
}
|
||||||
@@ -1162,12 +1188,22 @@ void App::transform(int command, int snippet)
|
|||||||
if (snippet >= 0 && sent && tx.valid())
|
if (snippet >= 0 && sent && tx.valid())
|
||||||
{
|
{
|
||||||
auto t = parseTemplate(config.snippets[snippet].text);
|
auto t = parseTemplate(config.snippets[snippet].text);
|
||||||
if (!t.suffix.empty() && tx.select(caretSteps(t.suffix)))
|
if (!t.suffix.empty())
|
||||||
{
|
{
|
||||||
|
if (!tx.select(caretSteps(t.suffix))) return;
|
||||||
auto suffix = tx.copy();
|
auto suffix = tx.copy();
|
||||||
if (tx.valid())
|
if (!tx.valid()) return;
|
||||||
chord(newlines(suffix) == newlines(t.suffix) ? VK_LEFT : VK_RIGHT);
|
if (newlines(suffix) != newlines(t.suffix))
|
||||||
|
{
|
||||||
|
chord(VK_RIGHT);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
if (!chord(VK_LEFT)) return;
|
||||||
|
}
|
||||||
|
// Action markers run after insertion and cursor placement,
|
||||||
|
// before any physical typing buffered during expansion.
|
||||||
|
for (int i = 0; i < t.enters && tx.valid(); ++i)
|
||||||
|
if (!chord(VK_RETURN)) break;
|
||||||
}
|
}
|
||||||
if (to >= 0 && config.switchLayout && tx.valid() && (sent || result == text))
|
if (to >= 0 && config.switchLayout && tx.valid() && (sent || result == text))
|
||||||
{
|
{
|
||||||
@@ -1517,15 +1553,20 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
case AutoCorrectMessage:
|
case AutoCorrectMessage:
|
||||||
if (app->autoPending)
|
if (app->autoPending)
|
||||||
{
|
{
|
||||||
if (!app->installingUpdate && app->settings.autoCorrect && app->inputVersion.load() == (uint64_t)wp)
|
if (!app->installingUpdate && !app->snippetTyping && app->settings.autoCorrect && app->inputVersion.load() == (uint64_t)wp)
|
||||||
app->transform(AutoCorrect);
|
app->transform(AutoCorrect);
|
||||||
else
|
else
|
||||||
app->replayTyping();
|
app->replayTyping();
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
case SnippetMessage:
|
case SnippetMessage:
|
||||||
if (app->installingUpdate) return 0;
|
if (app->snippetPending == (int)wp)
|
||||||
|
{
|
||||||
|
if (!app->installingUpdate && app->inputVersion.load() == (uint64_t)lp)
|
||||||
app->transform(0, (int)wp);
|
app->transform(0, (int)wp);
|
||||||
|
else
|
||||||
|
app->replayTyping();
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
case HistoryMessage:
|
case HistoryMessage:
|
||||||
if (app->historyWindow && IsWindowVisible(app->historyWindow))
|
if (app->historyWindow && IsWindowVisible(app->historyWindow))
|
||||||
|
|||||||
@@ -109,8 +109,10 @@ class App
|
|||||||
HotkeyHook hotkeyHook;
|
HotkeyHook hotkeyHook;
|
||||||
HWND typingTarget{};
|
HWND typingTarget{};
|
||||||
bool autoPending{};
|
bool autoPending{};
|
||||||
|
int snippetPending = -1;
|
||||||
|
bool snippetTyping{};
|
||||||
std::vector<INPUT> deferredTyping;
|
std::vector<INPUT> deferredTyping;
|
||||||
bool beginTypingDeferral(HWND, uint64_t);
|
bool beginTypingDeferral(HWND, uint64_t, bool snippet = false);
|
||||||
void replayTyping();
|
void replayTyping();
|
||||||
static LRESULT CALLBACK windowProc(HWND, UINT, WPARAM, LPARAM);
|
static LRESULT CALLBACK windowProc(HWND, UINT, WPARAM, LPARAM);
|
||||||
static LRESULT CALLBACK keyProc(int, WPARAM, LPARAM);
|
static LRESULT CALLBACK keyProc(int, WPARAM, LPARAM);
|
||||||
|
|||||||
+11
-1
@@ -261,7 +261,17 @@ Template parseTemplate(const std::wstring &text)
|
|||||||
size_t cursor = std::wstring::npos;
|
size_t cursor = std::wstring::npos;
|
||||||
for (size_t i = 0; i < text.size();)
|
for (size_t i = 0; i < text.size();)
|
||||||
{
|
{
|
||||||
if (text.compare(i, 10, L"{{cursor}}\0", 10) == 0)
|
if (text.compare(i, 9, L"{{enter}}") == 0)
|
||||||
|
{
|
||||||
|
t.text += L"{enter}";
|
||||||
|
i += 9;
|
||||||
|
}
|
||||||
|
else if (text.compare(i, 7, L"{enter}") == 0)
|
||||||
|
{
|
||||||
|
++t.enters;
|
||||||
|
i += 7;
|
||||||
|
}
|
||||||
|
else if (text.compare(i, 10, L"{{cursor}}\0", 10) == 0)
|
||||||
{
|
{
|
||||||
t.text += L"{cursor}";
|
t.text += L"{cursor}";
|
||||||
i += 10;
|
i += 10;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ struct Template
|
|||||||
{
|
{
|
||||||
std::wstring text, suffix;
|
std::wstring text, suffix;
|
||||||
int cursors = 0;
|
int cursors = 0;
|
||||||
|
int enters = 0;
|
||||||
};
|
};
|
||||||
Template parseTemplate(const std::wstring &);
|
Template parseTemplate(const std::wstring &);
|
||||||
struct Hotkey
|
struct Hotkey
|
||||||
|
|||||||
+75
-2
@@ -178,6 +178,11 @@ struct Capture
|
|||||||
COLORREF currentColor=RGB(225,42,50);
|
COLORREF currentColor=RGB(225,42,50);
|
||||||
COLORREF customColors[16]{};
|
COLORREF customColors[16]{};
|
||||||
Mark stroke{};
|
Mark stroke{};
|
||||||
|
bool strokeConstrained{};
|
||||||
|
size_t strokeAnchor{};
|
||||||
|
int strokeAxis{}; // 0 = not chosen, 1 = horizontal, 2 = vertical, 3/4 = diagonals.
|
||||||
|
bool strokeAxisSettled{};
|
||||||
|
std::vector<POINT> strokeMotion;
|
||||||
POINT start{}, end{};
|
POINT start{}, end{};
|
||||||
RECT selection{}, view{};
|
RECT selection{}, view{};
|
||||||
std::vector<Mark> marks;
|
std::vector<Mark> marks;
|
||||||
@@ -250,7 +255,7 @@ void updateStatus(Capture &c)
|
|||||||
else if(c.pointer) text=L"Pointer (V): select and edit objects | Drag handles = geometry or thickness | Empty space clears selection";
|
else if(c.pointer) text=L"Pointer (V): select and edit objects | Drag handles = geometry or thickness | Empty space clears selection";
|
||||||
else if(c.textTool) text=L"Text (T): click the image to type | Double-click existing text to edit";
|
else if(c.textTool) text=L"Text (T): click the image to type | Double-click existing text to edit";
|
||||||
else if(c.highlight) text=L"Highlight (H): drag to paint | Ctrl+drag = paint over an object | V = pointer";
|
else if(c.highlight) text=L"Highlight (H): drag to paint | Ctrl+drag = paint over an object | V = pointer";
|
||||||
else if(c.freehand) text=L"Freehand (F): drag to draw | Ctrl+drag = draw over an object | V = pointer";
|
else if(c.freehand) text=L"Freehand (F): Shift = 45-degree steps; move >50 px off line to turn | Ctrl = draw over objects";
|
||||||
else if(c.arrow) text=L"Drag to draw | Ctrl+drag over objects = linked connector | V = pointer";
|
else if(c.arrow) text=L"Drag to draw | Ctrl+drag over objects = linked connector | V = pointer";
|
||||||
else text=L"Number (N): click to add | V = select and edit | Ctrl+C = copy screenshot";
|
else text=L"Number (N): click to add | V = select and edit | Ctrl+C = copy screenshot";
|
||||||
if(text!=c.statusText) {
|
if(text!=c.statusText) {
|
||||||
@@ -841,7 +846,7 @@ void edit(HWND w, Capture &c)
|
|||||||
InitCommonControlsEx(&statusControls);
|
InitCommonControlsEx(&statusControls);
|
||||||
CreateWindowExW(0,STATUSCLASSNAMEW,L"",WS_CHILD|WS_VISIBLE|SBARS_TOOLTIPS,
|
CreateWindowExW(0,STATUSCLASSNAMEW,L"",WS_CHILD|WS_VISIBLE|SBARS_TOOLTIPS,
|
||||||
0,0,0,0,w,(HMENU)50,c.app.instance,nullptr);
|
0,0,0,0,w,(HMENU)50,c.app.instance,nullptr);
|
||||||
const wchar_t *labels[]{L"Pointer (V): select and edit objects", L"Number (N)", L"Arrow (A); Ctrl+drag between objects to link", L"Straight line (L); Ctrl+drag between objects to link", L"Rectangle (R)", L"Circle (O)", L"Ellipse (E)", L"Freehand line (F); Ctrl+drag to draw over an object", L"Highlight (H); Ctrl+drag to paint over an object",
|
const wchar_t *labels[]{L"Pointer (V): select and edit objects", L"Number (N)", L"Arrow (A); Ctrl+drag between objects to link", L"Straight line (L); Ctrl+drag between objects to link", L"Rectangle (R)", L"Circle (O)", L"Ellipse (E)", L"Freehand line (F); Shift for 45-degree steps; move >50 px off line to turn; Ctrl to draw over objects", L"Highlight (H); Ctrl+drag to paint over an object",
|
||||||
L"Text (T); double-click or F2 to edit", L"Color — selected object and new objects", L"Undo (Ctrl+Z)", L"Redo (Ctrl+Y / Ctrl+Shift+Z)", L"Copy (Ctrl+C)"};
|
L"Text (T); double-click or F2 to edit", L"Color — selected object and new objects", L"Undo (Ctrl+Z)", L"Redo (Ctrl+Y / Ctrl+Shift+Z)", L"Copy (Ctrl+C)"};
|
||||||
static_assert(std::size(labels)==std::size(ToolIds));
|
static_assert(std::size(labels)==std::size(ToolIds));
|
||||||
auto tips=CreateWindowExW(WS_EX_TOPMOST,TOOLTIPS_CLASSW,nullptr,WS_POPUP|TTS_ALWAYSTIP|TTS_NOPREFIX,
|
auto tips=CreateWindowExW(WS_EX_TOPMOST,TOOLTIPS_CLASSW,nullptr,WS_POPUP|TTS_ALWAYSTIP|TTS_NOPREFIX,
|
||||||
@@ -1045,6 +1050,13 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
|
|
||||||
SetFocus(w); captureCursor(w,*c); InvalidateRect(w,nullptr,FALSE); return 0;
|
SetFocus(w); captureCursor(w,*c); InvalidateRect(w,nullptr,FALSE); return 0;
|
||||||
case WM_KEYDOWN:
|
case WM_KEYDOWN:
|
||||||
|
if(wp==VK_SHIFT && c->editing && c->dragging && c->freehand && c->part==MarkPart::None && !c->strokeConstrained) {
|
||||||
|
c->strokeConstrained=true;
|
||||||
|
c->strokeAnchor=c->stroke.points.size()-1;
|
||||||
|
c->strokeAxis=0;
|
||||||
|
c->strokeAxisSettled=false;
|
||||||
|
c->strokeMotion={c->stroke.points.back()};
|
||||||
|
}
|
||||||
if (wp==VK_ESCAPE) {
|
if (wp==VK_ESCAPE) {
|
||||||
if(!c->editing) DestroyWindow(w); // Cancel region selection only.
|
if(!c->editing) DestroyWindow(w); // Cancel region selection only.
|
||||||
return 0;
|
return 0;
|
||||||
@@ -1100,6 +1112,9 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
case WM_KEYUP:
|
||||||
|
if(wp==VK_SHIFT) c->strokeConstrained=false;
|
||||||
|
return 0;
|
||||||
case WM_LBUTTONDBLCLK:
|
case WM_LBUTTONDBLCLK:
|
||||||
if(c->editing && !c->dragging) {
|
if(c->editing && !c->dragging) {
|
||||||
POINT p{GET_X_LPARAM(lp),GET_Y_LPARAM(lp)};
|
POINT p{GET_X_LPARAM(lp),GET_Y_LPARAM(lp)};
|
||||||
@@ -1156,6 +1171,7 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
c->stroke={p,p,0}; c->stroke.thickness=c->highlight?24:3; c->stroke.color=c->currentColor;
|
c->stroke={p,p,0}; c->stroke.thickness=c->highlight?24:3; c->stroke.color=c->currentColor;
|
||||||
c->stroke.freehand=c->freehand; c->stroke.style=c->lineStyle; c->stroke.ends=c->freehandEnds;
|
c->stroke.freehand=c->freehand; c->stroke.style=c->lineStyle; c->stroke.ends=c->freehandEnds;
|
||||||
c->stroke.points.push_back(p);
|
c->stroke.points.push_back(p);
|
||||||
|
c->strokeConstrained=false;
|
||||||
}
|
}
|
||||||
c->dragging=true; SetCapture(w);
|
c->dragging=true; SetCapture(w);
|
||||||
}
|
}
|
||||||
@@ -1193,6 +1209,63 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
if(c->editing && c->part==MarkPart::None && c->shape!=ScreenshotShape::None)
|
if(c->editing && c->part==MarkPart::None && c->shape!=ScreenshotShape::None)
|
||||||
c->end=constrainScreenshotShape(c->shape,c->start,c->end,c->image->width,c->image->height);
|
c->end=constrainScreenshotShape(c->shape,c->start,c->end,c->image->width,c->image->height);
|
||||||
if(c->editing && (c->highlight || c->freehand) && c->part==MarkPart::None) {
|
if(c->editing && (c->highlight || c->freehand) && c->part==MarkPart::None) {
|
||||||
|
bool constrained=c->freehand && ((wp&MK_SHIFT) || (GetKeyState(VK_SHIFT)&0x8000));
|
||||||
|
if(constrained) {
|
||||||
|
if(!c->strokeConstrained) {
|
||||||
|
c->strokeAnchor=c->stroke.points.size()-1;
|
||||||
|
c->strokeAxis=0;
|
||||||
|
c->strokeAxisSettled=false;
|
||||||
|
c->strokeMotion={c->stroke.points.back()};
|
||||||
|
}
|
||||||
|
auto anchor=c->stroke.points[c->strokeAnchor];
|
||||||
|
// Measure the actual mouse trajectory, not its snapped projection.
|
||||||
|
auto raw=c->end;
|
||||||
|
if(c->strokeMotion.back().x!=raw.x || c->strokeMotion.back().y!=raw.y)
|
||||||
|
c->strokeMotion.push_back(raw);
|
||||||
|
while(c->strokeMotion.size()>2 && (c->strokeMotion.size()>64 ||
|
||||||
|
std::hypot(double(raw.x-c->strokeMotion[1].x),double(raw.y-c->strokeMotion[1].y))>=16))
|
||||||
|
c->strokeMotion.erase(c->strokeMotion.begin());
|
||||||
|
auto chooseAxis=[](POINT from,POINT to) {
|
||||||
|
LONG dx=to.x-from.x,dy=to.y-from.y;
|
||||||
|
constexpr double diagonalBoundary=2.414213562373095; // tan(67.5 degrees)
|
||||||
|
if(std::abs(dx)>std::abs(dy)*diagonalBoundary) return 1;
|
||||||
|
if(std::abs(dy)>std::abs(dx)*diagonalBoundary) return 2;
|
||||||
|
return (dx<0)==(dy<0)?3:4;
|
||||||
|
};
|
||||||
|
auto distance=[](int axis,POINT from,POINT to) {
|
||||||
|
double dx=to.x-from.x,dy=to.y-from.y;
|
||||||
|
return axis==1?std::abs(dy):axis==2?std::abs(dx):
|
||||||
|
std::abs(axis==3?dy-dx:dy+dx)/std::sqrt(2.0);
|
||||||
|
};
|
||||||
|
if(!c->strokeAxisSettled && (c->end.x!=anchor.x || c->end.y!=anchor.y)) {
|
||||||
|
c->strokeAxis=chooseAxis(anchor,c->end);
|
||||||
|
// A one-pixel first movement must not lock a diagonal gesture to an axis.
|
||||||
|
c->strokeAxisSettled=std::hypot(double(raw.x-anchor.x),double(raw.y-anchor.y))>=8;
|
||||||
|
}
|
||||||
|
else if(c->strokeAxis && distance(c->strokeAxis,anchor,c->end)>50) {
|
||||||
|
// Keep the completed segment and turn at its endpoint.
|
||||||
|
auto corner=c->stroke.points.back();
|
||||||
|
int nextAxis=chooseAxis(c->strokeMotion.front(),raw);
|
||||||
|
if(nextAxis!=c->strokeAxis) {
|
||||||
|
c->strokeAnchor=c->stroke.points.size()-1;
|
||||||
|
anchor=corner;
|
||||||
|
c->strokeAxis=nextAxis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(c->strokeAxis==1) c->end.y=anchor.y;
|
||||||
|
else if(c->strokeAxis==2) c->end.x=anchor.x;
|
||||||
|
else if(c->strokeAxis>=3) {
|
||||||
|
int sign=c->strokeAxis==3?1:-1;
|
||||||
|
LONG delta=std::lround(((c->end.x-anchor.x)+sign*(c->end.y-anchor.y))/2.0);
|
||||||
|
LONG minY=sign==1?-anchor.y:anchor.y-(c->image->height-1);
|
||||||
|
LONG maxY=sign==1?c->image->height-1-anchor.y:anchor.y;
|
||||||
|
delta=std::clamp(delta,std::max(-anchor.x,minY),std::min(LONG(c->image->width-1)-anchor.x,maxY));
|
||||||
|
c->end={anchor.x+delta,anchor.y+sign*delta};
|
||||||
|
}
|
||||||
|
// Only the current segment is a preview; earlier corners stay fixed.
|
||||||
|
c->stroke.points.resize(c->strokeAnchor+1);
|
||||||
|
}
|
||||||
|
c->strokeConstrained=constrained;
|
||||||
auto last=c->stroke.points.back();
|
auto last=c->stroke.points.back();
|
||||||
if(last.x!=c->end.x || last.y!=c->end.y) c->stroke.points.push_back(c->end);
|
if(last.x!=c->end.x || last.y!=c->end.y) c->stroke.points.push_back(c->end);
|
||||||
c->stroke.to=c->end;
|
c->stroke.to=c->end;
|
||||||
|
|||||||
+16
-12
@@ -1375,6 +1375,14 @@ struct SnippetView
|
|||||||
std::vector<Snippet> snippets;
|
std::vector<Snippet> snippets;
|
||||||
int selection = -1;
|
int selection = -1;
|
||||||
};
|
};
|
||||||
|
void updateSnippetEditor(HWND w, SnippetView *v)
|
||||||
|
{
|
||||||
|
bool selected = v->selection >= 0 && v->selection < (int)v->snippets.size();
|
||||||
|
value(w, 302, selected ? v->snippets[v->selection].trigger : L"");
|
||||||
|
value(w, 303, selected ? v->snippets[v->selection].text : L"");
|
||||||
|
for (int id : {302, 303, 311, 312, 313})
|
||||||
|
EnableWindow(GetDlgItem(w, id), selected);
|
||||||
|
}
|
||||||
void fillSnippets(HWND w, SnippetView *v)
|
void fillSnippets(HWND w, SnippetView *v)
|
||||||
{
|
{
|
||||||
SendDlgItemMessageW(w, 301, LB_RESETCONTENT, 0, 0);
|
SendDlgItemMessageW(w, 301, LB_RESETCONTENT, 0, 0);
|
||||||
@@ -1382,6 +1390,7 @@ void fillSnippets(HWND w, SnippetView *v)
|
|||||||
SendDlgItemMessageW(w, 301, LB_ADDSTRING, 0, (LPARAM)sn.trigger.c_str());
|
SendDlgItemMessageW(w, 301, LB_ADDSTRING, 0, (LPARAM)sn.trigger.c_str());
|
||||||
if (v->selection >= 0)
|
if (v->selection >= 0)
|
||||||
SendDlgItemMessageW(w, 301, LB_SETCURSEL, v->selection, 0);
|
SendDlgItemMessageW(w, 301, LB_SETCURSEL, v->selection, 0);
|
||||||
|
updateSnippetEditor(w, v);
|
||||||
}
|
}
|
||||||
void collectSnippet(HWND w, SnippetView *v)
|
void collectSnippet(HWND w, SnippetView *v)
|
||||||
{
|
{
|
||||||
@@ -1418,11 +1427,7 @@ LRESULT CALLBACK snippetProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
{
|
{
|
||||||
collectSnippet(w, v);
|
collectSnippet(w, v);
|
||||||
v->selection = (int)SendDlgItemMessageW(w, 301, LB_GETCURSEL, 0, 0);
|
v->selection = (int)SendDlgItemMessageW(w, 301, LB_GETCURSEL, 0, 0);
|
||||||
if (v->selection >= 0)
|
updateSnippetEditor(w, v);
|
||||||
{
|
|
||||||
value(w, 302, v->snippets[v->selection].trigger);
|
|
||||||
value(w, 303, v->snippets[v->selection].text);
|
|
||||||
}
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (id == 310)
|
if (id == 310)
|
||||||
@@ -1431,8 +1436,6 @@ LRESULT CALLBACK snippetProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
v->snippets.push_back({L";new", L""});
|
v->snippets.push_back({L";new", L""});
|
||||||
v->selection = (int)v->snippets.size() - 1;
|
v->selection = (int)v->snippets.size() - 1;
|
||||||
fillSnippets(w, v);
|
fillSnippets(w, v);
|
||||||
value(w, 302, L";new");
|
|
||||||
value(w, 303, L"");
|
|
||||||
SetFocus(GetDlgItem(w, 302));
|
SetFocus(GetDlgItem(w, 302));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -1443,14 +1446,14 @@ LRESULT CALLBACK snippetProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
v->snippets.erase(v->snippets.begin() + v->selection);
|
v->snippets.erase(v->snippets.begin() + v->selection);
|
||||||
v->selection = -1;
|
v->selection = -1;
|
||||||
fillSnippets(w, v);
|
fillSnippets(w, v);
|
||||||
value(w, 302, L"");
|
SetFocus(GetDlgItem(w, 301));
|
||||||
value(w, 303, L"");
|
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (id == 312)
|
if (id == 312 || id == 313)
|
||||||
{
|
{
|
||||||
SendDlgItemMessageW(w, 303, EM_REPLACESEL, TRUE, (LPARAM)L"{cursor}");
|
if (v->selection < 0 || v->selection >= (int)v->snippets.size()) return 0;
|
||||||
|
SendDlgItemMessageW(w, 303, EM_REPLACESEL, TRUE, (LPARAM)(id == 312 ? L"{cursor}" : L"{enter}"));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (id == IDOK)
|
if (id == IDOK)
|
||||||
@@ -1900,8 +1903,9 @@ void showSnippets(App &app)
|
|||||||
edit(w, 302, 236, 44, 488);
|
edit(w, 302, 236, 44, 488);
|
||||||
label(w, L"Replacement text", 236, 82, 480);
|
label(w, L"Replacement text", 236, 82, 480);
|
||||||
edit(w, 303, 236, 110, 488, 270, ES_MULTILINE | ES_WANTRETURN | ES_AUTOVSCROLL | WS_VSCROLL);
|
edit(w, 303, 236, 110, 488, 270, ES_MULTILINE | ES_WANTRETURN | ES_AUTOVSCROLL | WS_VSCROLL);
|
||||||
label(w, L"Use {cursor} once; {{cursor}} inserts the literal marker.", 236, 392, 488);
|
control(w, L"STATIC", L"{cursor}: caret; {enter}: press Enter after insertion.\nUse {{cursor}} / {{enter}} for literal markers.", 0, 0, 236, 392, 488, 36);
|
||||||
button(w, L"Insert cursor marker", 312, 236, 430, 190);
|
button(w, L"Insert cursor marker", 312, 236, 430, 190);
|
||||||
|
button(w, L"Insert Enter marker", 313, 436, 430, 190);
|
||||||
button(w, L"Save", IDOK, 510, 478, 100);
|
button(w, L"Save", IDOK, 510, 478, 100);
|
||||||
button(w, L"Cancel", IDCANCEL, 624, 478, 100);
|
button(w, L"Cancel", IDCANCEL, 624, 478, 100);
|
||||||
fillSnippets(w, (SnippetView *)GetWindowLongPtrW(w, GWLP_USERDATA));
|
fillSnippets(w, (SnippetView *)GetWindowLongPtrW(w, GWLP_USERDATA));
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
namespace ks
|
namespace ks
|
||||||
{
|
{
|
||||||
inline constexpr wchar_t AppVersion[] = L"0.6.14";
|
inline constexpr wchar_t AppVersion[] = L"0.6.17";
|
||||||
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
||||||
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
||||||
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public static class ConfigurationTestUi {
|
|||||||
[DllImport("user32.dll", EntryPoint="SendMessageW", CharSet=CharSet.Unicode)] public static extern IntPtr GetText(IntPtr w,uint msg,IntPtr count,System.Text.StringBuilder text);
|
[DllImport("user32.dll", EntryPoint="SendMessageW", CharSet=CharSet.Unicode)] public static extern IntPtr GetText(IntPtr w,uint msg,IntPtr count,System.Text.StringBuilder text);
|
||||||
[DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern IntPtr FindWindow(string cls,string title);
|
[DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern IntPtr FindWindow(string cls,string title);
|
||||||
[DllImport("user32.dll")] public static extern IntPtr GetDlgItem(IntPtr w,int id);
|
[DllImport("user32.dll")] public static extern IntPtr GetDlgItem(IntPtr w,int id);
|
||||||
|
[DllImport("user32.dll")] public static extern bool IsWindowEnabled(IntPtr w);
|
||||||
[DllImport("user32.dll")] public static extern bool PostMessage(IntPtr w,uint msg,IntPtr wp,IntPtr lp);
|
[DllImport("user32.dll")] public static extern bool PostMessage(IntPtr w,uint msg,IntPtr wp,IntPtr lp);
|
||||||
[DllImport("user32.dll")] public static extern IntPtr SendMessage(IntPtr w,uint msg,IntPtr wp,IntPtr lp);
|
[DllImport("user32.dll")] public static extern IntPtr SendMessage(IntPtr w,uint msg,IntPtr wp,IntPtr lp);
|
||||||
[DllImport("user32.dll", EntryPoint="SendMessageW", CharSet=CharSet.Unicode)] public static extern IntPtr SetText(IntPtr w,uint msg,IntPtr wp,string text);
|
[DllImport("user32.dll", EntryPoint="SendMessageW", CharSet=CharSet.Unicode)] public static extern IntPtr SetText(IntPtr w,uint msg,IntPtr wp,string text);
|
||||||
@@ -127,6 +128,29 @@ try {
|
|||||||
Assert ((Get-Content -LiteralPath $settingsPath -Raw | ConvertFrom-Json).maxItems -eq 321) 'cancel preserves saved settings'
|
Assert ((Get-Content -LiteralPath $settingsPath -Raw | ConvertFrom-Json).maxItems -eq 321) 'cancel preserves saved settings'
|
||||||
$config=Launch 'KSwitcher.Config.exe' '--snippets'
|
$config=Launch 'KSwitcher.Config.exe' '--snippets'
|
||||||
$snippets=Window 'KSwitcher.Snippets'
|
$snippets=Window 'KSwitcher.Snippets'
|
||||||
|
$null=[ConfigurationTestUi]::SendMessage($snippets,0,[IntPtr]::Zero,[IntPtr]::Zero)
|
||||||
|
foreach($id in @(302,303,311,312,313)) {
|
||||||
|
Assert (![ConfigurationTestUi]::IsWindowEnabled([ConfigurationTestUi]::GetDlgItem($snippets,$id))) "snippet control $id disabled without selection"
|
||||||
|
}
|
||||||
|
$null=[ConfigurationTestUi]::SendMessage($snippets,0x111,[IntPtr]310,[IntPtr]::Zero)
|
||||||
|
foreach($id in @(302,303,311,312,313)) {
|
||||||
|
Assert ([ConfigurationTestUi]::IsWindowEnabled([ConfigurationTestUi]::GetDlgItem($snippets,$id))) 'Add enables snippet editing'
|
||||||
|
}
|
||||||
|
$null=[ConfigurationTestUi]::SetText([ConfigurationTestUi]::GetDlgItem($snippets,303),0x0c,[IntPtr]::Zero,'preserved snippet')
|
||||||
|
$null=[ConfigurationTestUi]::SendMessage($snippets,0x111,[IntPtr]310,[IntPtr]::Zero)
|
||||||
|
$null=[ConfigurationTestUi]::SendMessage($snippets,0x111,[IntPtr]311,[IntPtr]::Zero)
|
||||||
|
foreach($id in @(302,303,311,312,313)) {
|
||||||
|
Assert (![ConfigurationTestUi]::IsWindowEnabled([ConfigurationTestUi]::GetDlgItem($snippets,$id))) 'deleting selection disables snippet editing'
|
||||||
|
}
|
||||||
|
$null=[ConfigurationTestUi]::SendMessage($snippets,0x111,[IntPtr]312,[IntPtr]::Zero)
|
||||||
|
$snippetText=[Text.StringBuilder]::new(128)
|
||||||
|
$null=[ConfigurationTestUi]::GetText([ConfigurationTestUi]::GetDlgItem($snippets,303),0x0d,[IntPtr]128,$snippetText)
|
||||||
|
Assert ($snippetText.Length -eq 0) 'marker command cannot edit an unselected snippet'
|
||||||
|
$null=[ConfigurationTestUi]::SendMessage([ConfigurationTestUi]::GetDlgItem($snippets,301),0x186,[IntPtr]0,[IntPtr]::Zero)
|
||||||
|
$null=[ConfigurationTestUi]::SendMessage($snippets,0x111,[IntPtr](301 -bor (1 -shl 16)),[IntPtr]::Zero)
|
||||||
|
Assert ([ConfigurationTestUi]::IsWindowEnabled([ConfigurationTestUi]::GetDlgItem($snippets,303))) 'selecting existing snippet enables editing'
|
||||||
|
$null=[ConfigurationTestUi]::GetText([ConfigurationTestUi]::GetDlgItem($snippets,303),0x0d,[IntPtr]128,$snippetText)
|
||||||
|
Assert ($snippetText.ToString() -eq 'preserved snippet') 'selection restores the retained snippet content'
|
||||||
$second=Launch 'KSwitcher.Config.exe' ''
|
$second=Launch 'KSwitcher.Config.exe' ''
|
||||||
Assert ($second.WaitForExit(6000) -and $second.ExitCode -eq 0) 'settings and snippets share a single process'
|
Assert ($second.WaitForExit(6000) -and $second.ExitCode -eq 0) 'settings and snippets share a single process'
|
||||||
$settings=Window 'KSwitcher.Settings'
|
$settings=Window 'KSwitcher.Settings'
|
||||||
|
|||||||
+77
-2
@@ -21,6 +21,12 @@ ks::App *app{};
|
|||||||
HWND host{}, field{};
|
HWND host{}, field{};
|
||||||
ks::Clip saved;
|
ks::Clip saved;
|
||||||
bool captured = false, failed = false;
|
bool captured = false, failed = false;
|
||||||
|
int enterPresses = 0;
|
||||||
|
LRESULT CALLBACK observeEnter(HWND w, UINT message, WPARAM wp, LPARAM lp, UINT_PTR, DWORD_PTR)
|
||||||
|
{
|
||||||
|
if (message == WM_KEYDOWN && wp == VK_RETURN) ++enterPresses;
|
||||||
|
return DefSubclassProc(w, message, wp, lp);
|
||||||
|
}
|
||||||
int stage = 0, ru = -1, en = -1;
|
int stage = 0, ru = -1, en = -1;
|
||||||
size_t punctuationCase = 0;
|
size_t punctuationCase = 0;
|
||||||
bool typeDuringCorrection = false, typedDuringCorrection = false;
|
bool typeDuringCorrection = false, typedDuringCorrection = false;
|
||||||
@@ -281,7 +287,73 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
break;
|
break;
|
||||||
case 17:
|
case 17:
|
||||||
require(text() == punctuationCases.back().first, "undo preserves custom punctuation");
|
require(text() == punctuationCases.back().first, "undo preserves custom punctuation");
|
||||||
[[fallthrough]];
|
stage = 20;
|
||||||
|
break;
|
||||||
|
case 30:
|
||||||
|
case 32:
|
||||||
|
case 34:
|
||||||
|
if (!captured)
|
||||||
|
{
|
||||||
|
saved = ks::readClipboard(app->window, 64 * 1024 * 1024);
|
||||||
|
captured = true;
|
||||||
|
}
|
||||||
|
app->ownSequence = ks::writeClipboard(app->window, ks::textClip(sentinel));
|
||||||
|
app->settings.autoCorrect = stage == 35;
|
||||||
|
app->settings.autoLanguages = {L"en", L"ru"};
|
||||||
|
app->autoPredictor.configure(L"dictionaries", app->settings.autoLanguages);
|
||||||
|
app->settings.snippets = {{L";r", stage == 31 ? L"rider .{cursor}" : L"rider .{cursor}{enter}"}};
|
||||||
|
enterPresses = 0;
|
||||||
|
ActivateKeyboardLayout(app->layouts[en].handle, 0);
|
||||||
|
prepare(L"prefix ", false);
|
||||||
|
{
|
||||||
|
std::vector<INPUT> inputs;
|
||||||
|
const std::vector<WORD> keys = stage == 35
|
||||||
|
? std::vector<WORD>{VK_OEM_1, 'R', 'G', 'H', 'B', 'D', 'T', 'N', VK_SPACE}
|
||||||
|
: std::vector<WORD>{VK_OEM_1, 'R', VK_SPACE, 'X'};
|
||||||
|
for (WORD key : keys)
|
||||||
|
for (DWORD flags : {DWORD(0), DWORD(KEYEVENTF_KEYUP)})
|
||||||
|
{
|
||||||
|
auto input = ks::keyboardInput(key, flags);
|
||||||
|
input.ki.dwExtraInfo = 123;
|
||||||
|
inputs.push_back(input);
|
||||||
|
}
|
||||||
|
require(SendInput((UINT)inputs.size(), inputs.data(), sizeof(INPUT)) == inputs.size(),
|
||||||
|
"snippet trigger and following text submitted without a pause");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 31:
|
||||||
|
require(text() == L"prefix rider . x", "snippet expands through keyboard hook and preserves following typing");
|
||||||
|
require(ks::readClipboard(app->window).text() == sentinel, "hook snippet restores clipboard");
|
||||||
|
require(enterPresses == 0, "snippet without Enter marker sends no Enter");
|
||||||
|
stage = 32;
|
||||||
|
break;
|
||||||
|
case 33:
|
||||||
|
require(text() == L"prefix rider .\r\n x", "Enter action precedes buffered typing");
|
||||||
|
require(enterPresses == 1, "Enter marker sends an actual Enter key press");
|
||||||
|
require(ks::readClipboard(app->window).text() == sentinel, "Enter snippet restores clipboard");
|
||||||
|
stage = 34;
|
||||||
|
break;
|
||||||
|
case 35:
|
||||||
|
require(text() == L"prefix rider .\r\nghbdtn ", "snippet replay does not trigger automatic correction");
|
||||||
|
require(app->settings.autoCorrect, "snippet preserves global auto-correction setting");
|
||||||
|
require(enterPresses == 1, "snippet punctuation and Enter survive with auto-correction enabled");
|
||||||
|
{
|
||||||
|
std::vector<INPUT> inputs;
|
||||||
|
for (WORD key : std::wstring(L"GHBDTN "))
|
||||||
|
for (DWORD flags : {DWORD(0), DWORD(KEYEVENTF_KEYUP)})
|
||||||
|
{
|
||||||
|
auto input = ks::keyboardInput(key == L' ' ? VK_SPACE : key, flags);
|
||||||
|
input.ki.dwExtraInfo = 123;
|
||||||
|
inputs.push_back(input);
|
||||||
|
}
|
||||||
|
require(SendInput((UINT)inputs.size(), inputs.data(), sizeof(INPUT)) == inputs.size(),
|
||||||
|
"normal typing submitted after snippet completes");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 36:
|
||||||
|
require(text() == L"prefix rider .\r\nghbdtn привет ", "automatic correction resumes for subsequent normal typing");
|
||||||
|
finish();
|
||||||
|
break;
|
||||||
case 20:
|
case 20:
|
||||||
if (!captured)
|
if (!captured)
|
||||||
{
|
{
|
||||||
@@ -353,7 +425,7 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
}
|
}
|
||||||
case 22:
|
case 22:
|
||||||
require(text() == L"робить ", "automatic correction selects and converts the whole word across an internal comma");
|
require(text() == L"робить ", "automatic correction selects and converts the whole word across an internal comma");
|
||||||
finish();
|
stage = 30;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -477,8 +549,11 @@ int main(int argc, char **argv)
|
|||||||
CreateWindowW(L"EDIT", L"", WS_CHILD | WS_VISIBLE | WS_BORDER | ES_MULTILINE | ES_WANTRETURN,
|
CreateWindowW(L"EDIT", L"", WS_CHILD | WS_VISIBLE | WS_BORDER | ES_MULTILINE | ES_WANTRETURN,
|
||||||
20, 20, 590, 230, host, nullptr, cls.hInstance, nullptr);
|
20, 20, 590, 230, host, nullptr, cls.hInstance, nullptr);
|
||||||
deadline = GetTickCount64() + 30000;
|
deadline = GetTickCount64() + 30000;
|
||||||
|
SetWindowSubclass(field, observeEnter, 1, 0);
|
||||||
if (argc > 1 && std::string(argv[1]) == "--typing-only")
|
if (argc > 1 && std::string(argv[1]) == "--typing-only")
|
||||||
stage = 20;
|
stage = 20;
|
||||||
|
if (argc > 1 && std::string(argv[1]) == "--snippets-only")
|
||||||
|
stage = 30;
|
||||||
SetTimer(host, 1, 100, nullptr);
|
SetTimer(host, 1, 100, nullptr);
|
||||||
app->execute();
|
app->execute();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,105 @@ int main()
|
|||||||
require(c.pencilCursor!=nullptr,"pencil drawing cursor loads");
|
require(c.pencilCursor!=nullptr,"pencil drawing cursor loads");
|
||||||
require(c.freehand && !c.pointer && ks::toolCursor(c)==c.pencilCursor,
|
require(c.freehand && !c.pointer && ks::toolCursor(c)==c.pencilCursor,
|
||||||
"new screenshot editor starts with the pencil tool");
|
"new screenshot editor starts with the pencil tool");
|
||||||
|
{
|
||||||
|
BYTE savedKeys[256]{},keys[256]{}; GetKeyboardState(savedKeys);
|
||||||
|
std::copy(savedKeys,savedKeys+256,keys); keys[VK_SHIFT]=0; SetKeyboardState(keys);
|
||||||
|
auto mouse=[&](UINT message,POINT p,bool shift=false) {
|
||||||
|
int x=c.view.left+MulDiv(p.x,c.view.right-c.view.left,c.image->width);
|
||||||
|
int y=c.view.top+MulDiv(p.y,c.view.bottom-c.view.top,c.image->height);
|
||||||
|
SendMessageW(window,message,(message==WM_LBUTTONUP?0:MK_LBUTTON)|(shift?MK_SHIFT:0),MAKELPARAM(x,y));
|
||||||
|
};
|
||||||
|
mouse(WM_LBUTTONDOWN,{40,40},true);
|
||||||
|
mouse(WM_MOUSEMOVE,{100,50},true);
|
||||||
|
require(c.stroke.to.x==100 && c.stroke.to.y==40,"Shift constrains pencil horizontally");
|
||||||
|
mouse(WM_MOUSEMOVE,{100,90},true);
|
||||||
|
require(c.stroke.to.x==100 && c.stroke.to.y==40 && c.stroke.points.size()==2,
|
||||||
|
"50 pixel deviation keeps the current axis");
|
||||||
|
mouse(WM_MOUSEMOVE,{100,91},true);
|
||||||
|
require(c.stroke.to.x==100 && c.stroke.to.y==91 && c.stroke.points.size()==3 && c.stroke.points[1].y==40,
|
||||||
|
"51 pixel deviation turns vertically and preserves the horizontal segment");
|
||||||
|
mouse(WM_MOUSEMOVE,{70,140});
|
||||||
|
require(c.stroke.to.x==70 && c.stroke.to.y==140 && c.stroke.points.size()==4,
|
||||||
|
"releasing Shift resumes freehand in the same stroke");
|
||||||
|
SendMessageW(window,WM_KEYDOWN,VK_SHIFT,0);
|
||||||
|
mouse(WM_MOUSEMOVE,{140,150},true);
|
||||||
|
require(c.stroke.to.x==140 && c.stroke.to.y==140 && c.stroke.points[3].x==70,
|
||||||
|
"pressing Shift mid-stroke anchors at the last drawn point");
|
||||||
|
SendMessageW(window,WM_KEYUP,VK_SHIFT,0);
|
||||||
|
SendMessageW(window,WM_KEYDOWN,VK_SHIFT,0);
|
||||||
|
mouse(WM_MOUSEMOVE,{150,200},true);
|
||||||
|
mouse(WM_LBUTTONUP,{150,210},true);
|
||||||
|
SetKeyboardState(savedKeys);
|
||||||
|
require(c.marks.size()==1 && c.marks[0].to.x==140 && c.marks[0].to.y==210,
|
||||||
|
"repressing Shift without mouse movement starts a new segment and constrains mouse-up");
|
||||||
|
SendMessageW(window,WM_COMMAND,12,0);
|
||||||
|
require(c.marks.empty(),"mixed freehand and constrained segments undo as one stroke");
|
||||||
|
SendMessageW(window,WM_COMMAND,18,0);
|
||||||
|
require(c.marks.size()==1 && c.marks[0].points.size()==6,"redo preserves constrained stroke geometry");
|
||||||
|
c.marks.clear(); c.selected=-1;
|
||||||
|
for(int sx:{-1,1}) for(int sy:{-1,1}) {
|
||||||
|
mouse(WM_LBUTTONDOWN,{160,120},true);
|
||||||
|
mouse(WM_MOUSEMOVE,{160+sx,120},true);
|
||||||
|
for(int step=2;step<=90;++step)
|
||||||
|
mouse(WM_MOUSEMOVE,{160+sx*step,120+sy*step},true);
|
||||||
|
require(c.stroke.points.size()==2 && c.stroke.to.x==160+sx*90 && c.stroke.to.y==120+sy*90,
|
||||||
|
"smooth diagonal after one-pixel horizontal jitter stays diagonal without stairs");
|
||||||
|
mouse(WM_LBUTTONUP,{160+sx*90,120+sy*90},true);
|
||||||
|
c.marks.clear(); c.selected=-1;
|
||||||
|
mouse(WM_LBUTTONDOWN,{160-sx*40,120-sy*70},true);
|
||||||
|
mouse(WM_MOUSEMOVE,{160,120-sy*70},true);
|
||||||
|
for(int step=1;step<=90;++step)
|
||||||
|
mouse(WM_MOUSEMOVE,{160+sx*step,120-sy*70+sy*step},true);
|
||||||
|
require(c.stroke.points.size()==3 && c.strokeAxis>=3 &&
|
||||||
|
std::abs(c.stroke.to.x-c.stroke.points[1].x)==std::abs(c.stroke.to.y-c.stroke.points[1].y),
|
||||||
|
"smooth turn from horizontal into diagonal uses raw mouse direction without stairs");
|
||||||
|
mouse(WM_LBUTTONUP,{160+sx*90,120-sy*70+sy*90},true);
|
||||||
|
c.marks.clear(); c.selected=-1;
|
||||||
|
}
|
||||||
|
for(int sx:{-1,1}) for(int sy:{-1,1}) {
|
||||||
|
mouse(WM_LBUTTONDOWN,{160,120},true);
|
||||||
|
mouse(WM_MOUSEMOVE,{160+sx*60,120+sy*50},true);
|
||||||
|
require(c.stroke.to.x==160+sx*55 && c.stroke.to.y==120+sy*55,
|
||||||
|
"Shift snaps to exact 45 degree diagonals in all four directions");
|
||||||
|
mouse(WM_LBUTTONUP,{160+sx*60,120+sy*50},true);
|
||||||
|
c.marks.clear(); c.selected=-1;
|
||||||
|
}
|
||||||
|
mouse(WM_LBUTTONDOWN,{40,40},true);
|
||||||
|
mouse(WM_MOUSEMOVE,{100,40},true);
|
||||||
|
mouse(WM_MOUSEMOVE,{160,100},true);
|
||||||
|
require(c.stroke.points.size()==3 && c.stroke.to.x==160 && c.stroke.to.y==100 &&
|
||||||
|
c.stroke.points[1].x==100 && c.stroke.points[1].y==40,
|
||||||
|
"large deviation turns a horizontal segment diagonally without losing its corner");
|
||||||
|
mouse(WM_MOUSEMOVE,{200,100},true);
|
||||||
|
require(c.stroke.to.x==180 && c.stroke.to.y==120 && c.stroke.points.size()==3,
|
||||||
|
"small perpendicular deviation keeps the diagonal axis");
|
||||||
|
mouse(WM_MOUSEMOVE,{260,120},true);
|
||||||
|
require(c.stroke.to.x==260 && c.stroke.to.y==120 && c.stroke.points.size()==4,
|
||||||
|
"diagonal deviation beyond 50 pixels turns back to horizontal");
|
||||||
|
mouse(WM_LBUTTONUP,{260,120},true);
|
||||||
|
c.marks.clear(); c.selected=-1;
|
||||||
|
for(int sx:{-1,1}) for(int sy:{-1,1}) {
|
||||||
|
POINT start{sx>0?310L:9L,120};
|
||||||
|
mouse(WM_LBUTTONDOWN,start,true);
|
||||||
|
mouse(WM_MOUSEMOVE,{start.x+sx*8,start.y+sy*8},true);
|
||||||
|
mouse(WM_MOUSEMOVE,{sx>0?319L:0L,start.y+sy*25},true);
|
||||||
|
auto end=c.stroke.to;
|
||||||
|
require(end.x>=0 && end.x<320 && end.y>=0 && end.y<240 &&
|
||||||
|
std::abs(end.x-start.x)==std::abs(end.y-start.y),
|
||||||
|
"diagonal stays at 45 degrees and inside image boundaries");
|
||||||
|
mouse(WM_LBUTTONUP,{sx>0?319L:0L,start.y+sy*25},true);
|
||||||
|
c.marks.clear(); c.selected=-1;
|
||||||
|
}
|
||||||
|
keys[VK_CONTROL]=0x80; SetKeyboardState(keys);
|
||||||
|
mouse(WM_LBUTTONDOWN,{40,40});
|
||||||
|
mouse(WM_MOUSEMOVE,{100,70});
|
||||||
|
mouse(WM_LBUTTONUP,{120,90});
|
||||||
|
SetKeyboardState(savedKeys);
|
||||||
|
require(c.marks.size()==1 && c.marks[0].points[1].x==100 && c.marks[0].points[1].y==70 &&
|
||||||
|
c.marks[0].to.x==120 && c.marks[0].to.y==90,
|
||||||
|
"Ctrl keeps its existing drawing behavior without constraining the pencil");
|
||||||
|
c.marks.clear(); c.selected=-1;
|
||||||
|
}
|
||||||
for(int size:{32,48,64,96}) {
|
for(int size:{32,48,64,96}) {
|
||||||
auto cursor=(HCURSOR)LoadImageW(app.instance,MAKEINTRESOURCEW(IDC_SCREENSHOT_PENCIL),IMAGE_CURSOR,size,size,0);
|
auto cursor=(HCURSOR)LoadImageW(app.instance,MAKEINTRESOURCEW(IDC_SCREENSHOT_PENCIL),IMAGE_CURSOR,size,size,0);
|
||||||
ICONINFO info{};
|
ICONINFO info{};
|
||||||
|
|||||||
@@ -301,6 +301,12 @@ int main(int argc, char **argv)
|
|||||||
require(ks::transliterate(L"Shchuka") == L"Щука", "reverse transliteration");
|
require(ks::transliterate(L"Shchuka") == L"Щука", "reverse transliteration");
|
||||||
require(ks::parseTemplate(L"a{cursor}b").suffix == L"b", "snippet suffix");
|
require(ks::parseTemplate(L"a{cursor}b").suffix == L"b", "snippet suffix");
|
||||||
require(ks::parseTemplate(L"{{cursor}}").text == L"{cursor}", "escaped cursor");
|
require(ks::parseTemplate(L"{{cursor}}").text == L"{cursor}", "escaped cursor");
|
||||||
|
require(ks::parseTemplate(L"rider .{enter}").text == L"rider ." &&
|
||||||
|
ks::parseTemplate(L"rider .{enter}").enters == 1, "Enter is an action, not inserted text");
|
||||||
|
require(ks::parseTemplate(L"a{cursor}b{enter}{enter}").suffix == L"b" &&
|
||||||
|
ks::parseTemplate(L"a{cursor}b{enter}{enter}").enters == 2, "Enter actions preserve cursor suffix");
|
||||||
|
require(ks::parseTemplate(L"{{enter}}").text == L"{enter}" &&
|
||||||
|
ks::parseTemplate(L"{{enter}}").enters == 0, "escaped Enter is literal");
|
||||||
require(ks::parseTemplate(L"{cursor}{cursor}").cursors == 2, "duplicate cursor detection");
|
require(ks::parseTemplate(L"{cursor}{cursor}").cursors == 2, "duplicate cursor detection");
|
||||||
require(ks::Hotkey::parse(L"Ctrl+Alt+Shift+Space")->key == VK_SPACE, "hotkey parse");
|
require(ks::Hotkey::parse(L"Ctrl+Alt+Shift+Space")->key == VK_SPACE, "hotkey parse");
|
||||||
require(!ks::Hotkey::parse(L"Ctrl+F25"), "invalid function key");
|
require(!ks::Hotkey::parse(L"Ctrl+F25"), "invalid function key");
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ try {
|
|||||||
$binaryInfo = [Diagnostics.FileVersionInfo]::GetVersionInfo((Resolve-Path -LiteralPath $Executable).Path)
|
$binaryInfo = [Diagnostics.FileVersionInfo]::GetVersionInfo((Resolve-Path -LiteralPath $Executable).Path)
|
||||||
$binaryVersion = Get-StableVersion $binaryInfo.FileVersion
|
$binaryVersion = Get-StableVersion $binaryInfo.FileVersion
|
||||||
Assert ($null -ne $binaryVersion) 'executable has a stable version'
|
Assert ($null -ne $binaryVersion) 'executable has a stable version'
|
||||||
|
$header = Get-Content -LiteralPath (Join-Path $PSScriptRoot '../src/update.h') -Raw
|
||||||
|
$match = [regex]::Match($header, 'AppVersion\[\]\s*=\s*L"([0-9.]+)"')
|
||||||
|
Assert ($match.Success -and (Get-StableVersion $match.Groups[1].Value) -eq $binaryVersion) 'updater runtime version matches packaged executable'
|
||||||
|
$cmake = Get-Content -LiteralPath (Join-Path $PSScriptRoot '../CMakeLists.txt') -Raw
|
||||||
|
$projectVersion = [regex]::Match($cmake, 'project\(KSwitcher VERSION ([0-9.]+)')
|
||||||
|
Assert ($projectVersion.Success -and (Get-StableVersion $projectVersion.Groups[1].Value) -eq $binaryVersion) 'CMake version matches packaged executable'
|
||||||
Assert ($binaryInfo.FileDescription -ceq ('KSwitcher ' + [char]0x2014 + ' native keyboard and clipboard utility')) 'executable description preserves Unicode em dash'
|
Assert ($binaryInfo.FileDescription -ceq ('KSwitcher ' + [char]0x2014 + ' native keyboard and clipboard utility')) 'executable description preserves Unicode em dash'
|
||||||
Assert ((Get-StableVersion 'v0.10.0') -gt (Get-StableVersion 'v0.2.9')) 'numeric versions'
|
Assert ((Get-StableVersion 'v0.10.0') -gt (Get-StableVersion 'v0.2.9')) 'numeric versions'
|
||||||
Assert ($null -eq (Get-StableVersion 'v1.0.0-beta')) 'prerelease rejected'
|
Assert ($null -eq (Get-StableVersion 'v1.0.0-beta')) 'prerelease rejected'
|
||||||
@@ -24,6 +30,8 @@ try {
|
|||||||
Assert ($null -eq (Select-Release @($release) ([version]'0.3.0'))) 'no downgrade'
|
Assert ($null -eq (Select-Release @($release) ([version]'0.3.0'))) 'no downgrade'
|
||||||
Assert ((Get-StableVersion 'release-v0.5.0') -eq [version]'0.5.0') 'modern stable release tag'
|
Assert ((Get-StableVersion 'release-v0.5.0') -eq [version]'0.5.0') 'modern stable release tag'
|
||||||
$modern=@{draft=$false;prerelease=$false;tag_name='release-v0.5.0';assets=$assets}
|
$modern=@{draft=$false;prerelease=$false;tag_name='release-v0.5.0';assets=$assets}
|
||||||
|
$installedRelease=@{draft=$false;prerelease=$false;tag_name=('release-v'+$binaryInfo.FileVersion);assets=$assets}
|
||||||
|
Assert ($null -eq (Select-Release @($installedRelease) (Get-StableVersion $match.Groups[1].Value))) 'runtime updater does not offer the installed modern release again'
|
||||||
Assert ((Select-Release @($release,$modern) ([version]'0.4.2')).version -eq 'release-v0.5.0') 'bridge discovers modern channel'
|
Assert ((Select-Release @($release,$modern) ([version]'0.4.2')).version -eq 'release-v0.5.0') 'bridge discovers modern channel'
|
||||||
$draft=@{draft=$true;prerelease=$false;tag_name='v9.0.0';assets=$assets}
|
$draft=@{draft=$true;prerelease=$false;tag_name='v9.0.0';assets=$assets}
|
||||||
Assert ((Select-Release @($draft,$release) ([version]'0.2.0')).version -eq 'v0.2.1') 'draft ignored'
|
Assert ((Select-Release @($draft,$release) ([version]'0.2.0')).version -eq 'v0.2.1') 'draft ignored'
|
||||||
|
|||||||
Reference in New Issue
Block a user