Compare commits

..
Author SHA1 Message Date
agrynco 00cd1262b4 chore(release): package kswitcher v0.6.80 2026-11-14 16:10:08 +02:00
agrynco 7bc734a9a8 fix(files): distinguish lan peer lookup failures 2026-11-14 16:07:54 +02:00
agrynco a19ccaf035 Merge pull request 'Synchronize Files with PowerShell and fix the LAN drive picker' (#21) from codex/files-polish-release into main 2026-11-14 16:01:54 +02:00
agrynco 60625103f8 chore(release): package kswitcher v0.6.79 2026-11-14 15:53:27 +02:00
agrynco aa02f2cacb fix(files): synchronize terminal navigation and polish lan picker 2026-11-14 15:52:14 +02:00
agrynco 6a1e39efa9 Merge pull request 'Synchronize KSwitcher Files settings across open windows' (#20) from codex/files-live-settings into main 2026-11-14 15:18:15 +02:00
agrynco 7242c3f775 test(settings): keep picker fixture within the monitor 2026-11-14 15:17:54 +02:00
agrynco a43f6441b4 chore(release): refresh verified windows package 2026-11-14 15:15:20 +02:00
agrynco 2f5ae373a1 fix(settings): preserve unset files shortcut in options 2026-11-14 15:14:16 +02:00
agrynco b978361180 chore(release): package kswitcher v0.6.78 with files 2026-11-14 15:11:26 +02:00
agrynco 93b8055085 fix(files): synchronize settings across windows
Reload shared preferences in open windows and apply only fields changed in Options.
2026-11-14 15:09:17 +02:00
agrynco 70ee338fc3 Merge pull request 'Add searchable command history to KSwitcher Files' (#19) from codex/files-command-history into main 2026-11-14 14:43:10 +02:00
agrynco 46b4b51cea feat(files): add searchable command history
Persist command recall and align keyboard, mouse, and dialog behavior.

Refs #18
2026-11-14 14:42:48 +02:00
agrynco 721c3f0f38 docs: record KSwitcher issue workflow 2026-11-14 12:56:47 +02:00
agrynco 47851d75ef Merge pull request 'Add folder shortcuts to KSwitcher Files' (#17) from codex/issue-15-folder-shortcuts into main
Fixes #15
2026-11-14 12:20:53 +02:00
24 changed files with 1366 additions and 248 deletions

No files matched your search

+23
View File
@@ -40,8 +40,31 @@ Wiki: https://gitea.hrynco.com/hrynco/hrynco-kswitcher/wiki
Updating only the release notes or changing only the Wiki version number does
not satisfy this rule; the current functionality must also be reviewed.
## Gitea issue authentication
For Gitea issue API requests from this checkout, use the project token stored
with Windows DPAPI in `kswitcher-gitea-token.dpapi` under the directory returned
by `git rev-parse --absolute-git-dir`. Read it with PowerShell
`Get-Content -LiteralPath $tokenFile | ConvertTo-SecureString` and use it only
for the Gitea authorization header. Keep the token out of tracked files and
command output.
## KSwitcher Files UI
Before changing the file manager UI, read [FILES-UI.md](FILES-UI.md).
Reuse the shared dialog rendering and layout helpers. Update the document
when an agreed UI requirement changes.
## Link changes to Gitea issues
For commits that implement a Gitea issue, include `Refs #<number>` in the
commit message body. In a pull request, use `Fixes #<number>` when merging
fully resolves the issue, or `Refs #<number>` when the work is partial.
Use the existing issue for follow-up commits on the same change.
Assign each new issue to the [KSwitcher project board](https://gitea.hrynco.com/hrynco/hrynco-kswitcher/projects/8)
using its default column, and verify that the issue appears on the board.
Use Markdown headings, lists, and bold emphasis to make issue descriptions
easy to scan. Embed relevant screenshots as Markdown images using their
Gitea attachment URLs, and verify that the images render in the description.
+1 -1
View File
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.24)
project(KSwitcher VERSION 0.6.77 LANGUAGES CXX RC)
project(KSwitcher VERSION 0.6.80 LANGUAGES CXX RC)
if(NOT WIN32)
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
endif()
+68 -1
View File
@@ -4,9 +4,47 @@ Far Manager є зразком для оформлення файлового м
## Скорочення
- У командному рядку Ctrl+V і Shift+Insert вставляють текст із буфера обміну
в позицію курсора або замінюють виділення. Якщо фокус на файловій панелі,
ці клавіші переводять його до командного рядка й вставляють текст там.
Enter запускає команду. Escape очищає поле й залишає фокус у командному рядку.
Промпт і текст команди вирівняні по спільній базовій лінії.
- Ctrl+E підставляє останню команду та переводить фокус із файлової панелі
до командного рядка. Повторне натискання
підставляє старішу, доки не досягне початку історії. Alt+F8 відкриває історію
з групуванням за датами. Enter або подвійний клік у списку підставляє вибрану
команду в поле. Для запуску натисни Enter у самому полі.
- Історія зберігає 1000 останніх непорожніх команд, надісланих із поля KSwitcher
Files, включно з повторами. Вона зберігається локально в профілі Windows між
запусками. Options → Command line → Command history змінює обидві комбінації,
перевіряє конфлікти та дозволяє відновити типові значення.
- Ширина історії підлаштовується під найдовшу команду в межах робочої області
монітора. Рядок, який не вміщується, закінчується трьома крапками. Вибір
підставляє до командного рядка повний текст.
- Пошук в історії фільтрує команди за всіма введеними словами, незалежно від
регістру. Збіги за текстом або датою йдуть перед тематичними. Тематичні
ключові слова визначаються локально й використовуються лише для пошуку,
зокрема в раніше збережених записах. Кожна команда відображається один
раз, за датою останнього запуску.
- Історія використовує оформлення діалогів Delete і Copy та спільне поле Far.
Підпис, поле й список вирівнюються за відступами Dialog frame. Рядки команд
і роздільники дат мають власний внутрішній відступ.
Вікно історії бірюзове, поле пошуку сіро-бірюзове. Поки історія відкрита,
дії в панелях, меню й командному рядку заблоковано. Заголовок і рамка
головного вікна доступні для переміщення, зміни розміру та керування вікном.
Escape або клік поза діалогом закриває історію. Заголовок і рамка головного
вікна залишають історію відкритою під час керування вікном.
Історія центрується відносно головного вікна й рухається разом із ним.
Tab циклічно переводить фокус між пошуком і списком.
Стрілка вгору або вниз із пошуку одразу пересуває вибір у списку.
Висота списку залежить від кількості результатів. Коли місця бракує,
прокручується список.
- Короткий Alt перемикає головне меню Files при відпусканні. F9 також перемикає меню. Після закриття фокус повертається до активної панелі. Alt з літерою верхнього рівня спрацьовує одразу при натисканні літери. Alt+O відкриває Options, а відпускання Alt після комбінації не перемикає меню.
- Коли головне меню відкрите, L, F, C, O та R відкривають Left, Files, Commands, Options та Right. У підменю підсвічена літера викликає відповідну команду. Літери унікальні в межах поточного рівня. Усталені мнемоніки мають пріоритет, зокрема X для Exit.
- Глобальне скорочення **Open KSwitcher Files** можна змінити або очистити в налаштуваннях. Для профілю без цього поля пропонується Ctrl+Alt+F, якщо Windows реєструє комбінацію і вона вільна серед прив’язок KSwitcher.
- Глобальне скорочення повертає до відкритого вікна Files із тієї ж збірки або запускає Files, якщо такого вікна немає. Пункт **Files** у треї відкриває нове вікно.
- Ctrl+Shift+0–9 відкриває діалог Title і Path із поточною папкою. OK додає запис без дублювання, Escape скасовує. Title необов’язковий, порожнє значення показує шлях. Записи LAN зберігають ID ПК, а редагування Path залишає запис на тому самому ПК.
- Ctrl+0–9 одразу відкриває єдину папку. Для кількох папок відкривається компактний попап Far з одним заголовком Folder shortcuts у подвійній рамці та коротким рядом клавіш у нижній рамці. Ширина залежить від найдовшого запису й обмежується розміром вікна, а висота — кількістю видимих рядків. Нижня рамка показує Ins Del F4 Ctrl+Up Ctrl+Down. Insert додає поточну папку через Title і Path, Delete відв’язує, F4 редагує, Ctrl+Up і Ctrl+Down змінюють і зберігають порядок. Enter відкриває папку. Escape або клік поза попапом закриває його й повертає фокус до панелі. Commands → Folder shortcuts дає вибрати зайняту цифру й відкрити той самий попап навіть для єдиного запису. Brief і Wide доступні в меню Left/Right та Options.
@@ -28,6 +66,26 @@ Far Manager є зразком для оформлення файлового м
## Панелі
- У Change drive повідомлення No paired computers з’являється після
успішної відповіді з порожнім списком. Збої локального запиту до
KSwitcher відображаються окремо.
- Якщо команда у вбудованому PowerShell змінює поточну папку,
панель, з якої надіслано команду, переходить до цієї папки.
- Коли список LAN-дисків змінює розмір меню Change drive, рамка й список
змінюють позицію разом. Рамка повністю перемальовується після зміни
розміру, а список має однаковий внутрішній відступ зліва і справа.
- Тайм-аут запиту дисків відображається як Connection timed out. Якщо
ввімкнено Write log, Files записує помилки запитів пар і дисків в окремий
журнал `data/kswitcher-files.log`. Системні помилки містять код Windows.
- Застосовані налаштування Files одразу підхоплюють інші відкриті вікна.
Apply зберігає поля, змінені в поточній чернетці. Якщо два вікна одночасно
редагували набір стилів файлів, відкрий Options заново, щоб побачити свіжі правила.
- Типовий колір тексту для ZIP-архівів — світлий коралово-рожевий.
- Меню Change drive показує літеру, тип, загальний обсяг і вільне місце. Відкриття меню запускає нове LAN-виявлення. Секція KSwitcher LAN показує збережені пари, зокрема офлайн. Після кожного відкриття меню асинхронно запитує диски й показує їх під назвою відповідного ПК. Поки триває запит, видно стан завантаження. Якщо запит не вдався, видно помилку. Повторне відкриття запускає новий запит. Кожна панель має власний диск і шлях, а в заголовку віддаленої панелі перед шляхом показано назву ПК.
- Список дисків і файлові операції використовують права облікового запису Windows, під яким працює віддалений KSwitcher. Достатньо спарювання й доступного TCP 45885, спільні папки налаштовувати не потрібно.
@@ -38,12 +96,18 @@ Far Manager є зразком для оформлення файлового м
- У широкому режимі ширина під вирівняні розширення залежить від найдовшого розширення в поточній панелі. За вузької панелі залишай місце для імені та колонки Size.
- Бірюзовий курсор поточного рядка видно лише в активній панелі. Кожна панель зберігає свою позицію курсора.
- Бірюзовий курсор поточного рядка видно лише в активній панелі, зокрема коли клавіатурний фокус переходить у командний рядок. Кожна панель зберігає свою позицію курсора. Коли панелі видимі, F5 і F6 з командного рядка діють на активну панель.
- Клік у панелі нижче списку ставить курсор на останній рядок, а клік вище списку — на перший.
- Поточний шлях відображається по центру верхньої подвійної лінії панелі. Лінію видно з обох боків напису. Бірюзове тло активної панелі охоплює лише напис шляху з невеликими відступами. Довгий шлях скорочується в межах панелі.
## Спільні елементи діалогів
- Власні діалоги Files блокують дії панелей і командного рядка, залишаючи
системний заголовок і рамку головного вікна доступними. Клік поза діалогом
діє як Cancel. Для прогресу копіювання, переміщення й видалення це відкриває
підтвердження скасування.
- Enter активує кнопку у фокусі. Для кнопок із власним малюванням використовуй спільний обробник `dialogButtonEnter`. У підтвердженні скасування початковий фокус стоїть на No.
- При перенесенні вікна між моніторами з різним DPI оновлюй шрифт, висоту рядків панелей і компонування відкритого прогресу копіювання. Перевір перенесення під час активного копіювання.
@@ -73,6 +137,9 @@ Far Manager є зразком для оформлення файлового м
## Попередження про конфлікт
- Для однойменних файлів Warning показує точний розмір у байтах із розділенням тисяч і місцевий
час останньої зміни окремо для вхідного файла та файла в папці призначення.
- Після розрахунку остаточної висоти центр Warning збігається з центром вікна прогресу.
- Remember choice стоїть по вертикальному центру власної секції. Кнопки Overwrite, Skip і Cancel стоять по центру нижньої секції.
- Tab проходить Remember choice, Overwrite, Skip і Cancel. Стрілки вліво та вправо циклічно перемикають три кнопки в порядку їх розташування.
Binary file not shown.
+30
View File
@@ -1,5 +1,35 @@
# Latest Windows release
## v0.6.80 — 2026-11-14
Includes the Files updates from v0.6.79. Change drive now explains why the local peer lookup failed and shows No paired computers only when the response is empty.
**Українською.** Випуск включає зміни Files із v0.6.79. Change drive показує конкретну причину збою запиту пар. No paired computers з’являється лише після успішної порожньої відповіді.
## v0.6.79 — 2026-11-14
In KSwitcher Files, changing directory in PowerShell updates the panel
that sent the command. The global Files shortcut brings an open window to
the foreground. Change drive redraws its frame when LAN drives arrive and
shows a short timeout message. Files writes LAN lookup errors to its own log.
**Українською.** Зміна каталогу в PowerShell оновлює панель, з якої
надіслано команду. Глобальна клавіша Files повертає до відкритого вікна.
Change drive перемальовує рамку після появи LAN-дисків і показує коротке
повідомлення про тайм-аут. Files записує помилки LAN у власний журнал.
## v0.6.78 — 2026-11-14
The Windows package includes KSwitcher Files, a two-panel file manager with
local and paired LAN drives, folder shortcuts, an embedded PowerShell terminal,
and searchable command history. Applied Files settings update other open
instances immediately.
**Українською.** До пакета для Windows входить KSwitcher Files, двопанельний
файловий менеджер із локальними дисками й дисками спарених ПК, ярликами папок,
вбудованим PowerShell та пошуком в історії команд. Застосовані налаштування
Files одразу підхоплюють інші відкриті вікна.
## v0.6.77 — 2026-11-13
The screenshot editor remembers the local pencil smoothing default across
+1 -1
View File
@@ -1 +1 @@
51d95639527590b36453a5ef485532a7d2135950222fa197eb431a5309f1a45c KSwitcher-win-x64.zip
f6067958677c4045fc5243459fc37522489a7f081969dac42befd07101814e61 KSwitcher-win-x64.zip
+80 -35
View File
@@ -1,86 +1,131 @@
{
"version": "0.6.77",
"schema": 1,
"version": "0.6.80",
"files": [
{
"sha256": "654544aa5a7c944f607edf5324cff68d1f447b9219fb9b3933e90b0eb8a3ba13",
"path": "KSwitcher.exe",
"size": 2249216,
"sha256": "f1c79d337d746efcda03946cc833b7b5588ef4e739f1dff7d3afa15d0465469f"
"size": 2341376
},
{
"sha256": "a546c2dfd811ee8a3cf4301b5b21d1d6a72dd7280c3eac272eebd7963bc48b32",
"path": "KSwitcher.Config.exe",
"size": 2251776,
"sha256": "016edc389007e0d15ee365fff7115fc91a820b46080cb39a3fb362f3b463bae7"
"size": 2341888
},
{
"sha256": "a5cc8809b12698b266857440eece154a26bba64570bc65be1cb77959f7b0ebef",
"path": "KSwitcher.Screenshot.exe",
"size": 3247616,
"sha256": "c8ef070ad01acc410802c62e0df6a96cb299fe036fe8e99f0cf6a398b7324ec6"
"size": 3319808
},
{
"sha256": "d827cf29385db23dbdc5729d4397d768c32dd5ed7581ef10cb9f8c81370ec8a0",
"path": "KSwitcher.Files.exe",
"size": 1826304
},
{
"sha256": "d3fce90bdeecb08596e377dea6a316354dd8949e32ff09b89d51be00c14e21b6",
"path": "WebView2Loader.dll",
"size": 194912
},
{
"sha256": "e256f01188af527e4d06d21d06fbf785ae9c50d4b328bf03cbe0ba7f0aa4228f",
"path": "terminal/addon-fit-LICENSE.txt",
"size": 1103
},
{
"sha256": "ba3ea256ce0620a0992a197d6c9baea64823fc93d8da07a9e366ca9943c18527",
"path": "terminal/addon-fit.js",
"size": 1521
},
{
"sha256": "084d97a727b778fb10d31adcd557ee7840a38377ed60e0ef0f3ece93b349b1ef",
"path": "terminal/index.html",
"size": 527
},
{
"sha256": "f677e61d7d90d3023839ee131ddabed7a5c3510b18d47cc9a3c19353913ca5cc",
"path": "terminal/terminal.js",
"size": 3481
},
{
"sha256": "b569f629d00f2626a8100df2a1798210535621e42164dfd426a6fe5aac7b0ccd",
"path": "terminal/xterm-LICENSE.txt",
"size": 1261
},
{
"sha256": "854a7c0fb70e8b1a083c16797ab827299fb18744f5ad34f227b48337e33293c6",
"path": "terminal/xterm.css",
"size": 7112
},
{
"sha256": "14903579ff54664cd72f8e8699e6961a6272c21863ec1c3b118cdc8af5d4a972",
"path": "terminal/xterm.js",
"size": 488663
},
{
"sha256": "d3c81140cce01c26bf6cbf52aa40e0825e1fa35b9a91d1387170a65d2597082c",
"path": "LICENSE",
"size": 4401,
"sha256": "d3c81140cce01c26bf6cbf52aa40e0825e1fa35b9a91d1387170a65d2597082c"
"size": 4401
},
{
"sha256": "c0d068392ea65358b798b8c165103560f06e9e3b38c4ab4e2d8810a7b931af86",
"path": "json-LICENSE.txt",
"size": 1075,
"sha256": "c0d068392ea65358b798b8c165103560f06e9e3b38c4ab4e2d8810a7b931af86"
"size": 1075
},
{
"sha256": "df6d40d0eb66ad7da8db1cffe6bd211de70dfee474cc228e041c821b3940718b",
"path": "README.md",
"size": 93885,
"sha256": "df6d40d0eb66ad7da8db1cffe6bd211de70dfee474cc228e041c821b3940718b"
"size": 93885
},
{
"sha256": "2c13d3ed4761e679c352fb1ade24a1ec3060ad2862376ede801cdee5fcc3bada",
"path": "LAN-PREVIEW.md",
"size": 11418,
"sha256": "2c13d3ed4761e679c352fb1ade24a1ec3060ad2862376ede801cdee5fcc3bada"
"size": 11418
},
{
"sha256": "5f267c141c3d6837870e1c098f86b89a113ba300343119432644ead88f720c21",
"path": "dictionaries/en.txt",
"size": 682867,
"sha256": "5f267c141c3d6837870e1c098f86b89a113ba300343119432644ead88f720c21"
"size": 682867
},
{
"sha256": "28a9529c7d0bb4dc51f4bf5c116a3d16ef247a052f7591466768ddf563fd1cf5",
"path": "dictionaries/LICENSE-CC-BY-SA-4.0.txt",
"size": 20138,
"sha256": "28a9529c7d0bb4dc51f4bf5c116a3d16ef247a052f7591466768ddf563fd1cf5"
"size": 20138
},
{
"sha256": "3017bf44d3be9d9dfab21bd6778aa52b7947e61acf9ce2bc86b41d6e949ac311",
"path": "dictionaries/README.md",
"size": 2230,
"sha256": "2df17fe4e17d527c67ab57269b07dce59a49c1fb128c429a1408decbb47109ec"
"size": 2265
},
{
"sha256": "9ef922c7699569304a428f6aba0fd13a21341c0d0cb9561f37ef83c41577a28b",
"path": "dictionaries/ru.txt",
"size": 1060364,
"sha256": "9ef922c7699569304a428f6aba0fd13a21341c0d0cb9561f37ef83c41577a28b"
"size": 1060364
},
{
"sha256": "98e5b3fe7304e37705c9e5ff48b0b399ccb16e806736d21f09921b99e5f2a408",
"path": "dictionaries/sources.json",
"size": 1279,
"sha256": "d6e11a3aaba22a8a7eaba19f778d8a5bd66653ff306810e85a1b0e7fff3a6b86"
"size": 1280
},
{
"sha256": "d30e990d6ec2983c04f3644a3c61c6c668e8c542323f5931916fdd55b916eebf",
"path": "dictionaries/uk.txt",
"size": 898248,
"sha256": "caeb097749d4afc84b68ad96b3d7dfb6a6569561cd32ccce0bee94a13b43ba86"
"size": 940799
},
{
"sha256": "a34b5161a8315af037e1ec71d816bfc688f7c364141d4ee2347cf0515ef0c82a",
"path": "tools/README.txt",
"size": 993,
"sha256": "a34b5161a8315af037e1ec71d816bfc688f7c364141d4ee2347cf0515ef0c82a"
"size": 993
},
{
"sha256": "6ef5cf64708d62b7fc63c3c793cc99188a513433ff4774cae01dc014f6664376",
"path": "tools/StayHere.exe",
"size": 134656,
"sha256": "6ef5cf64708d62b7fc63c3c793cc99188a513433ff4774cae01dc014f6664376"
"size": 134656
},
{
"sha256": "ba214e2aca7405e698b3ab21ceb1f41ce088edd996b3e30e4a9d58ff496f9413",
"path": "tools/StayHere-README.md",
"size": 2796,
"sha256": "ba214e2aca7405e698b3ab21ceb1f41ce088edd996b3e30e4a9d58ff496f9413"
"size": 2796
}
],
"schema": 1
]
}
+3 -3
View File
@@ -9,8 +9,8 @@ IDC_SCREENSHOT_PENCIL CURSOR "icons/lucide/pencil.cur"
1 RT_MANIFEST "app.manifest"
201 RCDATA "update.ps1"
1 VERSIONINFO
FILEVERSION 0,6,77,0
PRODUCTVERSION 0,6,77,0
FILEVERSION 0,6,80,0
PRODUCTVERSION 0,6,80,0
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_APP
BEGIN
@@ -20,7 +20,7 @@ BEGIN
BEGIN
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
VALUE "ProductName", "KSwitcher"
VALUE "FileVersion", "0.6.77"
VALUE "FileVersion", "0.6.80"
END
END
BLOCK "VarFileInfo"
+31 -12
View File
@@ -39,6 +39,10 @@ BEGIN
COMBOBOX 305, 311, 51, 56, 125, CBS_DROPDOWNLIST | WS_TABSTOP | WS_VSCROLL
LTEXT "&Row text padding (characters)", 306, 131, 77, 173, 12
COMBOBOX 307, 311, 74, 56, 76, CBS_DROPDOWNLIST | WS_TABSTOP | WS_VSCROLL
LTEXT "Previous command", 320, 131, 30, 120, 12
CONTROL "", 321, "msctls_hotkey32", WS_TABSTOP | WS_BORDER, 260, 27, 110, 15
LTEXT "Open command history", 322, 131, 53, 120, 12
CONTROL "", 323, "msctls_hotkey32", WS_TABSTOP | WS_BORDER, 260, 50, 110, 15
PUSHBUTTON "Reset selected to defaults", 284, 131, 181, 125, 15
COMBOBOX 285, 131, 183, 139, 95, CBS_DROPDOWNLIST | WS_TABSTOP | WS_VSCROLL
PUSHBUTTON "Reset property", 286, 278, 183, 108, 15
@@ -62,18 +66,19 @@ BEGIN
PUSHBUTTON "&Apply", 283, 338, 289, 50, 14
END
102 DIALOGEX 0, 0, 350, 114
102 DIALOGEX 0, 0, 350, 145
STYLE DS_SETFONT | DS_CENTER | WS_POPUP
CAPTION "Copy"
FONT 10, "Consolas"
BEGIN
LTEXT "Copy to", 210, 14, 15, 322, 12, SS_PATHELLIPSIS
FAR_EDIT(211, ES_MULTILINE, 14, 29, 322, 18)
LTEXT "Existing files:", 212, 14, 61, 122, 14, SS_CENTERIMAGE
COMBOBOX 213, 140, 61, 196, 100, CBS_DROPDOWNLIST | CBS_OWNERDRAWFIXED | CBS_HASSTRINGS | WS_VSCROLL | WS_TABSTOP
CONTROL "Also ask on read-only files", 214, "Button", BS_OWNERDRAW | WS_TABSTOP, 14, 81, 322, 14
CONTROL "&Copy", IDOK, "Button", BS_OWNERDRAW | WS_TABSTOP, 87, 96, 86, 18
CONTROL "&Cancel", IDCANCEL, "Button", BS_OWNERDRAW | WS_TABSTOP, 177, 96, 86, 18
LTEXT "", 328, 14, 49, 322, 35, SS_NOPREFIX
LTEXT "Existing files:", 212, 14, 92, 122, 14, SS_CENTERIMAGE
COMBOBOX 213, 140, 92, 196, 100, CBS_DROPDOWNLIST | CBS_OWNERDRAWFIXED | CBS_HASSTRINGS | WS_VSCROLL | WS_TABSTOP
CONTROL "Also ask on read-only files", 214, "Button", BS_OWNERDRAW | WS_TABSTOP, 14, 112, 322, 14
CONTROL "&Copy", IDOK, "Button", BS_OWNERDRAW | WS_TABSTOP, 87, 127, 86, 18
CONTROL "&Cancel", IDCANCEL, "Button", BS_OWNERDRAW | WS_TABSTOP, 177, 127, 86, 18
END
103 DIALOGEX 0, 0, 460, 126
@@ -92,7 +97,7 @@ BEGIN
CONTROL "&Cancel", IDCANCEL, "Button", BS_OWNERDRAW | WS_TABSTOP, 187, 106, 86, 18
END
104 DIALOGEX 0, 0, 360, 106
104 DIALOGEX 0, 0, 360, 139
STYLE DS_SETFONT | DS_CENTER | WS_POPUP
CAPTION "Warning"
FONT 10, "Consolas"
@@ -100,11 +105,16 @@ BEGIN
LTEXT "", 230, 12, 16, 336, 12
LTEXT "", 231, 12, 33, 336, 12, SS_PATHELLIPSIS
LTEXT "", 233, 12, 55, 1, 1
LTEXT "", 234, 12, 81, 1, 1
CONTROL "Remember choice", 232, "Button", BS_OWNERDRAW | WS_TABSTOP, 12, 57, 336, 18
CONTROL "&Overwrite", IDYES, "Button", BS_OWNERDRAW | WS_TABSTOP, 42, 89, 110, 18
CONTROL "&Skip", IDNO, "Button", BS_OWNERDRAW | WS_TABSTOP, 158, 89, 68, 18
CONTROL "&Cancel", IDCANCEL, "Button", BS_OWNERDRAW | WS_TABSTOP, 232, 89, 86, 18
LTEXT "New", 329, 12, 58, 65, 13
RTEXT "", 330, 78, 58, 270, 13
LTEXT "Existing", 331, 12, 72, 65, 13
RTEXT "", 332, 78, 72, 270, 13
LTEXT "", 333, 12, 88, 1, 1
LTEXT "", 234, 12, 114, 1, 1
CONTROL "Remember choice", 232, "Button", BS_OWNERDRAW | WS_TABSTOP, 12, 90, 336, 18
CONTROL "&Overwrite", IDYES, "Button", BS_OWNERDRAW | WS_TABSTOP, 42, 122, 110, 18
CONTROL "&Skip", IDNO, "Button", BS_OWNERDRAW | WS_TABSTOP, 158, 122, 68, 18
CONTROL "&Cancel", IDCANCEL, "Button", BS_OWNERDRAW | WS_TABSTOP, 232, 122, 86, 18
END
105 DIALOGEX 0, 0, 310, 65
@@ -165,3 +175,12 @@ BEGIN
CONTROL "&OK", IDOK, "Button", BS_OWNERDRAW | WS_TABSTOP, 97, 86, 86, 18
CONTROL "&Cancel", IDCANCEL, "Button", BS_OWNERDRAW | WS_TABSTOP, 187, 86, 86, 18
END
111 DIALOGEX 0, 0, 385, 220
STYLE DS_SETFONT | DS_CENTER | WS_POPUP
FONT 9, "Consolas"
BEGIN
LTEXT "&Search commands or topics", 326, 12, 14, 361, 12
FAR_EDIT(325, ES_MULTILINE, 12, 28, 361, 16)
LISTBOX 324, 12, 49, 361, 155, LBS_NOTIFY | LBS_OWNERDRAWFIXED | LBS_HASSTRINGS | LBS_NOINTEGRALHEIGHT | WS_TABSTOP
END
+10
View File
@@ -7,6 +7,16 @@ const terminal = new Terminal({
theme: { background: '#181818', foreground: '#e8e8e8' },
});
const fit = new FitAddon.FitAddon();
terminal.parser.registerOscHandler(633, data => {
const prefix = 'ks-directory:';
if (!data.startsWith(prefix)) return false;
try {
bridge.postMessage('directory:' + decodeURIComponent(data.slice(prefix.length)));
} catch {
bridge.postMessage('directory:');
}
return true;
});
terminal.loadAddon(fit);
const host = document.getElementById('terminal');
terminal.open(host);
+23 -1
View File
@@ -143,6 +143,28 @@ ULONG_PTR snippetReplayMarker()
}
constexpr int OpenFiles = 25, FilesHotkeyFirst = 0x4B10, FilesHotkeySecond = 0x4B11;
bool activateFilesWindow()
{
HWND files{};
EnumWindows([](HWND candidate, LPARAM context) -> BOOL
{
wchar_t name[32]{};
if(!IsWindowVisible(candidate) || !GetClassNameW(candidate,name,std::size(name)) ||
wcscmp(name,L"KSwitcher.Files") || !configurationPeer(candidate,L"KSwitcher.Files.exe"))
return TRUE;
*reinterpret_cast<HWND *>(context)=candidate;
return FALSE;
},reinterpret_cast<LPARAM>(&files));
if(!files) return false;
if(IsIconic(files)) ShowWindowAsync(files,SW_RESTORE);
if(!SetForegroundWindow(files))
{
FLASHWINFO flash{sizeof(flash),files,FLASHW_TRAY,3,0};
FlashWindowEx(&flash);
}
return true;
}
std::vector<std::pair<int, std::wstring>> shortcuts(const Settings &s)
{
std::vector<std::pair<int, std::wstring>> out{{Toggle, s.toggle},
@@ -3037,7 +3059,7 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
case WM_HOTKEY:
if (app->filesHotkeyId && wp == static_cast<WPARAM>(app->filesHotkeyId))
{
app->dispatch(OpenFiles);
if(!activateFilesWindow()) app->dispatch(OpenFiles);
}
else if (wp != FilesHotkeyFirst && wp != FilesHotkeySecond) app->dispatch((int)wp);
return 0;
+107
View File
@@ -0,0 +1,107 @@
constexpr std::array<WORD,2> defaultHistoryKeys{MAKEWORD('E',HOTKEYF_CONTROL),MAKEWORD(VK_F8,HOTKEYF_ALT)};
auto commandHistoryKeys=defaultHistoryKeys;
constexpr size_t commandHistoryLimit=1000;
bool validHistoryKeys(const std::array<WORD,2> &keys)
{
if(keys[0]==keys[1]) return false;
return std::all_of(keys.begin(),keys.end(),[](WORD binding)
{
const auto key=LOBYTE(binding), modifiers=HIBYTE(binding);
const bool letter=key>='A' && key<='Z';
const bool function=key>=VK_F1 && key<=VK_F12;
if(!letter && !function) return false;
if(modifiers==HOTKEYF_CONTROL) return (letter && key!='C' && key!='V' && key!='X' && key!='A' && key!='Z' && key!='Y') || function;
if(modifiers==HOTKEYF_ALT) return function && key!=VK_F1 && key!=VK_F2 && key!=VK_F4 && key!=VK_F10;
return false;
});
}
std::array<WORD,2> optionHistoryKeys(HWND dialog)
{
return {static_cast<WORD>(SendDlgItemMessageW(dialog,321,HKM_GETHOTKEY,0,0)),
static_cast<WORD>(SendDlgItemMessageW(dialog,323,HKM_GETHOTKEY,0,0))};
}
void setOptionHistoryKeys(HWND dialog,const std::array<WORD,2> &keys)
{
SendDlgItemMessageW(dialog,321,HKM_SETHOTKEY,keys[0],0);
SendDlgItemMessageW(dialog,323,HKM_SETHOTKEY,keys[1],0);
}
void loadCommandHistoryKeys()
{
const std::array<WORD,2> keys{static_cast<WORD>(preference(L"PreviousCommandKey",defaultHistoryKeys[0])),
static_cast<WORD>(preference(L"CommandHistoryKey",defaultHistoryKeys[1]))};
commandHistoryKeys=validHistoryKeys(keys)?keys:defaultHistoryKeys;
}
void loadCommandHistory()
{
DWORD bytes{};
if(RegGetValueW(HKEY_CURRENT_USER,schemeKey,L"CommandHistory",RRF_RT_REG_BINARY,nullptr,nullptr,&bytes)
!=ERROR_SUCCESS || bytes>16*1024*1024 || bytes%sizeof(wchar_t)) return;
std::wstring data(bytes/sizeof(wchar_t),L'\0');
if(RegGetValueW(HKEY_CURRENT_USER,schemeKey,L"CommandHistory",RRF_RT_REG_BINARY,nullptr,data.data(),&bytes)
!=ERROR_SUCCESS) return;
size_t start{};
while(start<data.size())
{
const auto dateEnd=data.find(L'\0',start);
if(dateEnd==std::wstring::npos) break;
const auto commandEnd=data.find(L'\0',dateEnd+1);
if(commandEnd==std::wstring::npos) break;
terminalHistory.push_back({data.substr(start,dateEnd-start),data.substr(dateEnd+1,commandEnd-dateEnd-1)});
start=commandEnd+1;
}
terminalHistoryIndex=terminalHistory.size();
}
void rememberCommand(const std::wstring &command)
{
SYSTEMTIME now{};
GetLocalTime(&now);
wchar_t date[24]{};
GetDateFormatEx(LOCALE_NAME_USER_DEFAULT,0,&now,L"dd.MM.yyyy",date,std::size(date),nullptr);
terminalHistory.push_back({date,command});
if(terminalHistory.size()>commandHistoryLimit) terminalHistory.erase(terminalHistory.begin());
std::wstring data;
for(const auto &entry:terminalHistory)
{
data.append(entry.date).push_back(L'\0');
data.append(entry.command).push_back(L'\0');
}
if(RegSetKeyValueW(HKEY_CURRENT_USER,schemeKey,L"CommandHistory",REG_BINARY,data.data(),
static_cast<DWORD>(data.size()*sizeof(wchar_t)))!=ERROR_SUCCESS)
setStatus(L"Could not save command history.");
refreshCommandHistory();
}
void recallCommand(size_t index)
{
terminalHistoryIndex=index;
SetWindowTextW(terminalInput,index<terminalHistory.size()?terminalHistory[index].command.c_str():L"");
SetFocus(terminalInput);
SendMessageW(terminalInput,EM_SETSEL,-2,-1);
}
void showCommandHistory(HWND owner);
bool handleCommandHistoryKey(HWND window,const MSG &message)
{
if(message.message!=WM_KEYDOWN && message.message!=WM_SYSKEYDOWN) return false;
if((GetKeyState(VK_LWIN)&0x8000) || (GetKeyState(VK_RWIN)&0x8000)) return false;
const BYTE modifiers=((GetKeyState(VK_CONTROL)&0x8000)?HOTKEYF_CONTROL:0) |
((GetKeyState(VK_MENU)&0x8000)?HOTKEYF_ALT:0) | ((GetKeyState(VK_SHIFT)&0x8000)?HOTKEYF_SHIFT:0);
const auto binding=MAKEWORD(message.wParam,modifiers);
if(binding==commandHistoryKeys[0] && (message.hwnd==terminalInput ||
message.hwnd==panels[0].list || message.hwnd==panels[1].list))
{
if(terminalHistoryIndex) recallCommand(terminalHistoryIndex-1);
return true;
}
if(binding!=commandHistoryKeys[1]) return false;
if(!(message.lParam&(1LL<<30))) showCommandHistory(window);
return true;
}
+365
View File
@@ -0,0 +1,365 @@
constexpr int historyList=324;
constexpr int historySearch=325;
constexpr COLORREF historySearchSurface=RGB(132,177,177);
constexpr int historySearchLabel=326;
HWND commandHistoryWindow{};
HHOOK historyMouseHook{};
LRESULT CALLBACK dismissHistoryOnOutsideClick(int code,WPARAM message,LPARAM data)
{
const auto dialog=commandHistoryWindow;
if(code==HC_ACTION && dialog && IsWindowVisible(dialog) &&
(message==WM_LBUTTONDOWN || message==WM_RBUTTONDOWN ||
message==WM_MBUTTONDOWN || message==WM_XBUTTONDOWN))
{
const auto point=reinterpret_cast<const MSLLHOOKSTRUCT*>(data)->pt;
const auto clicked=WindowFromPoint(point);
const auto owner=GetWindow(dialog,GW_OWNER);
if(clicked!=dialog && !IsChild(dialog,clicked))
{
POINT local=point;
RECT client{};
GetClientRect(owner,&client);
ScreenToClient(owner,&local);
const bool ownerClient=clicked==owner || IsChild(owner,clicked);
if(!ownerClient || PtInRect(&client,local))
{
const auto focusPanel=clicked==panels[0].list?panels[0].list:
clicked==panels[1].list?panels[1].list:panels[active].list;
if(PostMessageW(dialog,WM_CLOSE,reinterpret_cast<WPARAM>(ownerClient?focusPanel:nullptr),0)
&& ownerClient) return 1;
}
}
}
return CallNextHookEx(nullptr,code,message,data);
}
std::wstring historyLower(std::wstring value)
{
if(!value.empty()) CharLowerBuffW(value.data(),static_cast<DWORD>(value.size()));
return value;
}
std::wstring_view commandTopicKeywords(const std::wstring &command)
{
const auto start=command.find_first_not_of(L" \t");
if(start==command.npos) return L"";
const auto end=command.find_first_of(L" \t",start);
auto tool=command.substr(start,end-start);
if(tool.ends_with(L".exe")) tool.resize(tool.size()-4);
const auto args=end==command.npos?L"":command.substr(end);
if(tool==L"ipconfig" && args.find(L"/flushdns")!=args.npos)
return L"network tools мережа dns flush clear cache очистити кеш";
if(tool==L"nslookup" || tool==L"resolve-dnsname")
return L"network tools мережа dns lookup запит";
if(tool==L"ping" || tool==L"tracert" || tool==L"ipconfig" || tool==L"test-netconnection" ||
tool==L"netstat" || tool==L"curl" || tool==L"invoke-webrequest")
return L"network tools мережа з’єднання connectivity address адреса";
if(tool==L"cls" || tool==L"clear" || tool==L"clear-host")
return L"terminal clear screen очистити екран консоль";
if(tool==L"git")
{
const auto first=args.find_first_not_of(L" \t");
const auto action=first==args.npos?L"":args.substr(first,args.find_first_of(L" \t",first)-first);
if(action==L"pull" || action==L"fetch" || action==L"update")
return L"git version control repository update fetch pull оновити репозиторій синхронізація";
return L"git version control repository репозиторій контроль версій";
}
return L"";
}
void populateCommandHistory(HWND dialog)
{
const auto search=GetDlgItem(dialog,historySearch);
std::wstring query(GetWindowTextLengthW(search)+1,L'\0');
query.resize(GetWindowTextW(search,query.data(),static_cast<int>(query.size())));
query=historyLower(std::move(query));
std::wistringstream stream(query);
std::vector<std::wstring> words;
for(std::wstring word;stream>>word;) words.push_back(word);
const auto list=GetDlgItem(dialog,historyList);
SendMessageW(list,WM_SETREDRAW,FALSE,0);
SendMessageW(list,LB_RESETCONTENT,0,0);
const auto add=[&](const std::wstring &label,LPARAM index)
{
const auto row=SendMessageW(list,LB_ADDSTRING,0,reinterpret_cast<LPARAM>(label.c_str()));
if(row>=0) SendMessageW(list,LB_SETITEMDATA,row,index);
return row;
};
std::array<std::vector<size_t>,2> matches;
std::set<std::wstring_view> seen;
for(size_t index=terminalHistory.size();index-- > 0;)
{
const auto &entry=terminalHistory[index];
if(!seen.insert(entry.command).second) continue;
const auto command=historyLower(entry.command);
const auto containsAll=[&](const std::wstring &text)
{
return std::all_of(words.begin(),words.end(),[&](const auto &word){return text.find(word)!=text.npos;});
};
if(containsAll(command+L" "+entry.date))
{
matches[0].push_back(index);
continue;
}
const auto keywords=commandTopicKeywords(command);
if(!keywords.empty() && containsAll(command+L" "+entry.date+L" "+std::wstring(keywords)))
matches[1].push_back(index);
}
LRESULT selected=LB_ERR;
for(const auto &group:matches)
{
std::wstring lastDate;
bool first=true;
for(auto match=group.rbegin();match!=group.rend();++match)
{
const auto index=*match;
const auto &entry=terminalHistory[index];
const auto &date=entry.date;
if(first || date!=lastDate) add(date,-1);
first=false;
lastDate=date;
const auto row=add(entry.command,index);
if(row>=0 && (words.empty() || selected==LB_ERR)) selected=row;
}
}
if(selected==LB_ERR) add(words.empty()?L"No commands yet":L"No matching commands",-2);
else SendMessageW(list,LB_SETCURSEL,selected,0);
SendMessageW(list,WM_SETREDRAW,TRUE,0);
InvalidateRect(list,nullptr,TRUE);
}
LRESULT CALLBACK historySearchKeys(HWND search,UINT message,WPARAM key,LPARAM flags,UINT_PTR,DWORD_PTR)
{
if(message==WM_GETDLGCODE)
{
const auto code=DefSubclassProc(search,message,key,flags);
if(key==VK_RETURN || key==VK_TAB || key==VK_ESCAPE) return code&~DLGC_WANTALLKEYS;
return code|DLGC_WANTARROWS;
}
if(message==WM_KEYDOWN && (key==VK_UP || key==VK_DOWN))
{
const auto list=GetDlgItem(GetParent(search),historyList);
SetFocus(list);
SendMessageW(list,message,key,flags);
return 0;
}
return DefSubclassProc(search,message,key,flags);
}
int historyRowInset(UINT dpi)
{
return std::max(MulDiv(4,dpi,96),dialogFrameMetrics(dpi).inner)+dialogRowTextInset(cellWidth);
}
LRESULT CALLBACK historyListScroll(HWND list,UINT message,WPARAM wp,LPARAM lp,UINT_PTR,DWORD_PTR)
{
if(message==LB_GETTOPINDEX || message==WM_NCDESTROY)
return DefSubclassProc(list,message,wp,lp);
const auto top=SendMessageW(list,LB_GETTOPINDEX,0,0);
const auto result=scrollList(list,message,wp,lp);
if(top!=SendMessageW(list,LB_GETTOPINDEX,0,0))
InvalidateRect(GetParent(list),nullptr,FALSE);
return result;
}
void sizeCommandHistory(HWND dialog,HWND list,bool movingOwner=false)
{
const auto owner=GetWindow(dialog,GW_OWNER);
RECT ownerBounds{};
if(!GetWindowRect(owner,&ownerBounds) || IsIconic(owner)) return;
MONITORINFO monitor{sizeof(monitor)};
if(!GetMonitorInfoW(MonitorFromWindow(owner,MONITOR_DEFAULTTONEAREST),&monitor)) return;
RECT area{};
if(!IntersectRect(&area,&ownerBounds,&monitor.rcWork)) area=monitor.rcWork;
const auto dc=GetDC(list);
if(!dc) return;
const auto previous=SelectObject(dc,mono);
LONG longest{};
for(const auto &entry:terminalHistory)
{
SIZE size{};
if(GetTextExtentPoint32W(dc,entry.command.c_str(),static_cast<int>(entry.command.size()),&size))
longest=std::max(longest,size.cx);
}
SelectObject(dc,previous);
ReleaseDC(list,dc);
const auto dpi=GetDpiForWindow(dialog);
const auto frame=dialogFrameMetrics(dpi);
const int inset=frame.content();
const int top=dialogListInset(frame,textHeight);
const int gap=MulDiv(4,dpi,96);
const int listTop=top+2*textHeight+2*gap;
const LONG desired=std::max<LONG>(MulDiv(320,dpi,96),
longest+2*cellWidth+2*historyRowInset(dpi)+GetSystemMetricsForDpi(SM_CXVSCROLL,dpi))+2*inset;
const auto target=std::min(desired,area.right-area.left);
const auto rows=std::max<LRESULT>(1,SendMessageW(list,LB_GETCOUNT,0,0));
RECT listWindow{},listClient{};
GetWindowRect(list,&listWindow);
GetClientRect(list,&listClient);
const auto listBorder=listWindow.bottom-listWindow.top-listClient.bottom;
const auto height=std::min<LONG>(listTop+rows*textHeight+listBorder+inset,
area.bottom-area.top);
const auto left=std::clamp((ownerBounds.left+ownerBounds.right-target)/2,area.left,area.right-target);
const auto y=std::clamp((ownerBounds.top+ownerBounds.bottom-height)/2,area.top,area.bottom-height);
RECT current{};
if(movingOwner && GetWindowRect(dialog,&current) &&
current.right-current.left==target && current.bottom-current.top==height)
{
if(current.left!=left || current.top!=y)
SetWindowPos(dialog,nullptr,left,y,0,0,SWP_NOSIZE|SWP_NOZORDER|SWP_NOACTIVATE);
return;
}
SetWindowPos(dialog,nullptr,left,y,target,height,SWP_NOZORDER|SWP_NOACTIVATE);
const auto place=[&](HWND control,int rowTop,int rowHeight)
{
SetWindowPos(control,nullptr,inset,rowTop,std::max(1L,target-2*inset),rowHeight,
SWP_NOZORDER|SWP_NOACTIVATE);
};
place(GetDlgItem(dialog,historySearchLabel),top,textHeight);
place(GetDlgItem(dialog,historySearch),top+textHeight+gap,textHeight);
SetWindowPos(list,nullptr,inset,listTop,std::max(1L,target-2*inset-5),
std::max(1L,height-listTop-inset),SWP_NOZORDER|SWP_NOACTIVATE);
GetClientRect(list,&listClient);
if(rows*textHeight<=listClient.bottom) SendMessageW(list,LB_SETTOPINDEX,0,0);
InvalidateRect(dialog,nullptr,FALSE);
}
void layoutOpenCommandHistory(bool movingOwner=false)
{
if(commandHistoryWindow)
sizeCommandHistory(commandHistoryWindow,GetDlgItem(commandHistoryWindow,historyList),movingOwner);
}
INT_PTR CALLBACK commandHistoryDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
{
if(message==WM_INITDIALOG)
{
prepareCopyDialog(dialog);
const auto list=GetDlgItem(dialog,historyList);
SetWindowSubclass(list,historyListScroll,1,0);
SendMessageW(list,WM_SETFONT,reinterpret_cast<WPARAM>(mono),TRUE);
SendMessageW(list,LB_SETITEMHEIGHT,0,std::max(1,textHeight));
const auto search=GetDlgItem(dialog,historySearch);
SendDlgItemMessageW(dialog,historySearchLabel,WM_SETFONT,reinterpret_cast<WPARAM>(mono),TRUE);
SetWindowSubclass(search,historySearchKeys,1,0);
populateCommandHistory(dialog);
sizeCommandHistory(dialog,list);
SetFocus(search);
return FALSE;
}
if(message==WM_COMMAND && LOWORD(wp)==historySearch && HIWORD(wp)==EN_CHANGE)
{
populateCommandHistory(dialog);
sizeCommandHistory(dialog,GetDlgItem(dialog,historyList));
return TRUE;
}
if(message==WM_NCDESTROY && commandHistoryWindow==dialog)
{
if(historyMouseHook) UnhookWindowsHookEx(historyMouseHook);
historyMouseHook=nullptr;
commandHistoryWindow=nullptr;
}
if(message==WM_CTLCOLORLISTBOX || message==WM_CTLCOLOREDIT)
{
const auto dc=reinterpret_cast<HDC>(wp);
const bool search=message==WM_CTLCOLOREDIT;
SetTextColor(dc,search?RGB(0,0,0):colors[Text]);
const auto background=search?historySearchSurface:colors[Dialog];
SetBkColor(dc,background);
if(search) SetDCBrushColor(dc,background);
return reinterpret_cast<INT_PTR>(search?GetStockObject(DC_BRUSH):teal);
}
if(message!=WM_DRAWITEM || wp!=historyList)
if(const auto painted=paintCopyDialog(dialog,message,wp,lp,L" History ",false,{},true,
GetDlgItem(dialog,historyList)))
return painted;
if(message==WM_DRAWITEM && wp==historyList)
{
const auto &item=*reinterpret_cast<DRAWITEMSTRUCT*>(lp);
if(item.itemID==UINT(-1)) return TRUE;
const auto count=SendMessageW(item.hwndItem,LB_GETTEXTLEN,item.itemID,0);
if(count<0) return TRUE;
std::wstring label(static_cast<size_t>(count)+1,L'\0');
SendMessageW(item.hwndItem,LB_GETTEXT,item.itemID,reinterpret_cast<LPARAM>(label.data()));
label.resize(count);
const bool selected=(item.itemState&ODS_SELECTED)!=0 && item.itemData<terminalHistory.size();
const auto background=colors[selected?Selection:Dialog];
const auto ink=colors[Text];
if(item.itemData==static_cast<ULONG_PTR>(-1))
{
auto bounds=item.rcItem;
InflateRect(&bounds,-historyRowInset(GetDpiForWindow(dialog)),0);
const auto saved=SaveDC(item.hDC);
SelectObject(item.hDC,mono);
SetBkColor(item.hDC,background);
SetTextColor(item.hDC,ink);
ExtTextOutW(item.hDC,0,0,ETO_OPAQUE,&bounds,nullptr,0,nullptr);
const auto middle=(bounds.top+bounds.bottom)/2;
RECT rule{bounds.left,middle,bounds.right,middle+1};
SetBkColor(item.hDC,ink);
ExtTextOutW(item.hDC,0,0,ETO_OPAQUE,&rule,nullptr,0,nullptr);
SIZE size{};
GetTextExtentPoint32W(item.hDC,label.c_str(),static_cast<int>(label.size()),&size);
const auto left=(bounds.left+bounds.right-size.cx)/2;
RECT caption{left-cellWidth,bounds.top,left+size.cx+cellWidth,bounds.bottom};
SetBkColor(item.hDC,background);
ExtTextOutW(item.hDC,left,middle-size.cy/2,ETO_OPAQUE,&caption,label.c_str(),
static_cast<UINT>(label.size()),nullptr);
RestoreDC(item.hDC,saved);
}
else paintDialogListRow(item.hDC,item.rcItem,label,mono,ink,background,
historyRowInset(GetDpiForWindow(dialog)),DT_END_ELLIPSIS);
if(selected && (item.itemState&ODS_FOCUS)) DrawFocusRect(item.hDC,&item.rcItem);
return TRUE;
}
if(message==WM_CLOSE || (message==WM_COMMAND && LOWORD(wp)==IDCANCEL))
{
const auto owner=GetWindow(dialog,GW_OWNER);
const auto focusPanel=message==WM_CLOSE?reinterpret_cast<HWND>(wp):panels[active].list;
DestroyWindow(dialog);
if(focusPanel && IsWindow(owner) && !IsIconic(owner))
{
SetActiveWindow(owner);
SetFocus(focusPanel);
}
return TRUE;
}
if(message==WM_COMMAND && (LOWORD(wp)==IDOK || (LOWORD(wp)==historyList && HIWORD(wp)==LBN_DBLCLK)))
{
const auto row=SendDlgItemMessageW(dialog,historyList,LB_GETCURSEL,0,0);
const auto index=SendDlgItemMessageW(dialog,historyList,LB_GETITEMDATA,row,0);
if(index>=0 && static_cast<size_t>(index)<terminalHistory.size())
{
const auto owner=GetWindow(dialog,GW_OWNER);
DestroyWindow(dialog);
SetActiveWindow(owner);
recallCommand(static_cast<size_t>(index));
}
return TRUE;
}
return FALSE;
}
void refreshCommandHistory()
{
if(commandHistoryWindow) populateCommandHistory(commandHistoryWindow);
layoutOpenCommandHistory();
}
void showCommandHistory(HWND owner)
{
if(!commandHistoryWindow)
commandHistoryWindow=CreateDialogParamW(GetModuleHandleW(nullptr),MAKEINTRESOURCEW(111),owner,commandHistoryDialog,0);
if(!commandHistoryWindow) return;
if(!historyMouseHook)
historyMouseHook=SetWindowsHookExW(WH_MOUSE_LL,dismissHistoryOnOutsideClick,GetModuleHandleW(nullptr),0);
ShowWindow(commandHistoryWindow,SW_SHOW);
SetActiveWindow(commandHistoryWindow);
SetFocus(GetDlgItem(commandHistoryWindow,historySearch));
}
+128
View File
@@ -0,0 +1,128 @@
HWND activeFilesDialog{};
HHOOK filesDialogMouseHook{};
bool filesMouseDown(UINT message)
{
return message==WM_LBUTTONDOWN || message==WM_RBUTTONDOWN ||
message==WM_MBUTTONDOWN || message==WM_XBUTTONDOWN;
}
LRESULT CALLBACK filesDialogMouse(int code,WPARAM message,LPARAM data)
{
const auto dialog=activeFilesDialog;
if(code==HC_ACTION && dialog && IsWindowVisible(dialog) && filesMouseDown(static_cast<UINT>(message)))
{
const auto point=reinterpret_cast<const MSLLHOOKSTRUCT*>(data)->pt;
const auto clicked=WindowFromPoint(point);
const auto root=GetAncestor(dialog,GA_ROOTOWNER);
if(clicked!=dialog && !IsChild(dialog,clicked))
{
RECT client{};
POINT local=point;
GetClientRect(root,&client);
ScreenToClient(root,&local);
if(clicked!=root || PtInRect(&client,local))
{
const bool internal=clicked==root || IsChild(root,clicked) ||
(clicked && GetAncestor(clicked,GA_ROOTOWNER)==root);
if(PostMessageW(dialog,WM_COMMAND,IDCANCEL,0) && internal) return 1;
}
}
}
return CallNextHookEx(nullptr,code,message,data);
}
struct FilesDialogInput
{
HWND previous=activeFilesDialog;
explicit FilesDialogInput(HWND dialog)
{
activeFilesDialog=dialog;
if(!filesDialogMouseHook)
filesDialogMouseHook=SetWindowsHookExW(WH_MOUSE_LL,filesDialogMouse,GetModuleHandleW(nullptr),0);
}
~FilesDialogInput()
{
activeFilesDialog=previous;
if(!previous && filesDialogMouseHook)
{
UnhookWindowsHookEx(filesDialogMouseHook);
filesDialogMouseHook=nullptr;
}
}
};
bool filesDialogHandlesMessage(HWND dialog,MSG &message)
{
if(message.hwnd==dialog || IsChild(dialog,message.hwnd)) return IsDialogMessageW(dialog,&message);
if(!message.hwnd || GetAncestor(message.hwnd,GA_ROOTOWNER)!=GetAncestor(dialog,GA_ROOTOWNER)) return false;
const bool input=(message.message>=WM_KEYFIRST && message.message<=WM_KEYLAST) ||
(message.message>=WM_MOUSEFIRST && message.message<=WM_MOUSELAST) ||
message.message==WM_CONTEXTMENU;
if(input && message.message==WM_KEYDOWN && message.wParam==VK_ESCAPE)
PostMessageW(dialog,WM_COMMAND,IDCANCEL,0);
return input;
}
struct FilesDialogResult
{
DLGPROC procedure;
LPARAM argument;
INT_PTR result=IDCANCEL;
bool finished=false;
};
constexpr wchar_t filesDialogResultProperty[]=L"KSwitcher.Files.DialogResult";
INT_PTR CALLBACK filesDialogProcedure(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
{
auto *result=reinterpret_cast<FilesDialogResult*>(GetPropW(dialog,filesDialogResultProperty));
if(message==WM_INITDIALOG)
{
result=reinterpret_cast<FilesDialogResult*>(lp);
if(!SetPropW(dialog,filesDialogResultProperty,result)) return FALSE;
lp=result->argument;
}
if(!result) return FALSE;
const auto handled=result->procedure(dialog,message,wp,lp);
if(message==WM_NCDESTROY) RemovePropW(dialog,filesDialogResultProperty);
return handled;
}
void finishFilesDialog(HWND dialog,INT_PTR answer)
{
auto *result=reinterpret_cast<FilesDialogResult*>(GetPropW(dialog,filesDialogResultProperty));
if(!result) return;
result->result=answer;
result->finished=true;
ShowWindow(dialog,SW_HIDE);
}
INT_PTR runFilesDialog(HINSTANCE instance,LPCWSTR resource,HWND owner,DLGPROC procedure,LPARAM argument)
{
FilesDialogResult result{procedure,argument};
const auto dialog=CreateDialogParamW(instance,resource,owner,filesDialogProcedure,
reinterpret_cast<LPARAM>(&result));
if(!dialog) return -1;
GetWindowRect(GetAncestor(owner,GA_ROOTOWNER),&previousOwnerBounds);
FilesDialogInput input(dialog);
ShowWindow(dialog,SW_SHOW);
SetActiveWindow(dialog);
MSG message{};
while(!result.finished && IsWindow(dialog))
{
const auto status=GetMessageW(&message,nullptr,0,0);
if(status<=0)
{
if(status==0) PostQuitMessage(static_cast<int>(message.wParam));
break;
}
if(filesDialogHandlesMessage(dialog,message)) continue;
TranslateMessage(&message);
DispatchMessageW(&message);
}
if(IsWindow(dialog)) DestroyWindow(dialog);
if(IsWindow(owner) && !IsIconic(GetAncestor(owner,GA_ROOTOWNER))) SetActiveWindow(owner);
return result.result;
}
+10 -32
View File
@@ -104,7 +104,7 @@ INT_PTR CALLBACK folderShortcutEditDialog(HWND dialog,UINT message,WPARAM wp,LPA
}
if(message==WM_CLOSE || (message==WM_COMMAND && LOWORD(wp)==IDCANCEL))
{
EndDialog(dialog,IDCANCEL);
finishFilesDialog(dialog,IDCANCEL);
return TRUE;
}
if(message!=WM_COMMAND || LOWORD(wp)!=IDOK) return FALSE;
@@ -139,13 +139,14 @@ INT_PTR CALLBACK folderShortcutEditDialog(HWND dialog,UINT message,WPARAM wp,LPA
return TRUE;
}
folderShortcuts=std::move(updated);
EndDialog(dialog,IDOK);
notifyFilesPreferences(GetAncestor(dialog,GA_ROOTOWNER));
finishFilesDialog(dialog,IDOK);
return TRUE;
}
bool editFolderShortcut(HWND owner,FolderShortcutEdit &edit)
{
return DialogBoxParamW(GetModuleHandleW(nullptr),MAKEINTRESOURCEW(110),owner,
return runFilesDialog(GetModuleHandleW(nullptr),MAKEINTRESOURCEW(110),owner,
folderShortcutEditDialog,reinterpret_cast<LPARAM>(&edit))==IDOK;
}
@@ -186,7 +187,6 @@ struct FolderShortcutView
std::vector<int> digits;
int rowHeight{};
Panel *panel{};
HHOOK mouseHook{};
};
void layoutQuickFolders(HWND dialog,FolderShortcutView &view,int selection)
@@ -275,26 +275,6 @@ LRESULT CALLBACK quickFolderKeys(HWND list,UINT message,WPARAM key,LPARAM flags,
return scrollList(list,message,key,flags);
}
HWND quickFolderWindow{};
bool dismissQuickFoldersAt(HWND dialog,POINT point)
{
RECT bounds{};
if(!dialog || !IsWindowVisible(dialog) || !IsWindowEnabled(dialog) ||
GetForegroundWindow()!=dialog || !GetWindowRect(dialog,&bounds) || PtInRect(&bounds,point))
return false;
return PostMessageW(dialog,WM_COMMAND,IDCANCEL,0)!=FALSE;
}
LRESULT CALLBACK quickFolderMouse(int code,WPARAM message,LPARAM data)
{
if(code==HC_ACTION && (message==WM_LBUTTONDOWN || message==WM_RBUTTONDOWN ||
message==WM_MBUTTONDOWN || message==WM_XBUTTONDOWN) &&
dismissQuickFoldersAt(quickFolderWindow,reinterpret_cast<const MSLLHOOKSTRUCT*>(data)->pt))
return 1;
return CallNextHookEx(nullptr,code,message,data);
}
INT_PTR CALLBACK quickFolderDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
{
if(message==WM_INITDIALOG)
@@ -305,8 +285,6 @@ INT_PTR CALLBACK quickFolderDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
layoutQuickFolders(dialog,view,0);
const auto list=GetDlgItem(dialog,shortcutList);
SetWindowSubclass(list,quickFolderKeys,1,0);
quickFolderWindow=dialog;
view.mouseHook=SetWindowsHookExW(WH_MOUSE_LL,quickFolderMouse,GetModuleHandleW(nullptr),0);
SetFocus(list);
return FALSE;
}
@@ -314,8 +292,6 @@ INT_PTR CALLBACK quickFolderDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
if(!view) return FALSE;
if(message==WM_DESTROY)
{
if(view->mouseHook) UnhookWindowsHookEx(view->mouseHook);
quickFolderWindow=nullptr;
return FALSE;
}
if(message==WM_PAINT)
@@ -347,7 +323,7 @@ INT_PTR CALLBACK quickFolderDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
}
if(message==WM_CLOSE || (message==WM_COMMAND && LOWORD(wp)==IDCANCEL))
{
EndDialog(dialog,IDCANCEL);
finishFilesDialog(dialog,IDCANCEL);
return TRUE;
}
if(message!=WM_COMMAND) return FALSE;
@@ -387,6 +363,7 @@ INT_PTR CALLBACK quickFolderDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
else
{
folderShortcuts=std::move(updated);
notifyFilesPreferences(GetAncestor(dialog,GA_ROOTOWNER));
layoutQuickFolders(dialog,*view,static_cast<int>(target));
}
return TRUE;
@@ -408,12 +385,13 @@ INT_PTR CALLBACK quickFolderDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
return TRUE;
}
folderShortcuts=std::move(updated);
if(folderShortcuts[view->digit].empty()) EndDialog(dialog,IDCANCEL);
notifyFilesPreferences(GetAncestor(dialog,GA_ROOTOWNER));
if(folderShortcuts[view->digit].empty()) finishFilesDialog(dialog,IDCANCEL);
else layoutQuickFolders(dialog,*view,static_cast<int>(row));
return TRUE;
}
view->chosen=group[row];
EndDialog(dialog,IDOK);
finishFilesDialog(dialog,IDOK);
return TRUE;
}
@@ -441,7 +419,7 @@ void showFolderShortcuts(Panel &panel,int digit)
}
FolderShortcutView view{digit};
view.panel=&panel;
const auto result=DialogBoxParamW(GetModuleHandleW(nullptr),MAKEINTRESOURCEW(109),GetParent(panel.list),
const auto result=runFilesDialog(GetModuleHandleW(nullptr),MAKEINTRESOURCEW(109),GetParent(panel.list),
quickFolderDialog,reinterpret_cast<LPARAM>(&view));
SetFocus(panel.list);
if(result==IDOK && view.chosen) openFolderShortcut(panel,*view.chosen);
+344 -107
View File
@@ -6,6 +6,7 @@
#include <commdlg.h>
#include <algorithm>
#include <array>
#include <deque>
#include <filesystem>
#include <fstream>
#include <future>
@@ -269,8 +270,11 @@ bool terminalViewRequested{};
HWND terminalInput{},terminalOutput{},terminalPrompt{};
constexpr UINT_PTR terminalTimer=2;
bool terminalVisible{};
std::deque<int> terminalCommandPanels;
std::wstring terminalDirectory;
int terminalRows{};
std::vector<std::wstring> terminalHistory;
struct CommandEntry { std::wstring date, command; };
std::vector<CommandEntry> terminalHistory;
size_t terminalHistoryIndex{};
constexpr UINT_PTR transferHighlightTimer=1;
unsigned transferHighlightPhase{};
@@ -295,6 +299,8 @@ std::vector<fs::path> drivePaths;
int driveHover=-1;
std::array<LONG,3> driveColumnStarts{};
HWND drivePeerService{};
bool drivePeerReplyReceived{};
std::wstring drivePeerLookupError;
std::vector<FilesPeer> drivePeers;
struct DriveQuery
{
@@ -363,12 +369,22 @@ DWORD preference(const wchar_t *name, DWORD fallback)
== ERROR_SUCCESS ? value : fallback;
}
const UINT filesPreferencesChanged=RegisterWindowMessageW(L"KSwitcher.Files.PreferencesChanged");
void notifyFilesPreferences(HWND source)
{
SendNotifyMessageW(HWND_BROADCAST,filesPreferencesChanged,reinterpret_cast<WPARAM>(source),0);
}
void savePreference(const wchar_t *name, DWORD value)
{
RegSetKeyValueW(HKEY_CURRENT_USER,schemeKey,name,REG_DWORD,&value,sizeof(value));
}
#include "files_styles.inl"
void setStatus(const std::wstring &message);
void refreshCommandHistory();
#include "files_command_history.inl"
constexpr const wchar_t *panelPathKeys[]{L"LeftPath",L"RightPath"};
constexpr const wchar_t *panelCursorKeys[]{L"LeftCursor",L"RightCursor"};
@@ -389,6 +405,7 @@ void setMode(int index, bool wide)
{
panels[index].wide=wide;
savePreference(index ? L"RightWide" : L"LeftWide",wide);
notifyFilesPreferences(GetParent(panels[index].list));
layout(GetParent(panels[index].list));
InvalidateRect(panels[index].list,nullptr,TRUE);
showFileDetails(panels[active]);
@@ -620,7 +637,7 @@ int dialogRowTextInset(int characterWidth,DialogFramePadding padding=dialogFrame
}
void paintDialogListRow(HDC dc,RECT bounds,std::wstring_view label,HFONT font,
COLORREF foreground,COLORREF background,int inset)
COLORREF foreground,COLORREF background,int inset,UINT ellipsis=DT_PATH_ELLIPSIS)
{
const auto saved=SaveDC(dc);
SetBkColor(dc,background);
@@ -631,7 +648,7 @@ void paintDialogListRow(HDC dc,RECT bounds,std::wstring_view label,HFONT font,
InflateRect(&bounds,-inset,0);
if(bounds.right>bounds.left)
DrawTextW(dc,label.data(),static_cast<int>(label.size()),&bounds,
DT_SINGLELINE|DT_VCENTER|DT_NOPREFIX|DT_PATH_ELLIPSIS);
DT_SINGLELINE|DT_VCENTER|DT_NOPREFIX|ellipsis);
if(saved) RestoreDC(dc,saved);
}
@@ -785,6 +802,8 @@ void showTerminal(HWND owner,bool show)
SetFocus(terminalInput);
}
void open(Panel &panel,const fs::path &directory,bool offerElevation);
bool startTerminal(HWND owner)
{
if(terminal.alive()) return true;
@@ -807,6 +826,20 @@ bool startTerminal(HWND owner)
traceTerminal(events[phase],height,rows,viewport,base,offset);
}
}
else if(message.starts_with(L"directory:"))
{
const auto directory=message.substr(10);
if(!terminalCommandPanels.empty())
{
const int panel=terminalCommandPanels.front();
terminalCommandPanels.pop_front();
std::error_code error;
if(panel>=0 && !panels[panel].peer && directory!=terminalDirectory &&
!directory.empty() && fs::is_directory(directory,error))
open(panels[panel],directory,false);
}
terminalDirectory=directory;
}
else if(message==L"focus-input")
{
if(GetForegroundWindow()==owner) SetFocus(terminalInput);
@@ -843,7 +876,12 @@ bool startTerminal(HWND owner)
setStatus(L"Select a local panel to start the terminal.");
return false;
}
if(terminal.start(panels[active].directory.c_str(),terminalSize)) return true;
if(terminal.start(panels[active].directory.c_str(),terminalSize))
{
terminalCommandPanels={-1};
terminalDirectory.clear();
return true;
}
setStatus(L"Could not start PowerShell.");
return false;
}
@@ -856,6 +894,17 @@ void updateTerminalOutput()
}
LRESULT CALLBACK terminalInputKeys(HWND input,UINT message,WPARAM wp,LPARAM lp,UINT_PTR,DWORD_PTR)
{
if(message==WM_CHAR && wp==VK_ESCAPE)
{
recallCommand(terminalHistory.size());
return 0;
}
if((message==WM_CHAR && wp==22) ||
(message==WM_KEYDOWN && wp==VK_INSERT && (GetKeyState(VK_SHIFT)&0x8000)))
{
SendMessageW(input,WM_PASTE,0,0);
return 0;
}
// The edit control receives Ctrl+Backspace as DEL.
if(message==WM_CHAR && wp==127)
{
@@ -884,11 +933,8 @@ LRESULT CALLBACK terminalInputKeys(HWND input,UINT message,WPARAM wp,LPARAM lp,U
std::wstring value(GetWindowTextLengthW(input)+1,L'\0');
value.resize(GetWindowTextW(input,value.data(),static_cast<int>(value.size())));
if(!terminal.write(value+L"\r")) { setStatus(L"Could not send PowerShell input."); return 0; }
if(!value.empty() && (terminalHistory.empty() || terminalHistory.back()!=value))
{
if(terminalHistory.size()==100) terminalHistory.erase(terminalHistory.begin());
terminalHistory.push_back(value);
}
terminalCommandPanels.push_back(active);
if(value.find_first_not_of(L" \t\r\n")!=std::wstring::npos) rememberCommand(value);
terminalHistoryIndex=terminalHistory.size();
SetWindowTextW(input,L"");
return 0;
@@ -914,11 +960,8 @@ LRESULT CALLBACK terminalInputKeys(HWND input,UINT message,WPARAM wp,LPARAM lp,U
}
if(message==WM_KEYDOWN && (wp==VK_UP || wp==VK_DOWN))
{
if(wp==VK_UP && terminalHistoryIndex) --terminalHistoryIndex;
if(wp==VK_DOWN && terminalHistoryIndex<terminalHistory.size()) ++terminalHistoryIndex;
SetWindowTextW(input,terminalHistoryIndex<terminalHistory.size()?
terminalHistory[terminalHistoryIndex].c_str():L"");
SendMessageW(input,EM_SETSEL,-2,-1);
if(wp==VK_UP && terminalHistoryIndex) recallCommand(terminalHistoryIndex-1);
if(wp==VK_DOWN && terminalHistoryIndex<terminalHistory.size()) recallCommand(terminalHistoryIndex+1);
return 0;
}
return DefSubclassProc(input,message,wp,lp);
@@ -1194,14 +1237,39 @@ LRESULT CALLBACK pickerKeys(HWND window, UINT msg, WPARAM wp, LPARAM lp, UINT_PT
void queryDrivePeers()
{
drivePeers.clear();
drivePeerLookupError.clear();
drivePeerReplyReceived=false;
drivePeerService=FindWindowW(L"KSwitcher.Main",L"KSwitcher");
if(drivePeerService)
{
COPYDATASTRUCT request{FilesLanPeers,0,nullptr};
DWORD_PTR result{};
SendMessageTimeoutW(drivePeerService,WM_COPYDATA,reinterpret_cast<WPARAM>(GetParent(picker)),
reinterpret_cast<LPARAM>(&request),SMTO_ABORTIFHUNG|SMTO_ERRORONEXIT,350,&result);
if(result!=1) drivePeers.clear();
SetLastError(ERROR_SUCCESS);
const auto delivered=SendMessageTimeoutW(drivePeerService,WM_COPYDATA,
reinterpret_cast<WPARAM>(GetParent(picker)),reinterpret_cast<LPARAM>(&request),
SMTO_ABORTIFHUNG|SMTO_ERRORONEXIT,350,&result);
const auto error=GetLastError();
if(!delivered || result!=1 || !drivePeerReplyReceived)
{
drivePeers.clear();
if(delivered && result!=1)
drivePeerLookupError=L"KSwitcher rejected the peer request";
else if(delivered)
drivePeerLookupError=L"Invalid peer list from KSwitcher";
else if(error==ERROR_ACCESS_DENIED)
drivePeerLookupError=L"Access denied by Windows";
else if(error==ERROR_TIMEOUT)
drivePeerLookupError=L"KSwitcher did not respond in time";
else
drivePeerLookupError=L"Peer request failed (Windows code "+std::to_wstring(error)+L")";
ks::log(L"LAN Files peer lookup failed: delivered="+std::to_wstring(delivered!=0)+
L", reply="+std::to_wstring(result)+L", Windows error="+std::to_wstring(error));
}
}
else
{
drivePeerLookupError=L"KSwitcher is not running";
ks::log(L"LAN Files peer lookup failed: local KSwitcher window not found");
}
drivePeerService=nullptr;
}
@@ -1242,8 +1310,17 @@ void layoutDrivePicker(int selected,bool focus)
static_cast<int>(SendMessageW(picker,LB_GETCOUNT,0,0)))*textHeight;
const int x=panel.left+(panel.right-panel.left-width)/2;
const int y=panel.top+(panel.bottom-panel.top-height)/2;
SetWindowPos(pickerFrame,HWND_TOP,x-inset,y-vertical,width+2*inset,height+2*vertical,SWP_SHOWWINDOW|SWP_NOACTIVATE);
SetWindowPos(picker,HWND_TOP,x,y,width,height,SWP_SHOWWINDOW|SWP_NOACTIVATE);
constexpr UINT flags=SWP_SHOWWINDOW|SWP_NOACTIVATE|SWP_NOCOPYBITS;
auto layout=BeginDeferWindowPos(2);
if(layout) layout=DeferWindowPos(layout,pickerFrame,HWND_TOP,
x-inset,y-vertical,width+2*inset,height+2*vertical,flags);
if(layout) layout=DeferWindowPos(layout,picker,HWND_TOP,x,y,width,height,flags);
if(!layout || !EndDeferWindowPos(layout))
{
SetWindowPos(pickerFrame,HWND_TOP,x-inset,y-vertical,width+2*inset,height+2*vertical,flags);
SetWindowPos(picker,HWND_TOP,x,y,width,height,flags);
}
RedrawWindow(pickerFrame,nullptr,nullptr,RDW_INVALIDATE|RDW_UPDATENOW);
SendMessageW(picker,LB_SETCURSEL,selected,0);
if(focus) SetFocus(picker);
}
@@ -1265,7 +1342,8 @@ void showRemoteDrives(bool focus=false,int initialRow=0)
return SendMessageW(picker,LB_ADDSTRING,0,reinterpret_cast<LPARAM>(label.c_str()));
};
add(L"KSwitcher LAN");
if(driveQueries.empty()) add(L" No paired computers");
if(!drivePeerLookupError.empty()) add(L" "+drivePeerLookupError);
else if(driveQueries.empty()) add(L" No paired computers");
for(const auto &query:driveQueries)
{
add(query.peer.name);
@@ -1331,10 +1409,15 @@ void pollDriveQueries(HWND owner)
try {query.drives=query.pending.get();}
catch(const std::system_error &e)
{
query.error=query.peer.filesBrowse?fileError(e):L"Check build / TCP 45885";
ks::log(L"LAN Files drive query failed: Windows error "+std::to_wstring(e.code().value()));
if(e.code()==std::error_code(WSAETIMEDOUT,std::system_category()))
query.error=L"Connection timed out";
else
query.error=query.peer.filesBrowse?fileError(e):L"Check build / TCP 45885";
}
catch(const std::exception &)
{
ks::log(L"LAN Files drive query failed: unexpected error");
query.error=query.peer.filesBrowse?L"Cannot reach TCP 45885":L"Check build / TCP 45885";
}
changed=true;
@@ -1451,6 +1534,9 @@ bool directoryExists(const FilesPeer *peer,const fs::path &path)
}
HBRUSH warningBrush{};
RECT previousOwnerBounds{};
#include "files_dialog_input.inl"
LRESULT CALLBACK dialogButtonEnter(HWND button,UINT message,WPARAM wp,LPARAM lp,UINT_PTR,DWORD_PTR);
@@ -1473,15 +1559,8 @@ bool isDialogEdit(HWND control)
return lstrcmpiW(name,L"Edit")==0;
}
void prepareDialogEdit(HWND field)
void centerDialogEditText(HWND field)
{
SendMessageW(field,WM_SETFONT,reinterpret_cast<WPARAM>(mono),FALSE);
const auto margin=MulDiv(2,GetDpiForWindow(field),96);
SendMessageW(field,EM_SETMARGINS,EC_LEFTMARGIN|EC_RIGHTMARGIN,MAKELPARAM(margin,margin));
if(!(GetWindowLongPtrW(field,GWL_STYLE)&ES_MULTILINE)) return;
// A single-line Edit centers its own text. Copy uses a multiline Edit for
// its formatting rectangle and keeps its existing dialog key handling.
RECT edit{};
GetClientRect(field,&edit);
if(const auto dc=GetDC(field))
@@ -1496,6 +1575,25 @@ void prepareDialogEdit(HWND field)
SendMessageW(field,EM_SETRECTNP,0,reinterpret_cast<LPARAM>(&edit));
}
LRESULT CALLBACK dialogEditLayout(HWND field,UINT message,WPARAM wp,LPARAM lp,UINT_PTR,DWORD_PTR)
{
const auto result=DefSubclassProc(field,message,wp,lp);
if(message==WM_SIZE || message==WM_SETFONT) centerDialogEditText(field);
return result;
}
void prepareDialogEdit(HWND field)
{
SendMessageW(field,WM_SETFONT,reinterpret_cast<WPARAM>(mono),FALSE);
const auto margin=MulDiv(2,GetDpiForWindow(field),96);
SendMessageW(field,EM_SETMARGINS,EC_LEFTMARGIN|EC_RIGHTMARGIN,MAKELPARAM(margin,margin));
if(GetWindowLongPtrW(field,GWL_STYLE)&ES_MULTILINE)
{
SetWindowSubclass(field,dialogEditLayout,1,0);
centerDialogEditText(field);
}
}
void prepareCopyDialog(HWND dialog)
{
installDialogButtonKeys(dialog);
@@ -1554,10 +1652,10 @@ LONG dialogDividerY(HWND dialog,int id)
if(id==214)
{
const auto combo=dialogControlBounds(dialog,213);
const auto name=dialogControlBounds(dialog,211);
return bounds.bottom+std::max(0L,(combo.top-name.bottom-1)/2);
const auto validation=dialogControlBounds(dialog,328);
return bounds.bottom+std::max(0L,(combo.top-validation.bottom-1)/2);
}
if(id==213) return (dialogControlBounds(dialog,211).bottom+bounds.top-1)/2;
if(id==213) return (dialogControlBounds(dialog,328).bottom+bounds.top-1)/2;
return bounds.top-4;
}
@@ -1619,11 +1717,12 @@ void paintDialogButton(const DRAWITEMSTRUCT &item,COLORREF surface,COLORREF fore
}
INT_PTR paintCopyDialog(HWND dialog, UINT message, WPARAM wp, LPARAM lp,
const wchar_t *caption, bool warning, std::initializer_list<int> dividers)
const wchar_t *caption, bool warning, std::initializer_list<int> dividers, bool useDialogPalette=false,
HWND scrollListControl=nullptr)
{
const auto foreground=warning?RGB(255,255,255):RGB(0,0,0);
const auto surface=warning?RGB(128,0,0):RGB(192,192,192);
const auto brush=warning?warningBrush:static_cast<HBRUSH>(GetStockObject(LTGRAY_BRUSH));
const auto foreground=useDialogPalette?colors[Text]:warning?RGB(255,255,255):RGB(0,0,0);
const auto surface=useDialogPalette?colors[Dialog]:warning?RGB(128,0,0):RGB(192,192,192);
const auto brush=useDialogPalette?teal:warning?warningBrush:static_cast<HBRUSH>(GetStockObject(LTGRAY_BRUSH));
if(message==WM_CTLCOLORDLG) return reinterpret_cast<INT_PTR>(brush);
if(message==WM_CTLCOLORSTATIC || message==WM_CTLCOLOREDIT || message==WM_CTLCOLORBTN)
{
@@ -1641,7 +1740,8 @@ INT_PTR paintCopyDialog(HWND dialog, UINT message, WPARAM wp, LPARAM lp,
RECT bounds{};
GetClientRect(dialog,&bounds);
const auto client=bounds;
const auto black=static_cast<HBRUSH>(GetStockObject(warning?WHITE_BRUSH:BLACK_BRUSH));
const auto black=static_cast<HBRUSH>(GetStockObject(useDialogPalette?DC_BRUSH:warning?WHITE_BRUSH:BLACK_BRUSH));
if(useDialogPalette) SetDCBrushColor(dc,foreground);
bounds=paintDialogFrame(dc,client,dialogFrameMetrics(GetDpiForWindow(dialog)),brush,black);
for(const auto control:dividers)
{
@@ -1650,6 +1750,7 @@ INT_PTR paintCopyDialog(HWND dialog, UINT message, WPARAM wp, LPARAM lp,
FillRect(dc,&divider,black);
}
paintDialogFrameCaptions(dc,client,bounds,caption,brush,foreground,mono);
if(scrollListControl) paintScrollPosition(dialog,dc,scrollListControl,surface);
EndPaint(dialog,&paint);
return TRUE;
}
@@ -1744,6 +1845,7 @@ INT_PTR CALLBACK copyDialog(HWND dialog, UINT message, WPARAM wp, LPARAM lp)
SendDlgItemMessageW(dialog,pathField,EM_SETREADONLY,!single && !request.move,0);
if(request.move) SetDlgItemTextW(dialog,IDOK,L"&Rename");
layoutDialogButtons(dialog,214,{IDOK,IDCANCEL});
SendDlgItemMessageW(dialog,328,WM_SETFONT,reinterpret_cast<WPARAM>(mono),FALSE);
SendDlgItemMessageW(dialog,summary,WM_SETFONT,reinterpret_cast<WPARAM>(mono),FALSE);
SendDlgItemMessageW(dialog,conflicts,WM_SETFONT,reinterpret_cast<WPARAM>(mono),FALSE);
for(const auto control:{213,214})
@@ -1765,7 +1867,7 @@ INT_PTR CALLBACK copyDialog(HWND dialog, UINT message, WPARAM wp, LPARAM lp)
}
if(message==WM_CLOSE || (message==WM_COMMAND && LOWORD(wp)==IDCANCEL))
{
EndDialog(dialog,IDCANCEL);
finishFilesDialog(dialog,IDCANCEL);
return TRUE;
}
if(message!=WM_COMMAND) return FALSE;
@@ -1781,7 +1883,17 @@ INT_PTR CALLBACK copyDialog(HWND dialog, UINT message, WPARAM wp, LPARAM lp)
InvalidateRect(GetDlgItem(dialog,213),nullptr,FALSE);
return FALSE;
}
if(LOWORD(wp)==pathField && HIWORD(wp)==EN_CHANGE)
{
SetDlgItemTextW(dialog,328,L"");
return TRUE;
}
if(LOWORD(wp)!=IDOK) return FALSE;
const auto showError=[&](const wchar_t *message)
{
SetDlgItemTextW(dialog,328,message);
SetFocus(GetDlgItem(dialog,pathField));
};
try
{
if(request.move)
@@ -1801,9 +1913,7 @@ INT_PTR CALLBACK copyDialog(HWND dialog, UINT message, WPARAM wp, LPARAM lp)
(!directoryExists(peer,target) &&
(request.sources.size()!=1 || !target.has_filename() || !directoryExists(peer,target.parent_path()))))
{
MessageBoxW(dialog,L"Enter an existing destination folder, or a new name for one item.",
L"Rename/Move",MB_OK|MB_ICONWARNING);
SetFocus(field);
showError(L"Enter an existing destination folder, or a new name for one item.");
return TRUE;
}
request.destinationPeer=peer;
@@ -1829,8 +1939,7 @@ INT_PTR CALLBACK copyDialog(HWND dialog, UINT message, WPARAM wp, LPARAM lp)
name.find_first_of(L"<>:\"/\\|?*")!=name.npos ||
std::any_of(name.begin(),name.end(),[](wchar_t ch){return ch<32;}))
{
MessageBoxW(dialog,L"Enter a valid name for the copy.",L"Copy",MB_OK|MB_ICONWARNING);
SetFocus(field);
showError(L"Enter a valid name for the copy.");
return TRUE;
}
if(CompareStringOrdinal(name.c_str(),-1,request.sources.front().filename().c_str(),-1,TRUE)!=CSTR_EQUAL)
@@ -1843,7 +1952,7 @@ INT_PTR CALLBACK copyDialog(HWND dialog, UINT message, WPARAM wp, LPARAM lp)
const auto peer=request.move || request.newName.empty()?request.destinationPeer:request.sourcePeer;
if(!directoryExists(peer,destination))
{
MessageBoxW(dialog,L"The destination folder is unavailable.",L"Copy",MB_OK|MB_ICONWARNING);
showError(L"The destination folder is unavailable.");
return TRUE;
}
const bool sameHost=(!peer && !request.sourcePeer) ||
@@ -1853,22 +1962,21 @@ INT_PTR CALLBACK copyDialog(HWND dialog, UINT message, WPARAM wp, LPARAM lp)
fs::equivalent(destination,sourceDirectory,error));
if(request.newName.empty() && sameFolder)
{
MessageBoxW(dialog,L"Open a different folder in the other panel, or enter a new name for a single item.",
request.move?L"Rename/Move":L"Copy",MB_OK|MB_ICONWARNING);
showError(L"Open a different folder in the other panel, or enter a new name for a single item.");
return TRUE;
}
const auto mode=SendDlgItemMessageW(dialog,213,CB_GETCURSEL,0,0);
if(mode!=CB_ERR) request.conflictMode=static_cast<ConflictMode>(
request.move && mode==static_cast<int>(ConflictMode::append)?static_cast<int>(ConflictMode::newer):mode);
EndDialog(dialog,IDOK);
finishFilesDialog(dialog,IDOK);
}
catch(const std::system_error &e)
{
MessageBoxW(dialog,fileError(e).c_str(),L"Destination unavailable",MB_OK|MB_ICONWARNING);
showError(fileError(e).c_str());
}
catch(const std::exception &)
{
MessageBoxW(dialog,L"The remote destination is unavailable.",L"Copy",MB_OK|MB_ICONWARNING);
showError(L"The remote destination is unavailable.");
}
return TRUE;
}
@@ -1890,7 +1998,6 @@ struct CopyProgress
};
bool copyInProgress{};
CopyProgress *activeCopy{};
RECT previousOwnerBounds{};
struct OwnedWindowMove
{
@@ -2010,7 +2117,7 @@ INT_PTR CALLBACK cancelOperationDialog(HWND window,UINT message,WPARAM wp,LPARAM
if(message==WM_CLOSE || (message==WM_COMMAND &&
(LOWORD(wp)==IDYES || LOWORD(wp)==IDNO || LOWORD(wp)==IDCANCEL)))
{
EndDialog(window,message==WM_COMMAND && LOWORD(wp)==IDYES?IDYES:IDNO);
finishFilesDialog(window,message==WM_COMMAND && LOWORD(wp)==IDYES?IDYES:IDNO);
return TRUE;
}
return FALSE;
@@ -2020,7 +2127,7 @@ bool confirmOperationCancellation(HWND owner,const wchar_t *question)
{
const auto previous=warningBrush;
if(!previous) warningBrush=CreateSolidBrush(RGB(128,0,0));
const auto answer=DialogBoxParamW(GetModuleHandleW(nullptr),MAKEINTRESOURCEW(105),owner,
const auto answer=runFilesDialog(GetModuleHandleW(nullptr),MAKEINTRESOURCEW(105),owner,
cancelOperationDialog,reinterpret_cast<LPARAM>(question));
if(!previous)
{
@@ -2134,9 +2241,28 @@ struct WarningPrompt
const wchar_t *details;
const wchar_t *accept;
const wchar_t *decline;
FileInfo incoming,existing;
bool remember{};
};
std::wstring conflictFileDetails(const FileInfo &file)
{
const FILETIME stamp{static_cast<DWORD>(file.modified),static_cast<DWORD>(file.modified>>32)};
SYSTEMTIME utc{},local{};
wchar_t date[32]=L"—";
if(file.modified && FileTimeToSystemTime(&stamp,&utc) && SystemTimeToTzSpecificLocalTime(nullptr,&utc,&local))
swprintf_s(date,L"%04u-%02u-%02u %02u:%02u:%02u",unsigned(local.wYear),
unsigned(local.wMonth),unsigned(local.wDay),unsigned(local.wHour),
unsigned(local.wMinute),unsigned(local.wSecond));
auto bytes=std::to_wstring(file.size);
for(auto position=bytes.size();position>3;)
{
position-=3;
bytes.insert(position,1,L'\u202F');
}
return bytes+L" B "+date;
}
LRESULT CALLBACK warningButtonKeys(HWND button,UINT message,WPARAM wp,LPARAM lp,
UINT_PTR,DWORD_PTR index)
{
@@ -2173,7 +2299,7 @@ INT_PTR CALLBACK warningDialog(HWND window,UINT message,WPARAM wp,LPARAM lp)
if(item.itemState&ODS_FOCUS) DrawFocusRect(item.hDC,&item.rcItem);
return TRUE;
}
if(const auto painted=paintCopyDialog(window,message,wp,lp,L" Warning ",true,{233,234}))
if(const auto painted=paintCopyDialog(window,message,wp,lp,L" Warning ",true,{233,333,234}))
return painted;
if(message==WM_INITDIALOG)
{
@@ -2183,11 +2309,13 @@ INT_PTR CALLBACK warningDialog(HWND window,UINT message,WPARAM wp,LPARAM lp)
const auto &warning=*reinterpret_cast<WarningPrompt*>(lp);
SetDlgItemTextW(window,230,warning.message);
SetDlgItemTextW(window,231,warning.details);
SetDlgItemTextW(window,330,conflictFileDetails(warning.incoming).c_str());
SetDlgItemTextW(window,332,conflictFileDetails(warning.existing).c_str());
SetDlgItemTextW(window,IDYES,warning.accept);
SetDlgItemTextW(window,IDNO,warning.decline);
for(const auto control:{230,231,232})
for(const auto control:{230,231,232,329,330,331,332})
SendDlgItemMessageW(window,control,WM_SETFONT,reinterpret_cast<WPARAM>(mono),FALSE);
centerDialogRow(window,233,234,{232});
centerDialogRow(window,333,234,{232});
layoutDialogButtons(window,234,{IDYES,IDNO,IDCANCEL});
SendMessageW(window,DM_SETDEFID,IDNO,0);
constexpr int buttons[]{IDYES,IDNO,IDCANCEL};
@@ -2199,7 +2327,7 @@ INT_PTR CALLBACK warningDialog(HWND window,UINT message,WPARAM wp,LPARAM lp)
}
if(message==WM_CLOSE)
{
EndDialog(window,IDCANCEL);
finishFilesDialog(window,IDCANCEL);
return TRUE;
}
if(message==WM_COMMAND && LOWORD(wp)==232 && HIWORD(wp)==BN_CLICKED)
@@ -2212,7 +2340,7 @@ INT_PTR CALLBACK warningDialog(HWND window,UINT message,WPARAM wp,LPARAM lp)
if(message==WM_COMMAND && (LOWORD(wp)==IDYES || LOWORD(wp)==IDNO || LOWORD(wp)==IDCANCEL))
{
const auto choice=LOWORD(wp);
EndDialog(window,choice);
finishFilesDialog(window,choice);
return TRUE;
}
return FALSE;
@@ -2235,7 +2363,7 @@ bool pumpCopy(CopyProgress &progress)
((message.message>=WM_KEYFIRST && message.message<=WM_KEYLAST) ||
(message.message>=WM_MOUSEFIRST && message.message<=WM_MOUSELAST)))
{
if(message.message==WM_KEYDOWN && message.wParam==VK_ESCAPE)
if(filesMouseDown(message.message) || (message.message==WM_KEYDOWN && message.wParam==VK_ESCAPE))
SendMessageW(progress.window,WM_COMMAND,IDCANCEL,0);
else if(message.message==WM_SYSKEYDOWN && message.wParam==VK_F4)
SendMessageW(owner,WM_CLOSE,0,0);
@@ -2416,9 +2544,9 @@ CopyResult copyItem(const CopyEndpoints &hosts,const fs::path &from,const fs::pa
if(!answer)
{
WarningPrompt warning{readOnly?L"Existing file is read-only":L"File already exists",
to.c_str(),L"&Overwrite",L"&Skip"};
to.c_str(),L"&Overwrite",L"&Skip",*incoming,*existing};
warningBrush=CreateSolidBrush(RGB(128,0,0));
answer=DialogBoxParamW(GetModuleHandleW(nullptr),MAKEINTRESOURCEW(104),progress.window,
answer=runFilesDialog(GetModuleHandleW(nullptr),MAKEINTRESOURCEW(104),progress.window,
warningDialog,reinterpret_cast<LPARAM>(&warning));
if(warning.remember && (answer==IDYES || answer==IDNO))
progress.conflictChoice=static_cast<int>(answer);
@@ -2454,7 +2582,7 @@ void copySelection(HWND owner,Panel &source,Panel &destination,bool move)
if(row<0 || static_cast<size_t>(row)>=source.rows.size() || source.sizes[row]==L"Up") return;
request.sources.push_back(source.rows[row]);
}
const auto result=DialogBoxParamW(GetModuleHandleW(nullptr),MAKEINTRESOURCEW(102),owner,copyDialog,
const auto result=runFilesDialog(GetModuleHandleW(nullptr),MAKEINTRESOURCEW(102),owner,copyDialog,
reinterpret_cast<LPARAM>(&request));
if(result==-1) setStatus(L"Could not open the Copy dialog.");
if(result!=IDOK) return;
@@ -2475,6 +2603,7 @@ void copySelection(HWND owner,Panel &source,Panel &destination,bool move)
setStatus(L"Could not open copy progress.");
return;
}
FilesDialogInput progressInput(progress.window);
copyInProgress=true;
activeCopy=&progress;
for(auto &panel:panels) EnableWindow(panel.list,FALSE);
@@ -2702,14 +2831,14 @@ INT_PTR CALLBACK deleteConfirmationDialog(HWND window,UINT message,WPARAM wp,LPA
}
if(message==WM_CLOSE || (message==WM_COMMAND && LOWORD(wp)==IDCANCEL))
{
EndDialog(window,IDCANCEL);
finishFilesDialog(window,IDCANCEL);
return TRUE;
}
if(message==WM_COMMAND && (LOWORD(wp)==IDYES ||
((data->folderPrompt || data->readOnlyPrompt) && (LOWORD(wp)==244 || LOWORD(wp)==IDNO)) ||
(data->readOnlyPrompt && LOWORD(wp)==249)))
{
EndDialog(window,LOWORD(wp));
finishFilesDialog(window,LOWORD(wp));
return TRUE;
}
return FALSE;
@@ -2751,7 +2880,7 @@ bool pumpDeletion(HWND window,bool &canceled)
canceled=true;
break;
}
if(!IsDialogMessageW(window,&message))
if(!filesDialogHandlesMessage(window,message))
{
TranslateMessage(&message);
DispatchMessageW(&message);
@@ -2801,7 +2930,7 @@ void deleteSelection(HWND owner,Panel &panel,bool permanent)
}
DeleteConfirmation confirmation{message,entries,static_cast<int>(shown+(shown<paths.size())),
permanent,false};
if(DialogBoxParamW(instance,MAKEINTRESOURCEW(106),owner,deleteConfirmationDialog,
if(runFilesDialog(instance,MAKEINTRESOURCEW(106),owner,deleteConfirmationDialog,
reinterpret_cast<LPARAM>(&confirmation))!=IDYES)
{
DeleteObject(warningBrush);
@@ -2815,7 +2944,8 @@ void deleteSelection(HWND owner,Panel &panel,bool permanent)
reinterpret_cast<LPARAM>(&canceled));
if(window)
{
EnableWindow(owner,FALSE);
GetWindowRect(owner,&previousOwnerBounds);
FilesDialogInput progressInput(window);
ShowWindow(window,SW_SHOW);
size_t done=0, skipped=0, failed=0, total=0;
bool all=false;
@@ -2879,7 +3009,7 @@ void deleteSelection(HWND owner,Panel &panel,bool permanent)
if(group.size()>1 && !all)
{
DeleteConfirmation folder{L"The following folder will be deleted:",group.front().wstring(),1,true,true};
const auto answer=DialogBoxParamW(instance,MAKEINTRESOURCEW(106),window,deleteConfirmationDialog,
const auto answer=runFilesDialog(instance,MAKEINTRESOURCEW(106),window,deleteConfirmationDialog,
reinterpret_cast<LPARAM>(&folder));
if(answer==244) all=true;
else if(answer==IDNO)
@@ -2906,7 +3036,7 @@ void deleteSelection(HWND owner,Panel &panel,bool permanent)
if(!answer)
{
DeleteConfirmation warning{L"Delete this read-only file?",current.wstring(),1,true,false,true};
answer=DialogBoxParamW(instance,MAKEINTRESOURCEW(106),window,deleteConfirmationDialog,
answer=runFilesDialog(instance,MAKEINTRESOURCEW(106),window,deleteConfirmationDialog,
reinterpret_cast<LPARAM>(&warning));
if(answer==244 || answer==249) readOnlyChoice=static_cast<int>(answer);
}
@@ -2950,7 +3080,7 @@ void deleteSelection(HWND owner,Panel &panel,bool permanent)
catch(const std::system_error &e){++failed;failure=fileError(e);}
catch(const std::exception &){++failed;failure=L"File connection interrupted.";}
RedrawWindow(window,nullptr,nullptr,RDW_UPDATENOW|RDW_ALLCHILDREN);
EnableWindow(owner,TRUE);
DestroyWindow(window);
for(auto &visible:panels)
if(!visible.directory.empty()) open(visible,visible.directory);
@@ -2964,8 +3094,29 @@ void deleteSelection(HWND owner,Panel &panel,bool permanent)
warningBrush=nullptr;
}
void selectPanelEdge(Panel &panel,bool first)
{
SetFocus(panel.list);
const auto count=SendMessageW(panel.list,LB_GETCOUNT,0,0);
if(count<=0) return;
SendMessageW(panel.list,LB_SETCURSEL,first?0:count-1,0);
InvalidateRect(panel.list,nullptr,FALSE);
showFileDetails(panel);
}
LRESULT CALLBACK panelKeys(HWND window, UINT message, WPARAM wp, LPARAM lp, UINT_PTR, DWORD_PTR index)
{
if(message==WM_LBUTTONDOWN)
{
const auto count=SendMessageW(window,LB_GETCOUNT,0,0);
RECT last{};
if(count>0 && SendMessageW(window,LB_GETITEMRECT,count-1,reinterpret_cast<LPARAM>(&last))!=LB_ERR &&
GET_Y_LPARAM(lp)>=last.bottom)
{
selectPanelEdge(panels[index],false);
return 0;
}
}
const bool rangeKey=wp==VK_UP || wp==VK_DOWN || wp==VK_LEFT || wp==VK_RIGHT;
if(message==WM_KEYDOWN && (wp==VK_INSERT || (rangeKey && (GetKeyState(VK_SHIFT)&0x8000))))
{
@@ -2994,7 +3145,6 @@ LRESULT CALLBACK panelKeys(HWND window, UINT message, WPARAM wp, LPARAM lp, UINT
}
if (message == WM_KEYDOWN && (wp == VK_LEFT || wp == VK_RIGHT))
return scrollList(window, message, wp == VK_LEFT ? VK_HOME : VK_END, lp);
if(message==WM_KILLFOCUS || message==WM_SETFOCUS) InvalidateRect(window,nullptr,FALSE);
if (message == WM_SETFOCUS)
{
const auto previous=active;
@@ -3068,6 +3218,36 @@ LRESULT CALLBACK panelKeys(HWND window, UINT message, WPARAM wp, LPARAM lp, UINT
}
#include "files_folder_shortcuts.inl"
#include "files_command_history_dialog.inl"
bool loadFilesPreferences(HWND window)
{
LOGFONTW font{};
DWORD bytes=sizeof(font);
if(RegGetValueW(HKEY_CURRENT_USER,schemeKey,L"DisplayFont",RRF_RT_REG_BINARY,nullptr,&font,&bytes)
!=ERROR_SUCCESS || bytes!=sizeof(font))
{
font=defaultFilesFont();
}
font.lfFaceName[LF_FACESIZE-1]=0;
if(!applyFont(window,font,static_cast<int>(std::clamp(preference(L"FontPoints",defaultFontPoints),80UL,240UL)))) return false;
loadColors();
dialogFramePadding={
std::min(preference(dialogFrameOuterKey,defaultDialogFramePadding.outer),maxDialogFramePadding),
std::min(preference(dialogFrameInnerKey,defaultDialogFramePadding.inner),maxDialogFramePadding),
std::min(preference(dialogRowTextKey,defaultDialogFramePadding.rowText),maxDialogRowTextPadding)};
loadFileStyles();
loadFolderShortcuts();
panels[0].wide=preference(L"LeftWide",defaultWide)!=0;
panels[1].wide=preference(L"RightWide",defaultWide)!=0;
sizeWidth=static_cast<int>(std::clamp(preference(L"SizeWidth",defaultSizeWidth),6UL,16UL));
alignExtensions=preference(L"AlignExtensions",defaultAlignExtensions)!=0;
showDetails=preference(L"ShowDetails",defaultShowDetails)!=0;
alignFolders=preference(L"AlignFolders",defaultAlignFolders)!=0;
statusFields=preference(L"StatusFields",defaultStatusFields)&15;
loadCommandHistoryKeys();
return true;
}
#include "files_options.inl"
void openMenu(HWND window, int index)
@@ -3185,6 +3365,18 @@ bool handleFilesMenuKey(HWND window,UINT message,WPARAM key,LPARAM flags,bool mo
LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
{
if(message==filesPreferencesChanged)
{
if(reinterpret_cast<HWND>(wp)!=window)
{
loadFilesPreferences(window);
layout(window);
layoutOpenCommandHistory();
showFileDetails(panels[active]);
RedrawWindow(window,nullptr,nullptr,RDW_INVALIDATE|RDW_ALLCHILDREN);
}
return 0;
}
switch (message)
{
case WM_ACTIVATEAPP:
@@ -3199,10 +3391,13 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
try
{
const auto *begin=static_cast<const char*>(reply->lpData);
for(const auto &row:Json::parse(begin,begin+reply->cbData))
const auto rows=Json::parse(begin,begin+reply->cbData);
if(!rows.is_array()) return FALSE;
for(const auto &row:rows)
drivePeers.push_back({row.at("id"),row.at("address"),ks::wide(row.at("name")),ks::lan::FilesPort,row.value("filesBrowse",false)});
}
catch(const std::exception &){drivePeers.clear();return FALSE;}
drivePeerReplyReceived=true;
return TRUE;
}
case WM_TIMER:
@@ -3221,29 +3416,7 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
case WM_CREATE:
{
auto instance = GetModuleHandleW(nullptr);
LOGFONTW font{};
DWORD bytes=sizeof(font);
if(RegGetValueW(HKEY_CURRENT_USER,schemeKey,L"DisplayFont",RRF_RT_REG_BINARY,nullptr,&font,&bytes)
!=ERROR_SUCCESS || bytes!=sizeof(font))
{
font=defaultFilesFont();
}
font.lfFaceName[LF_FACESIZE-1]=0;
if(!applyFont(window,font,static_cast<int>(std::clamp(preference(L"FontPoints",defaultFontPoints),80UL,240UL)))) return -1;
loadColors();
dialogFramePadding={
std::min(preference(dialogFrameOuterKey,defaultDialogFramePadding.outer),maxDialogFramePadding),
std::min(preference(dialogFrameInnerKey,defaultDialogFramePadding.inner),maxDialogFramePadding),
std::min(preference(dialogRowTextKey,defaultDialogFramePadding.rowText),maxDialogRowTextPadding)};
loadFileStyles();
loadFolderShortcuts();
panels[0].wide=preference(L"LeftWide",defaultWide)!=0;
panels[1].wide=preference(L"RightWide",defaultWide)!=0;
sizeWidth=static_cast<int>(std::clamp(preference(L"SizeWidth",defaultSizeWidth),6UL,16UL));
alignExtensions=preference(L"AlignExtensions",defaultAlignExtensions)!=0;
showDetails=preference(L"ShowDetails",defaultShowDetails)!=0;
alignFolders=preference(L"AlignFolders",defaultAlignFolders)!=0;
statusFields=preference(L"StatusFields",defaultStatusFields)&15;
if(!loadFilesPreferences(window)) return -1;
menuBar=CreateWindowW(L"BUTTON",L"",WS_CHILD|WS_CLIPSIBLINGS|BS_OWNERDRAW,
0,0,0,0,window,reinterpret_cast<HMENU>(120),instance,nullptr);
const wchar_t *menus[]{L"Left",L"Files",L"Commands",L"Options",L"Right"};
@@ -3264,7 +3437,7 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
0, 0, 0, 0, window, nullptr, instance, nullptr);
terminalInput=CreateWindowW(L"EDIT",L"",WS_CHILD|WS_VISIBLE|ES_AUTOHSCROLL,
0,0,0,0,window,nullptr,instance,nullptr);
terminalPrompt=CreateWindowW(L"STATIC",L"",WS_CHILD|WS_VISIBLE|SS_PATHELLIPSIS|SS_CENTERIMAGE,
terminalPrompt=CreateWindowW(L"STATIC",L"",WS_CHILD|WS_VISIBLE|SS_PATHELLIPSIS,
0,0,0,0,window,nullptr,instance,nullptr);
terminalOutput=CreateWindowW(L"STATIC",L"",WS_CHILD|WS_CLIPCHILDREN,
0,0,0,0,window,nullptr,instance,nullptr);
@@ -3325,7 +3498,8 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
return 0;
}
case WM_MOVE:
if(copyInProgress && !IsIconic(window))
layoutOpenCommandHistory(true);
if((copyInProgress || activeFilesDialog) && !IsIconic(window))
{
RECT bounds{};
if(GetWindowRect(window,&bounds))
@@ -3341,6 +3515,7 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
case WM_SIZE:
hidePicker();
layout(window);
layoutOpenCommandHistory();
return 0;
case WM_GETMINMAXINFO:
reinterpret_cast<MINMAXINFO *>(lp)->ptMinTrackSize = {540, 220};
@@ -3361,6 +3536,20 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
if (message == WM_LBUTTONDOWN) { dragging = true; SetCapture(window); }
return TRUE;
}
if(message==WM_LBUTTONDOWN && !terminalVisible)
{
for(auto &panel:panels)
{
RECT list{};
if(!GetWindowRect(panel.list,&list)) continue;
MapWindowPoints(nullptr,window,reinterpret_cast<POINT*>(&list),2);
if(point.x>=list.left && point.x<list.right && point.y>=0 && point.y<list.top)
{
selectPanelEdge(panel,true);
return 0;
}
}
}
break;
}
case WM_MOUSEMOVE:
@@ -3548,17 +3737,19 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
}
if(item.CtlID>=130 && item.CtlID<=134 && static_cast<int>(item.CtlID)-130==menuIndex)
background=Selection;
if(item.CtlID==102 || item.CtlID==103)
const bool panelRow=item.CtlID==102 || item.CtlID==103;
const bool activePanelRow=panelRow && item.hwndItem==panels[active].list;
if(panelRow)
{
const auto cursor=SendMessageW(item.hwndItem,LB_GETCURSEL,0,0);
if(GetFocus()==item.hwndItem && cursor!=LB_ERR &&
if(activePanelRow && cursor!=LB_ERR &&
static_cast<UINT>(cursor)==item.itemID) background=PanelCursor;
}
else if(item.itemState&(ODS_SELECTED|ODS_FOCUS))
background=Selection;
COLORREF rowBackground=colors[background];
HFONT rowFont{};
if(item.CtlID==102 || item.CtlID==103)
if(panelRow)
{
const auto &panel=panels[item.CtlID-102];
if(item.itemID<panel.rows.size() && panel.sizes[item.itemID]!=L"Up")
@@ -3567,7 +3758,7 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
const bool transferred=transferHighlightPhase%2==0 && panel.transferResults.contains(panel.rows[item.itemID]);
const bool cursor=SendMessageW(panel.list,LB_GETCURSEL,0,0)==item.itemID;
const auto state=marked?FileRowState::Marked:!cursor?FileRowState::Normal:
GetFocus()==panel.list?FileRowState::Focused:FileRowState::Unfocused;
activePanelRow?FileRowState::Focused:FileRowState::Unfocused;
const auto resolved=resolveFileStyle(panel.rows[item.itemID].extension().native(),
panel.sizes[item.itemID]!=L"Folder",panel.dimmed[item.itemID],
background==PanelCursor || marked || transferred,foreground,rowBackground,fileStyles,state,transferred);
@@ -3684,20 +3875,19 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
FillRect(item.hDC,&line,static_cast<HBRUSH>(GetStockObject(DC_BRUSH)));
}
}
if (item.CtlType==ODT_LISTBOX)
if (item.CtlType==ODT_LISTBOX && item.hwndItem!=picker)
{
RECT track{};
GetWindowRect(item.hwndItem,&track);
auto owner=item.hwndItem==picker?pickerFrame:window;
MapWindowPoints(nullptr,owner,reinterpret_cast<POINT*>(&track),2);
MapWindowPoints(nullptr,window,reinterpret_cast<POINT*>(&track),2);
track.left=track.right;
track.right+=5;
InvalidateRect(owner,&track,FALSE);
InvalidateRect(window,&track,FALSE);
}
return TRUE;
}
case WM_COMMAND:
if(copyInProgress) return 0;
if(copyInProgress || commandHistoryWindow || activeFilesDialog) return 0;
if(LOWORD(wp)==121 && HIWORD(wp)==LBN_DBLCLK) selectDrive();
if(LOWORD(wp)>=130 && LOWORD(wp)<=134 && HIWORD(wp)==BN_CLICKED)
{
@@ -3720,9 +3910,14 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
}
return 0;
case WM_CLOSE:
if(activeFilesDialog && !activeCopy)
{
PostMessageW(activeFilesDialog,WM_CLOSE,0,0);
return 0;
}
if(activeCopy)
{
if(!IsWindowEnabled(activeCopy->window))
if(activeFilesDialog!=activeCopy->window)
{
setStatus(L"Resolve the copy prompt before closing.");
return 0;
@@ -3759,6 +3954,16 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int show)
{
bool writeLog=true;
try
{
const auto settings=ks::dataDirectory()/L"settings.json";
if(std::filesystem::exists(settings))
writeLog=Json::parse(ks::readFile(settings,1024*1024)).value("writeLog",true);
}
catch(const std::exception &) {}
ks::setLogFilename(L"kswitcher-files.log");
ks::setLoggingEnabled(writeLog);
int count{};
if(auto args=CommandLineToArgvW(GetCommandLineW(),&count))
{
@@ -3780,8 +3985,9 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int show)
}
LocalFree(args);
}
INITCOMMONCONTROLSEX controls{sizeof(controls), ICC_STANDARD_CLASSES|ICC_PROGRESS_CLASS|ICC_TREEVIEW_CLASSES};
INITCOMMONCONTROLSEX controls{sizeof(controls), ICC_STANDARD_CLASSES|ICC_PROGRESS_CLASS|ICC_TREEVIEW_CLASSES|ICC_HOTKEY_CLASS};
InitCommonControlsEx(&controls);
loadCommandHistory();
WNDCLASSW type{};
type.hInstance = instance;
type.lpfnWndProc = filesWindow;
@@ -3799,6 +4005,21 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int show)
MSG message{};
while (GetMessageW(&message, nullptr, 0, 0) > 0)
{
if(commandHistoryWindow)
{
if(message.message==WM_KEYDOWN && (message.wParam==VK_RETURN || message.wParam==VK_ESCAPE) &&
(message.hwnd==commandHistoryWindow || IsChild(commandHistoryWindow,message.hwnd)))
{
SendMessageW(commandHistoryWindow,WM_COMMAND,message.wParam==VK_RETURN?IDOK:IDCANCEL,0);
continue;
}
if(IsDialogMessageW(commandHistoryWindow,&message)) continue;
const bool ownerClient=message.hwnd==window || IsChild(window,message.hwnd);
const bool clientInput=(message.message>=WM_KEYFIRST && message.message<=WM_KEYLAST) ||
(message.message>=WM_MOUSEFIRST && message.message<=WM_MOUSELAST) ||
message.message==WM_CONTEXTMENU;
if(ownerClient && clientInput) continue;
}
if(message.message==WM_KEYDOWN && (message.wParam==VK_UP || message.wParam==VK_DOWN) &&
(GetKeyState(VK_CONTROL)&0x8000) && !(GetKeyState(VK_MENU)&0x8000) &&
(message.hwnd==window || IsChild(window,message.hwnd)) && !terminalVisible && !copyInProgress)
@@ -3839,6 +4060,22 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int show)
continue;
}
const bool filesMessage=message.hwnd==window || IsChild(window,message.hwnd);
const bool panelFocused=message.hwnd==panels[0].list || message.hwnd==panels[1].list;
if(panelFocused && !copyInProgress && message.message==WM_KEYDOWN &&
!(GetKeyState(VK_MENU)&0x8000) &&
!(GetKeyState(VK_LWIN)&0x8000) && !(GetKeyState(VK_RWIN)&0x8000))
{
const bool ctrl=(GetKeyState(VK_CONTROL)&0x8000)!=0;
const bool shift=(GetKeyState(VK_SHIFT)&0x8000)!=0;
if((message.wParam=='V' && ctrl && !shift) ||
(message.wParam==VK_INSERT && shift && !ctrl))
{
SetFocus(terminalInput);
SendMessageW(terminalInput,WM_PASTE,0,0);
continue;
}
}
if(filesMessage && !copyInProgress && handleCommandHistoryKey(window,message)) continue;
const bool modified=(GetKeyState(VK_CONTROL)&0x8000) || (GetKeyState(VK_SHIFT)&0x8000) ||
(GetKeyState(VK_LWIN)&0x8000) || (GetKeyState(VK_RWIN)&0x8000);
if(filesMessage && !copyInProgress &&
+121 -48
View File
@@ -1,7 +1,7 @@
constexpr int optionsSearch=270,optionsTree=271,optionsTitle=272,optionsApply=283,optionsReset=284,optionsProperty=285,optionsResetProperty=286;
constexpr int allOptions=3;
enum OptionsPage { PanelModes, PanelColors, Appearance };
constexpr const wchar_t *optionsTitles[]{L"File panel modes",L"Colors",L"Appearance"};
constexpr int allOptions=4;
enum OptionsPage { PanelModes, PanelColors, Appearance, CommandLine };
constexpr const wchar_t *optionsTitles[]{L"File panel modes",L"Colors",L"Appearance",L"Command line"};
constexpr const wchar_t *colorNames[]{L"Panel background",L"Text",L"Frame",L"Popup background",
L"Menu selection",L"Command bar",L"File cursor"};
enum PanelSection { PanelLayout, WideColumns, StatusColumns };
@@ -52,8 +52,22 @@ void resetPanelProperty(HWND dialog,const PanelProperty &property)
else CheckDlgButton(dialog,property.control,property.defaultValue);
}
struct OptionsBaseline
{
LOGFONTW font=displayFont;
int points=fontPoints;
DialogFramePadding padding=dialogFramePadding;
std::array<COLORREF,ColorCount> palette=colors;
std::vector<FileStyle> styles=fileStyles;
std::array<WORD,2> keys=commandHistoryKeys;
DWORD width=static_cast<DWORD>(sizeWidth), fields=statusFields;
bool extensions=alignExtensions, folders=alignFolders, details=showDetails;
std::array<bool,2> wide{panels[0].wide,panels[1].wide};
};
struct OptionsDraft
{
OptionsBaseline baseline;
LOGFONTW font=displayFont;
int points=fontPoints;
DialogFramePadding framePadding=dialogFramePadding;
@@ -81,15 +95,28 @@ struct OptionsDraft
bool optionsFontChanged(const OptionsDraft &draft)
{
auto pending=draft.font, applied=displayFont;
auto pending=draft.font, applied=draft.baseline.font;
// Height follows the selected point size and DPI. applyFont always uses natural width.
pending.lfHeight=applied.lfHeight=0;
pending.lfWidth=applied.lfWidth=0;
return draft.points!=fontPoints ||
return draft.points!=draft.baseline.points ||
memcmp(&pending,&applied,offsetof(LOGFONTW,lfFaceName))!=0 ||
_wcsicmp(pending.lfFaceName,applied.lfFaceName)!=0;
}
struct OptionsWriteLock
{
HANDLE mutex=CreateMutexW(nullptr,FALSE,L"Local\\KSwitcher.Files.OptionsWrite");
DWORD result=mutex?WaitForSingleObject(mutex,2000):WAIT_FAILED;
bool acquired() const { return result==WAIT_OBJECT_0 || result==WAIT_ABANDONED; }
~OptionsWriteLock()
{
if(acquired()) ReleaseMutex(mutex);
if(mutex) CloseHandle(mutex);
}
};
void updateOptionsApply(HWND dialog,const OptionsDraft &draft)
{
BOOL valid{};
@@ -97,11 +124,13 @@ void updateOptionsApply(HWND dialog,const OptionsDraft &draft)
const auto checked=[&](int id){return IsDlgButtonChecked(dialog,id)==BST_CHECKED;};
DWORD fields=0;
for(int i=0;i<4;++i) if(checked(205+i)) fields|=1u<<i;
const bool dirty=!valid || width!=static_cast<UINT>(sizeWidth) ||
checked(273)!=panels[0].wide || checked(274)!=panels[1].wide ||
checked(202)!=alignExtensions || checked(204)!=alignFolders || checked(203)!=showDetails ||
fields!=statusFields || draft.palette!=colors || draft.styles!=fileStyles ||
draft.framePadding!=dialogFramePadding || optionsFontChanged(draft);
const auto &base=draft.baseline;
const bool dirty=!valid || width!=base.width ||
checked(273)!=base.wide[0] || checked(274)!=base.wide[1] ||
checked(202)!=base.extensions || checked(204)!=base.folders || checked(203)!=base.details ||
fields!=base.fields || draft.palette!=base.palette || draft.styles!=base.styles ||
draft.framePadding!=base.padding || optionsFontChanged(draft) ||
optionHistoryKeys(dialog)!=base.keys;
EnableWindow(GetDlgItem(dialog,optionsApply),dirty);
InvalidateRect(GetDlgItem(dialog,fileStylePreview),nullptr,FALSE);
}
@@ -181,7 +210,7 @@ void showOptionsPage(HWND dialog,OptionsDraft &draft,int page,int section)
if(section>=0)
title=page==PanelModes?panelSectionNames[section]:page==PanelColors?
(section==fileStyleSection?L"File type styles":colorNames[section]):
section==DialogFrame?L"Dialog frame":L"Font";
page==CommandLine?L"Command history":section==DialogFrame?L"Dialog frame":L"Font";
}
SetDlgItemTextW(dialog,optionsTitle,title);
const auto show=[&](auto &controls,bool visible)
@@ -215,11 +244,13 @@ void showOptionsPage(HWND dialog,OptionsDraft &draft,int page,int section)
for(const auto &option:frameOptions)
SendDlgItemMessageW(dialog,option.id,CB_SETCURSEL,draft.framePadding.*option.value,0);
}
constexpr int commandLineControls[]{320,321,322,323};
show(commandLineControls,page==CommandLine && section==0);
const bool fileTypes=page==PanelColors && section==fileStyleSection;
constexpr int fileStyleControls[]{287,288,289,290,291,292,293,294,295,296,297,298,300,fileStyleState};
show(fileStyleControls,fileTypes);
const auto preview=GetDlgItem(dialog,fileStylePreview);
ShowWindow(preview,page>=0 && page<allOptions && section>=0?SW_SHOW:SW_HIDE);
ShowWindow(preview,page>=0 && page<CommandLine && section>=0?SW_SHOW:SW_HIDE);
layoutOptionsPreview(dialog);
RedrawWindow(dialog,nullptr,nullptr,RDW_INVALIDATE|RDW_ERASE|RDW_ALLCHILDREN);
show(colorOptionControls,page==PanelColors && section>=0 && !fileTypes);
@@ -275,7 +306,7 @@ void filterOptions(HWND dialog,OptionsDraft &draft)
draft.wideColumnsNode=nullptr;
const auto root=add(L"All settings",allOptions,-1,TVI_ROOT);
HTREEITEM first{},selected{};
for(int page=PanelModes;page<=Appearance;++page)
for(int page=PanelModes;page<=CommandLine;++page)
{
HTREEITEM category{};
const bool wholeCategory=matches(optionsTitles[page]) || (!query.empty() && matches(L"All settings"));
@@ -305,6 +336,7 @@ void filterOptions(HWND dialog,OptionsDraft &draft)
addSection(L"File type styles",fileStyleSection,extensionMatches || matches(L"System and hidden") ||
matches(L"Underline") || matches(L"Strikethrough") || matches(L"Foreground"));
}
else if(page==CommandLine) addSection(L"Command history",0,matches(L"Previous command"));
else
{
addSection(L"Font",Font,matches(L"Display font"));
@@ -349,6 +381,7 @@ INT_PTR CALLBACK optionsDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
CheckDlgButton(dialog,273,panels[0].wide);
CheckDlgButton(dialog,274,panels[1].wide);
for(int i=0;i<4;++i) CheckDlgButton(dialog,205+i,(statusFields>>i)&1u);
setOptionHistoryKeys(dialog,commandHistoryKeys);
filterOptions(dialog,draft);
updateOptionsApply(dialog,draft);
return TRUE;
@@ -388,7 +421,7 @@ INT_PTR CALLBACK optionsDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
}
if(message==WM_CLOSE || (message==WM_COMMAND && LOWORD(wp)==IDCANCEL))
{
EndDialog(dialog,IDCANCEL);
finishFilesDialog(dialog,IDCANCEL);
return TRUE;
}
if(message!=WM_COMMAND) return FALSE;
@@ -461,6 +494,7 @@ INT_PTR CALLBACK optionsDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
}
return TRUE;
case optionsReset:
if(draft->page==allOptions || draft->page==CommandLine) setOptionHistoryKeys(dialog,defaultHistoryKeys);
if(draft->page==allOptions || draft->page==PanelModes)
for(const auto &property:panelProperties)
if(draft->section<0 || property.section==draft->section) resetPanelProperty(dialog,property);
@@ -498,6 +532,9 @@ INT_PTR CALLBACK optionsDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
case 273: case 274:
updateOptionsApply(dialog,*draft);
return TRUE;
case 321: case 323:
updateOptionsApply(dialog,*draft);
return TRUE;
case optionsApply:
case IDOK:
{
@@ -513,7 +550,30 @@ INT_PTR CALLBACK optionsDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
return TRUE;
}
const auto owner=GetWindow(dialog,GW_OWNER);
const bool stylesChanged=draft->styles!=fileStyles;
const OptionsWriteLock writeLock;
if(!writeLock.acquired())
{
setStatus(L"Another window is saving Files settings. Try Apply again.");
return TRUE;
}
const auto &base=draft->baseline;
auto keys=optionHistoryKeys(dialog);
loadCommandHistoryKeys();
for(size_t i=0;i<keys.size();++i)
if(keys[i]==base.keys[i]) keys[i]=commandHistoryKeys[i];
if(!validHistoryKeys(keys))
{
MessageBoxW(dialog,L"Choose distinct shortcuts using Ctrl or Alt that are free in Files.",L"Command history",MB_OK|MB_ICONWARNING);
return TRUE;
}
const bool stylesChanged=draft->styles!=base.styles;
loadFileStyles();
if(stylesChanged && fileStyles!=base.styles && fileStyles!=draft->styles)
{
MessageBoxW(dialog,L"File type styles changed in another window. Reopen Options before editing them again.",
L"KSwitcher Files Options",MB_OK|MB_ICONWARNING);
return TRUE;
}
if(stylesChanged && !saveFileStyles(draft->styles))
{
MessageBoxW(dialog,L"Could not save the file type styles.",L"KSwitcher Files Options",MB_OK|MB_ICONWARNING);
@@ -526,48 +586,61 @@ INT_PTR CALLBACK optionsDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
MessageBoxW(dialog,L"Could not apply the selected font.",optionsTitles[Appearance],MB_OK|MB_ICONWARNING);
return TRUE;
}
if(stylesChanged) fileStyles=draft->styles;
sizeWidth=static_cast<int>(width);
alignExtensions=IsDlgButtonChecked(dialog,202)==BST_CHECKED;
showDetails=IsDlgButtonChecked(dialog,203)==BST_CHECKED;
alignFolders=IsDlgButtonChecked(dialog,204)==BST_CHECKED;
statusFields=0;
for(int i=0;i<4;++i)
if(IsDlgButtonChecked(dialog,205+i)==BST_CHECKED) statusFields|=1u<<i;
savePreference(L"SizeWidth",sizeWidth);
savePreference(L"AlignExtensions",alignExtensions);
savePreference(L"ShowDetails",showDetails);
savePreference(L"AlignFolders",alignFolders);
savePreference(L"StatusFields",statusFields);
for(int i=0;i<ColorCount;++i)
if(colors[i]!=draft->palette[i])
{
colors[i]=draft->palette[i];
savePreference(std::to_wstring(i).c_str(),colors[i]);
}
refreshColors();
if(draft->framePadding!=dialogFramePadding)
const auto saveChanged=[](const wchar_t *name,DWORD value,DWORD initial)
{
dialogFramePadding=draft->framePadding;
savePreference(dialogFrameOuterKey,dialogFramePadding.outer);
savePreference(dialogFrameInnerKey,dialogFramePadding.inner);
savePreference(dialogRowTextKey,dialogFramePadding.rowText);
if(value!=initial) savePreference(name,value);
};
const auto checked=[&](int id){return IsDlgButtonChecked(dialog,id)==BST_CHECKED;};
const auto inputKeys=optionHistoryKeys(dialog);
saveChanged(L"PreviousCommandKey",inputKeys[0],base.keys[0]);
saveChanged(L"CommandHistoryKey",inputKeys[1],base.keys[1]);
saveChanged(L"SizeWidth",width,base.width);
saveChanged(L"AlignExtensions",checked(202),base.extensions);
saveChanged(L"AlignFolders",checked(204),base.folders);
saveChanged(L"ShowDetails",checked(203),base.details);
DWORD fields=preference(L"StatusFields",defaultStatusFields)&15;
const auto initialFields=fields;
for(int i=0;i<4;++i)
{
const DWORD bit=1u<<i;
if(checked(205+i)!=((base.fields&bit)!=0)) fields=checked(205+i)?fields|bit:fields&~bit;
}
saveChanged(L"StatusFields",fields,initialFields);
for(int i=0;i<ColorCount;++i)
saveChanged(std::to_wstring(i).c_str(),draft->palette[i],base.palette[i]);
saveChanged(dialogFrameOuterKey,draft->framePadding.outer,base.padding.outer);
saveChanged(dialogFrameInnerKey,draft->framePadding.inner,base.padding.inner);
saveChanged(dialogRowTextKey,draft->framePadding.rowText,base.padding.rowText);
if(fontChanged)
{
savePreference(L"FontPoints",fontPoints);
RegSetKeyValueW(HKEY_CURRENT_USER,schemeKey,L"DisplayFont",REG_BINARY,&displayFont,sizeof(displayFont));
draft->font=displayFont;
}
for(int i=0;i<2;++i)
{
const bool wide=IsDlgButtonChecked(dialog,273+i)==BST_CHECKED;
if(panels[i].wide!=wide) setMode(i,wide);
}
saveChanged(L"LeftWide",checked(273),base.wide[0]);
saveChanged(L"RightWide",checked(274),base.wide[1]);
loadFilesPreferences(owner);
notifyFilesPreferences(owner);
draft->baseline=OptionsBaseline{};
draft->font=displayFont;
draft->points=fontPoints;
draft->styles=fileStyles;
draft->palette=colors;
draft->framePadding=dialogFramePadding;
SetDlgItemInt(dialog,201,sizeWidth,FALSE);
CheckDlgButton(dialog,202,alignExtensions);
CheckDlgButton(dialog,203,showDetails);
CheckDlgButton(dialog,204,alignFolders);
CheckDlgButton(dialog,273,panels[0].wide);
CheckDlgButton(dialog,274,panels[1].wide);
for(int i=0;i<4;++i) CheckDlgButton(dialog,205+i,(statusFields>>i)&1u);
setOptionHistoryKeys(dialog,commandHistoryKeys);
if(draft->page==PanelColors && draft->section==fileStyleSection)
updateFileStyleEditor(dialog,*draft,true);
showOptionsPage(dialog,*draft,draft->page,draft->section);
layout(owner);
showFileDetails(panels[active]);
RedrawWindow(owner,nullptr,nullptr,RDW_INVALIDATE|RDW_ERASE|RDW_ALLCHILDREN|RDW_UPDATENOW);
if(LOWORD(wp)==IDOK) EndDialog(dialog,IDOK);
if(LOWORD(wp)==IDOK) finishFilesDialog(dialog,IDOK);
else updateOptionsApply(dialog,*draft);
return TRUE;
}
@@ -580,7 +653,7 @@ void editOptions(HWND owner)
const auto panel=panels[active].list;
OptionsDraft draft;
std::copy(std::begin(colors),std::end(colors),draft.palette.begin());
const auto result=DialogBoxParamW(GetModuleHandleW(nullptr),MAKEINTRESOURCEW(101),owner,optionsDialog,
const auto result=runFilesDialog(GetModuleHandleW(nullptr),MAKEINTRESOURCEW(101),owner,optionsDialog,
reinterpret_cast<LPARAM>(&draft));
SetFocus(panel);
if(result==-1) setStatus(L"Could not open Files Options.");
+1 -1
View File
@@ -15,7 +15,7 @@ struct FileStyle
const std::vector<FileStyle> defaultFileStyles{
{L"@system-hidden"},{L".exe",RGB(85,255,85)},{L".md",RGB(231,192,247)},
{L".7z",RGB(255,210,128)},{L".zip",RGB(255,210,128)},{L".rar",RGB(255,210,128)},
{L".7z",RGB(255,210,128)},{L".zip",RGB(255,155,170)},{L".rar",RGB(255,210,128)},
{L".tar",RGB(255,210,128)},{L".gz",RGB(255,210,128)},
{L".png",RGB(165,230,255)},{L".jpg",RGB(165,230,255)},{L".jpeg",RGB(165,230,255)},
{L".gif",RGB(165,230,255)},{L".webp",RGB(165,230,255)},{L".svg",RGB(165,230,255)},
+1 -1
View File
@@ -49,7 +49,7 @@ public:
startup.StartupInfo.cb=sizeof(startup);
startup.lpAttributeList=attributes;
PROCESS_INFORMATION info{};
wchar_t command[]=L"powershell.exe -NoLogo -NoProfile -NoExit -Command \"Remove-Module PSReadLine -ErrorAction SilentlyContinue; function global:prompt { ([string][char]27) + '[?25l' }; Clear-Host\"";
wchar_t command[]=L"powershell.exe -NoLogo -NoProfile -NoExit -Command \"Remove-Module PSReadLine -ErrorAction SilentlyContinue; function global:prompt { $location = Get-Location; $path = if ($location.Provider.Name -eq 'FileSystem') { [Uri]::EscapeDataString($location.ProviderPath) }; ([string][char]27) + ']633;ks-directory:' + $path + [char]7 + [char]27 + '[?25l' }; Clear-Host\"";
wchar_t system[MAX_PATH]{};
const auto length=GetSystemDirectoryW(system,MAX_PATH);
const auto shell=std::wstring(system)+L"\\WindowsPowerShell\\v1.0\\powershell.exe";
+7 -1
View File
@@ -737,6 +737,7 @@ namespace
{
std::recursive_mutex logGate;
bool loggingEnabled = false;
std::wstring logFilename = L"kswitcher.log";
DiagnosticBuffer diagnosticBuffer;
}
size_t DiagnosticBuffer::expire(ULONGLONG now)
@@ -832,6 +833,11 @@ void diagnosticOperation(const std::string &json)
record["context"] = utf8(diagnosticBuffer.record(L"Operation complete", true, GetTickCount64()));
log(L"Operation " + wide(record.dump(-1, ' ', true, nlohmann::json::error_handler_t::replace)));
}
void setLogFilename(std::wstring filename)
{
std::lock_guard lock(logGate);
logFilename = std::move(filename);
}
void log(const std::wstring &message)
{
std::lock_guard lock(logGate);
@@ -839,7 +845,7 @@ void log(const std::wstring &message)
return;
try
{
auto path = dataDirectory() / L"kswitcher.log";
auto path = dataDirectory() / logFilename;
std::error_code ec;
auto size = std::filesystem::file_size(path, ec);
if (!ec && size > 256 * 1024)
+1
View File
@@ -94,6 +94,7 @@ class DiagnosticBuffer
void clear() { entries.clear(); }
};
void setLoggingEnabled(bool enabled);
void setLogFilename(std::wstring filename);
void expireDiagnostics(ULONGLONG now);
void diagnosticEvent(const std::wstring &message, bool problem = false);
void diagnosticOperation(const std::string &json);
+3 -1
View File
@@ -1514,7 +1514,9 @@ Settings collectSettings(HWND w, SettingsView *v)
s.undo = value(w, 213);
s.clipboard = value(w, 214);
s.screenshot = value(w, 215);
s.filesHotkey = value(w, 216);
auto filesHotkey = value(w, 216);
if (s.filesHotkey || !filesHotkey.empty())
s.filesHotkey = std::move(filesHotkey);
s.maxItems = integer(w, 240);
s.maxMiB = integer(w, 241);
s.maxImageMiB = integer(w, 242);
+1 -1
View File
@@ -2,7 +2,7 @@
#include "platform.h"
namespace ks
{
inline constexpr wchar_t AppVersion[] = L"0.6.77";
inline constexpr wchar_t AppVersion[] = L"0.6.80";
inline constexpr UINT UpdateMessage = WM_APP + 7;
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
+7 -2
View File
@@ -292,11 +292,16 @@ int main()
HWND picker = GetDlgItem(dialog, 728);
RECT dialogBounds{};
GetWindowRect(dialog, &dialogBounds);
MONITORINFO monitor{sizeof(monitor)};
require(GetMonitorInfoW(MonitorFromWindow(dialog, MONITOR_DEFAULTTONEAREST), &monitor),
"monitor work area available");
const auto targetX = std::clamp(dialogBounds.left + 32, monitor.rcWork.left, monitor.rcWork.right - 120);
const auto targetY = std::clamp(dialogBounds.top + 100, monitor.rcWork.top, monitor.rcWork.bottom - 48);
HWND target = CreateWindowExW(WS_EX_TOPMOST | WS_EX_NOACTIVATE, L"EDIT", L"Picker fixture",
WS_POPUP | WS_VISIBLE, dialogBounds.left + 32, dialogBounds.top + 100, 120, 48,
WS_POPUP | WS_VISIBLE, targetX, targetY, 120, 48,
nullptr, nullptr, GetModuleHandleW(nullptr), nullptr);
require(picker && target, "window picker and target available");
POINT drop{dialogBounds.left + 33, dialogBounds.top + 101};
POINT drop{targetX + 1, targetY + 1};
require(GetAncestor(WindowFromPoint(drop), GA_ROOT) == target, "fixture window is under the drop point");
ScreenToClient(picker, &drop);
SendMessageW(picker, WM_LBUTTONDOWN, MK_LBUTTON, MAKELPARAM(8, 8));