Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00cd1262b4 | ||
|
|
7bc734a9a8 | ||
|
|
a19ccaf035 | ||
|
|
60625103f8 | ||
|
|
aa02f2cacb | ||
|
|
6a1e39efa9 | ||
|
|
7242c3f775 | ||
|
|
a43f6441b4 | ||
|
|
2f5ae373a1 | ||
|
|
b978361180 | ||
|
|
93b8055085 | ||
|
|
70ee338fc3 | ||
|
|
46b4b51cea | ||
|
|
721c3f0f38 | ||
|
|
47851d75ef |
No files matched your search
@@ -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
@@ -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
@@ -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.
@@ -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 @@
|
||||
51d95639527590b36453a5ef485532a7d2135950222fa197eb431a5309f1a45c KSwitcher-win-x64.zip
|
||||
f6067958677c4045fc5243459fc37522489a7f081969dac42befd07101814e61 KSwitcher-win-x64.zip
|
||||
+80
-35
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,¤t) &&
|
||||
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));
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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,÷r,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
@@ -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.");
|
||||
|
||||
@@ -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)},
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in new issue
Block a user